import importlib
import copy
import os
import re
import sys
import warnings
from collections import OrderedDict
import traceback

import h5py
import numpy as np

import deeprank
from deeprank.models.variant import PdbVariantSelection
from deeprank import config
from deeprank.config import logger
from deeprank.generate import GridTools as gt
from deeprank.operate import hdf5data
from deeprank.operate.pdb import get_pdb_path

import pdb2sql
from pdb2sql.align import align as align_along_axis
from pdb2sql.align import align_interface

try:
    from tqdm import tqdm
except ImportError:
    def tqdm(x):
        return x

try:
    from pycuda import driver, compiler, gpuarray, tools
    import pycuda.autoinit
except ImportError:
    pass

def _printif(string, cond): return print(string) if cond else None

class DataGenerator(object):

    # In the feature dataset, the first three columns are xyz.
    # The rest are feature values.
    FEATURE_POSITION_OFFSET = 3

    def __init__(self, environment, variants,
                 align=None,
                 compute_targets=None, compute_features=None,
                 data_augmentation=None, hdf5='database.h5',
                 mpi_comm=None):
        """Generate the data (features/targets/maps) required for deeprank.

        Args:
            environment (Environment): the environment settings
            variants (list(PdbVariantSelection)): the selected variants
            align (dict, optional): Dicitionary to align the compexes,
                                    e.g. align = {"selection":{"chainID":["A","B"]}, "axis":"z"}
                                    e.g. align = {"selection":{"chainID":["A","B"]}, "plane":"xy"}
                                    if "selection" is not specified the entire complex is used for alignement
            compute_targets (list(str), optional): List of python files computing the targets,
                "pdb_native" must be set if having targets to compute.
            compute_features (list(str), optional): List of python files computing the features
            data_augmentation (int, optional): Number of rotations performed one each complex
            hdf5 (str, optional): name of the hdf5 file where the data is saved, default to 'database.h5'
            mpi_comm (MPI_COMM): MPI COMMUNICATOR

        Raises:
            NotADirectoryError: if the source are not found

        Example:

            >>> from deeprank.generate import *
            >>> from deeprank.models.variant import *
            >>> from deeprank.domain.amino_acid import *
            >>> # sources to assemble the data base
            >>> variant = PdbVariantSelection(pdb_ac="1AK4",
            >>>                               chain_id="C",
            >>>                               residue_number=10,
            >>>                               wild_type_amino_acid=isoleucine,
            >>>                               variant_amino_acid=valine,
            >>>                               pssm_paths_by_chain={"C": "pssm_new/1AK4.C.pssm",
            >>>                                                    "D": "pssm_new/1AK4.D.pssm"},
            >>>                               variant_class=VariantClass.BENIGN)
            >>> h5file = '1ak4.hdf5'
            >>>
            >>> #init the data assembler
            >>> database = DataGenerator([variant],
            >>>                          data_augmentation=None,
            >>>                          compute_targets=['deeprank.targets.class'],
            >>>                          compute_features=['deeprank.features.atomic_contacts',
            >>>                                            'deeprank.features.accessibility',
            >>>                                            'deeprank.features.neighbour_profile'],
            >>>                          hdf5=h5file)
        """

        self.environment = environment

        self.variants = variants
        logger.debug("preprocess with {} variants".format(len(variants)))

        self.align = align
        logger.debug("preprocess with align={}".format(align))

        self.compute_targets = compute_targets
        self.compute_features = compute_features
        logger.debug("preprocess with targets={}, features={}".format(compute_targets, compute_features))

        self.data_augmentation = data_augmentation
        logger.debug("preprocess with data_augmentation={}".format(data_augmentation))

        self.hdf5 = hdf5

        self.mpi_comm = mpi_comm

        self.feature_error = []
        self.grid_error = []
        self.map_error = []

        self.logger = logger

# ====================================================================================
#
#       CREATE THE DATABASE ALL AT ONCE IF ALL OPTIONS ARE GIVEN
#
# ====================================================================================

    def create_database(
            self,
            verbose=False,
            remove_error=True,
            prog_bar=False,
            distance_cutoff=10.0,
            random_seed=None):
        """Create the hdf5 file architecture and compute the features/targets.

        Args:
            verbose (bool, optional): Print creation details
            remove_error (bool, optional): remove the groups that errored
            prog_bar (bool, optional): use tqdm
            distance_cutoff (float): max distance from center to include atoms, defaults to 10.0 Å
            random_seed (int): random seed for getting rotation axis and angle

        Raises:
            ValueError: If creation of the group errored.

        Example:

        >>> # sources to assemble the data base
        >>> variant = PdbVariantSelection(pdb_ac="1AK4",
        >>>                               chain_id="C",
        >>>                               residue_number=10,
        >>>                               amino_acid="T",
        >>>                               pssm_paths_by_chain={"C": "pssm_new/1AK4.C.pssm",
        >>>                                                    "D": "pssm_new/1AK4.D.pssm"},
        >>>                               variant_class=VariantClass.BENIGN)
        >>> h5file = '1ak4.hdf5'
        >>>
        >>> #init the data assembler
        >>> database = DataGenerator([variant],
        >>>                          data_augmentation=None,
        >>>                          compute_targets  = ['deeprank.targets.variant_class'],
        >>>                          compute_features = ['deeprank.features.AtomicFeature',
        >>>                                              'deeprank.features.PSSM_IC',
        >>>                                              'deeprank.features.BSA'],
        >>>                          hdf5=h5file)
        >>>
        >>> #create new files
        >>> database.create_database(prog_bar=True)
        """

        # deals with the parallelization
        self.local_variants = self.variants

        if self.mpi_comm is not None:
            rank = self.mpi_comm.Get_rank()
            size = self.mpi_comm.Get_size()
        else:
            size = 1

        if size > 1:
            if rank == 0:
                variants_divided = [self.variants[i::size] for i in range(size)]
                self.local_variants = variants_divided[0]
                # send to other procs
                for iP in range(1, size):
                    self.mpi_comm.send(variants_divided[iP], dest=iP, tag=11)
            else:
                # receive procs
                self.local_variants = self.mpi_comm.recv(source=0, tag=11)
            # change hdf5 name
            h5path, h5name = os.path.split(self.hdf5)
            self.hdf5 = os.path.join(h5path, f"{rank:03d}_{h5name}")

        # set metadata to hdf5 file
        with h5py.File(self.hdf5, 'w') as f5:

            f5.attrs['DeepRank_version'] = deeprank.__version__
            if self.compute_features is not None:
                f5.attrs['features'] = self.compute_features
            if self.compute_targets is not None:
                f5.attrs['targets'] = self.compute_targets

        ##################################################
        # Start generating HDF5 database
        ##################################################
        self.logger.info(
            f'\n# Start creating HDF5 database: {self.hdf5}')

        # get the local progress bar
        desc = '{:25s}'.format('Creating database')
        variant_tqdm = tqdm(self.local_variants, desc=desc,
                           disable=not prog_bar)

        for variant in variant_tqdm:

            pdb_path = get_pdb_path(self.environment.pdb_root, variant.pdb_ac)

            variant_tqdm.set_postfix(variant=os.path.basename(pdb_path))
            variant_name = hdf5data.get_variant_group_name(variant)
            self.logger.info(f'\nProcessing variant: {variant_name}')

            # names of the variant
            variant_aug_name_list = []

            with h5py.File(self.hdf5, 'a') as f5:

                ################################################
                #   get the pdbs of the conformation and its ref
                #   for the original data (not augmetned one)
                ################################################

                if verbose:
                    self.logger.info(
                        f'\nVariant: {variant_name}.'
                        f'\nStart generating top HDF5 group "{variant_name}"...'
                        f'\n{"":4s}Reading PDB data into database...')

                # get the bare name of the variant
                # and define the name of the native
                # i.e. 1AK4-m1324578236_100w -> 1AK4-m1324578236
                bare_variant_name = variant_name.split('_')[0]
                ref_name = bare_variant_name + '.pdb'

                # check if we have a decoy or native
                # and find the reference
                if variant_name == bare_variant_name:
                    ref = pdb_path
                else:
                    ref = None

                # create a subgroup for the variant
                variant_group = f5.require_group(variant_name)
                variant_group.attrs['type'] = 'variant'
                hdf5data.store_variant(variant_group, variant)

                # add the ref and the complex
                #self._add_pdb(variant_group, pdb_path, 'complex')
                #if ref is not None:
                #    self._add_pdb(variant_group, ref, 'native')

                if verbose:
                    self.logger.info(
                        f'{"":4s}Generated subgroup "complex"'
                        f' to store pdb data of the current model.')
                    if ref:
                        self.logger.info(
                            f'{"":4s}Generated subgroup "native"'
                            f' to store pdb data of the reference molecule.')

                ################################################
                #   add the features
                ################################################
                feature_error_flag = False  # when False: success; when True: failed

                if self.compute_features is not None:
                    if verbose:
                        self.logger.info(
                            f'{"":4s}Calculating features...')

                    variant_group.require_group('features')

                    feature_error_flag = self._compute_features(self.compute_features,
                                                                self.environment,
                                                                distance_cutoff,
                                                                variant_group['features'],
                                                                variant,
                                                                self.logger)
                    if feature_error_flag:
                        self.feature_error += [variant_name]
                        # ignore the targets/grid/augmentation computation
                        # and directly go to next variant. Remove errored
                        # variant later.
                        # Otherwise, keep computing and report errored variant.
                        if remove_error:
                            continue

                    if verbose:
                        if not feature_error_flag or not remove_error:
                            self.logger.info(
                                f'\n{"":4s}Generated subgroup "features"'
                                f' to store xyz-based feature values.'
                                f'{"":4s}Generated subgroup "features_raw"'
                                f' to store human read feature values')

                ################################################
                #   add the targets
                ################################################
                if self.compute_targets is not None:
                    if verbose:
                        self.logger.info(
                            f'{"":4s}Calculating targets...')

                    variant_group.require_group('targets')

                    self._compute_targets(self.compute_targets,
                                          variant,
                                          variant_group['targets'])

                    if verbose:
                        self.logger.info(
                            f'{"":4s}Generated subgroup "targets" '
                            f'to store targets, such as BIN_CLASS, dockQ, etc.')

                ################################################
                #   add the box center
                ################################################
                if verbose:
                    self.logger.info(
                        f'{"":4s}Calculating grid box center...')

                grid_error_flag = False
                variant_group.require_group('grid_points')

                try:
                    center = DataGenerator.get_grid_center(self.environment, variant)
                    variant_group['grid_points'].create_dataset('center', data=center, chunks=True)
                    if verbose:
                        self.logger.info(
                            f'{"":4s}Generated subgroup "grid_points"'
                            f' to store grid box center.')

                except:
                    grid_error_flag = True
                    self.grid_error += [variant_name]
                    self.logger.exception("Error while computing center for {}: {}".format(variant, traceback.format_exc()))
                    if remove_error:
                        continue

                ################################################
                #   DATA AUGMENTATION
                ################################################

                # GET ALL THE NAMES
                if self.data_augmentation is not None:
                    variant_aug_name_list = [
                        variant_name +
                        '_r%03d' %
                        (idir +
                         1) for idir in range(
                            self.data_augmentation)]
                else:
                    variant_aug_name_list = []

                if verbose and variant_aug_name_list:
                    self.logger.info(
                        f'{"":2s}Start augmenting data'
                        f' with {self.data_augmentation} times...')

                # loop over the variants
                for variant_aug_name in variant_aug_name_list:

                    # create a subgroup for the molecule
                    variant_group = f5.require_group(variant_aug_name)
                    variant_group.attrs['type'] = 'variant'
                    hdf5data.store_variant(variant_group, variant)

                    # copy the ref into it
                    #if ref is not None:
                    #    self._add_pdb(variant_group, ref, 'native')

                    # get the rotation axis and angle
                    if self.align is None:
                        rotation_axis, rotation_angle = pdb2sql.transform.get_rot_axis_angle(random_seed)
                    else:
                        rotation_axis, rotation_angle = self._get_aligned_rotation_axis_angle(random_seed, self.align)

                    # create the new pdb and get molecule center
                    # molecule center is the origin of rotation)
                    rotation_center = self._add_aug_pdb(variant_group, variant,
                                                        'complex', rotation_axis, rotation_angle)

                    # copy the targets/features
                    if 'targets' in f5[variant_name]:
                        f5.copy(variant_name + '/targets/', variant_group)
                    f5.copy(variant_name + '/features/', variant_group)

                    # rotate the feature
                    self._rotate_feature(variant_group, rotation_axis, rotation_angle, rotation_center)

                    # store the rotation axis/angl/center as attriutes
                    # in case we need them later
                    variant_group.attrs['rotation_axis'] = rotation_axis
                    variant_group.attrs['rotation_angle'] = rotation_angle
                    variant_group.attrs['rotation_center'] = rotation_center

                # cache aug variants if original variant has errored features
                if feature_error_flag:
                    self.feature_error += variant_aug_name_list
                if grid_error_flag:
                    self.grid_error += variant_aug_name_list

                if verbose and variant_aug_name_list:
                    self.logger.info(
                        f'{"":2s}Completed data augmentation'
                        f' and generated top HDF5 groups, e.g. {variant_aug_name}.')

                ################################################
                # Successul message
                ################################################
                if verbose:
                    self.logger.info(
                        f'\nSuccessfully generated top HDF5 group "{variant_name}".\n')

        ##################################################
        # Post processing
        ##################################################
        #  Remove errored variants
        errored_variants = list(set(self.feature_error + self.grid_error))
        if len(errored_variants) > 0:
            if remove_error:
                with h5py.File(self.hdf5, 'a') as f5:
                    for variant_name in errored_variants:
                        del f5[variant_name]
                if self.feature_error:
                    self.logger.info(
                        f'Molecules with errored features are removed:'
                        f'\n{self.feature_error}')
                if self.grid_error:
                    self.logger.info(
                        f'Molecules with errored grid points are removed:'
                        f'\n{self.grid_error}')
            else:
                if self.feature_error:
                    self.logger.warning(
                        f'The following variants have errored features:'
                        f'\n{self.feature_error}')
                if self.grid_error:
                    self.logger.warning(
                        f'The following variants have errored grid points:'
                        f'\n{self.grid_error}')

        # close the file
        self.logger.info(
            f'\n# Successfully created database: {self.hdf5}\n')

    def aug_data(self, augmentation, keep_existing_aug=True, random_seed=None):
        """Augment exiting original PDB data and features.

        Args:
            augmentation(int): Times of augmentation
            keep_existing_aug (bool, optional): Keep existing augmentated data.
                If False, existing aug will be removed. Defaults to True.

        Examples:
            >>> database = DataGenerator(h5='database.h5')
            >>> database.aug_data(augmentation=3, append=True)
            >>> grid_info = {
            >>>     'number_of_points': [20,20,20],
            >>>     'resolution': [1.,1.,1.],
            >>>     'atomic_densities': {'C':1.7, 'N':1.55, 'O':1.52, 'S':1.8},
            >>>     }
            >>> database.map_features(grid_info)
        """

        # check if file exists
        if not os.path.isfile(self.hdf5):
            raise FileNotFoundError(
                'File %s does not exists' % self.hdf5)

        # get the folder names
        f5 = h5py.File(self.hdf5, 'a')
        fnames = f5.keys()

        # get the non rotated ones
        fnames_original = list(
            filter(lambda x: not re.search(r'_r\d+$', x), fnames))

        # get the rotated ones
        fnames_augmented = list(
            filter(lambda x: re.search(r'_r\d+$', x), fnames))

        aug_id_start = 0
        if keep_existing_aug:
            exiting_augs = list(
                filter(lambda x: re.search(fnames_original[0] + r'_r\d+$', x), fnames_augmented))
            aug_id_start += len(exiting_augs)
        else:
            for i in fnames_augmented:
                del f5[i]

        self.logger.info(
            f'{"":s}\n# Start augmenting data'
            f' with {augmentation} times...')

        # GET ALL THE NAMES
        for variant_name in fnames_original:
            variant_aug_name_list = [
                variant_name + '_r%03d' % (idir + 1) for idir in
                range(aug_id_start, aug_id_start + augmentation)]

            variant = hdf5data.load_variant(f5[variant_name])

            # loop over the complexes
            for variant_aug_name in variant_aug_name_list:

                # create a subgroup for the variant
                variant_group = f5.require_group(variant_aug_name)
                variant_group.attrs['type'] = 'variant'
                hdf5data.store_variant(variant_group, variant)

                # copy the ref into it
                if 'native' in f5[variant_name]:
                    f5.copy(variant_name + '/native', variant_group)

                # get the rotation axis and angle
                if self.align is None:
                    rotation_axis, rotation_angle = pdb2sql.transform.get_rot_axis_angle(random_seed)
                else:
                    rotation_axis, rotation_angle = self._get_aligned_rotation_axis_angle(random_seed, self.align)

                # create the new pdb and get molecule center
                # molecule center is the origin of rotation)
                rotation_center = self._add_aug_pdb(variant_group, variant, 'complex',
                                                    rotation_axis, rotation_angle)

                # copy the targets/features
                if 'targets' in f5[variant_name]:
                    f5.copy(variant_name + '/targets/', variant_name)
                f5.copy(variant_name + '/features/', variant_group)

                # rotate the feature
                self._rotate_feature(variant_group, rotation_axis, rotation_angle, rotation_center)

                # store the rotation axis/angl/center as attriutes
                # in case we need them later
                variant_group.attrs['rotation_axis'] = rotation_axis
                variant_group.attrs['rotation_angle'] = rotation_angle
                variant_group.attrs['rotation_center'] = rotation_center
        f5.close()
        self.logger.info(
            f'\n# Successfully augmented data in {self.hdf5}')

# ====================================================================================
#
#       ADD FEATURES TO AN EXISTING DATASET
#
# ====================================================================================

    def add_feature(self, remove_error=True, prog_bar=True, distance_cutoff=10.0):
        """Add a feature to an existing hdf5 file.

        Args:
            remove_error (bool): remove errored variant
            prog_bar (bool, optional): use tqdm
            distance_cutoff (float, optional): max distance from center to include atoms, default 10.0 Å

        Example:

        >>> h5file = '1ak4.hdf5'
        >>>
        >>> #init the data assembler
        >>> database = DataGenerator(compute_features  = ['deeprank.features.ResidueDensity'],
        >>>                          hdf5=h5file)
        >>>
        >>> database.add_feature(remove_error=True, prog_bar=True)
        """

        # check if file exists
        if not os.path.isfile(self.hdf5):
            raise FileNotFoundError(
                'File %s does not exists' % self.hdf5)

        # get the folder names
        f5 = h5py.File(self.hdf5, 'a')
        fnames = f5.keys()

        # get the non rotated ones
        fnames_original = list(
            filter(lambda x: not re.search(r'_r\d+$', x), fnames))

        # get the rotated ones
        fnames_augmented = list(
            filter(lambda x: re.search(r'_r\d+$', x), fnames))

        # check feature_error
        if not self.feature_error:
            self.feature_error = []

        # computes the features of the original
        desc = '{:25s}'.format('Add features')
        for cplx_name in tqdm(
                fnames_original,
                desc=desc,
                ncols=100,
                disable=not prog_bar):

            # variant group
            variant_group = f5[cplx_name]

            variant = hdf5data.load_variant(variant_group)

            error_flag = False
            if self.compute_features is not None:

                # the internal features
                variant_group.require_group('features')
                variant_group.require_group('features_raw')

                error_flag = self._compute_features(self.compute_features,
                                                    self.environment,
                                                    distance_cutoff,
                                                    variant_group['features'],
                                                    variant,
                                                    self.logger)

                if error_flag:
                    self.feature_error += [cplx_name]

        # copy the data from the original to the augmented
        for cplx_name in fnames_augmented:

            # group of the variant
            aug_variant_group = f5[cplx_name]

            # get the source group
            variant_name = re.split(r'_r\d+', variant_group.name)[0]
            src_variant_group = f5[variant_name]

            # get the rotation parameters
            axis = aug_variant_group.attrs['axis']
            angle = aug_variant_group.attrs['angle']
            center = aug_variant_group.attrs['center']

            # copy the features to the augmented
            for k in variant_group['features']:
                if k not in aug_variant_group['features']:

                    # copy
                    data = src_variant_group['features/' + k][()]
                    aug_variant_group.require_group('features')

                    aug_variant_group.create_dataset("features/" + k, data=data, compression='lzf', chunks=True)

                    # rotate
                    self._rotate_feature(aug_variant_group, axis, angle, center, feat_name=[k])

        # find errored augmented variants
        tmp_aug_error = []
        for variant_name in self.feature_error:
            tmp_aug_error += list(filter(lambda x: variant_name in x,
                                         fnames_augmented))
        self.feature_error += tmp_aug_error

        #  Remove errored variants
        if self.feature_error:
            if remove_error:
                for variant_name in self.feature_error:
                    del f5[variant_name]
                self.logger.info(
                    f'Molecules with errored features are removed:\n'
                    f'{self.feature_error}')
            else:
                self.logger.warning(
                    f"The following variants have errored features:\n"
                    f'{self.feature_error}')

        # close the file
        f5.close()

# ====================================================================================
#
#       ADD TARGETS TO AN EXISTING DATASET
#
# ====================================================================================

    def add_unique_target(self, targdict):
        """Add identical targets for all the complexes in the datafile.

        This is usefull if you want to add the binary class of all the complexes
        created from decoys or natives

        Args:
            targdict (dict): Example: {'DOCKQ':1.0}

        >>> database = DataGenerator(hdf5='1ak4.hdf5')
        >>> database.add_unique_target({'DOCKQ':1.0})
        """

        # check if file exists
        if not os.path.isfile(self.hdf5):
            raise FileNotFoundError(
                'File %s does not exists' % self.hdf5)

        f5 = h5py.File(self.hdf5, 'a')
        for variant_name in list(f5.keys()):
            targrp = f5[variant_name].require_group('targets')
            for name, value in targdict.items():
                targrp.create_dataset(name, data=np.array([value]), chunks=True)
        f5.close()

    def add_target(self, prog_bar=False):
        """Add a target to an existing hdf5 file.

        Args:
            prog_bar (bool, optional): Use tqdm

        Example:

        >>> h5file = '1ak4.hdf5'
        >>>
        >>> #init the data assembler
        >>> database = DataGenerator(compute_targets =['deeprank.targets.binary_class'],
        >>>                          hdf5=h5file)
        >>>
        >>> database.add_target(prog_bar=True)
        """

        # check if file exists
        if not os.path.isfile(self.hdf5):
            raise FileNotFoundError(
                'File %s does not exists' % self.hdf5)

        # name of the hdf5 file
        f5 = h5py.File(self.hdf5, 'a')

        # get the folder names
        fnames = f5.keys()

        # get the non rotated ones
        fnames_original = list(
            filter(lambda x: not re.search(r'_r\d+$', x), fnames))
        fnames_augmented = list(
            filter(lambda x: re.search(r'_r\d+$', x), fnames))

        # compute the targets  of the original
        desc = '{:25s}'.format('Add targets')

        for variant_name in tqdm(fnames_original, desc=desc,
                              ncols=100, disable=not prog_bar):

            # group of the variant
            variant_group = f5[variant_name]

            # add the targets
            if self.compute_targets is not None:

                variant = hdf5data.load_variant(f5[variant_name])

                variant_group.require_group('targets')
                self._compute_targets(self.compute_targets,
                                      variant,
                                      variant_group['targets'])

        # copy the targets of the original to the rotated
        for cplx_name in fnames_augmented:

            # group of the variant
            aug_variant_group = f5[cplx_name]

            # get the source group
            variant_name = re.split(r'_r\d+', variant_group.name)[0]
            src_variant_group = f5[variant_name]

            # copy the targets to the augmented
            for k in variant_group['targets']:
                if k not in aug_variant_group['targets']:
                    data = src_variant_group['targets/' + k][()]
                    aug_variant_group.require_group('targets')
                    aug_variant_group.create_dataset("targets/" + k, data=data, chunks=True)

        # close the file
        f5.close()

    def realign_complexes(self, align, compute_features=None, pssm_source=None, distance_cutoff=10.0):
        """Align all the complexes already present in the HDF5.

        Arguments:
            align {dict} -- alignement dictionary (see __init__)

        Keyword Arguments:
            compute_features {list} -- list of features to be computed
                                       if None computes the features specified in
                                       the attrs['features'] of the file (if present)
            pssm_source {str} -- path of the pssm files. If None the source specfied in
                                 the attrs['pssm_source'] will be used (if present) (default: {None})
            distance_cutoff {float} -- max distance from the center to include atoms (default 10.0 Å)

        Raises:
            ValueError: If no PSSM detected

        Example:

        >>> database = DataGenerator(hdf5='1ak4.hdf5')
        >>> # if comute_features and pssm_source are not specified
        >>> # the values in hdf5.attrs['features'] and hdf5.attrs['pssm_source'] will be used
        >>> database.realign_complex(align={'axis':'x'},
        >>>                          compute_features['deeprank.features.X'],
        >>>                           pssm_source='./1ak4_pssm/')
        """

        f5 = h5py.File(self.hdf5, 'a')

        variant_names = f5.keys()
        self.logger.info(
            f'\n# Start aligning the HDF5 database: {self.hdf5}')

        # deal with the features
        if self.compute_features is None:
            if compute_features is None:
                if 'features' in f5.attrs:
                    self.compute_features = list(f5.attrs['features'])
            else:
                self.compute_features = compute_features

        # deal with the pssm source
        if self.pssm_source is not None:
            config.PATH_PSSM_SOURCE = self.pssm_source

        elif pssm_source is not None:
            config.PATH_PSSM_SOURCE = pssm_source

        elif 'pssm_source' in f5.attrs:
            config.PATH_PSSM_SOURCE = f5.attrs['pssm_source']
        else:
            raise ValueError('No pssm source detected')

        # loop over the complexes
        desc = '{:25s}'.format('Add features')
        for variant_name in tqdm(variant_names, desc=desc, ncols=100):

            variant = hdf5data.load_variant(f5[variant_name])

            # align the pdb
            variant_group = f5[variant_name]
            pdb = variant_group['complex'][()]

            sqldb = self._get_aligned_sqldb(pdb, align)
            data = sqldb.sql2pdb()

            data = np.array(data).astype('|S78')
            variant_group['complex'][...] = data

            # remove prexisting features
            old_dir = ['features', 'features_raw', 'mapped_features']
            for od in old_dir:
                if od in variant_group:
                    del variant_group[od]

            # the internal features
            variant_group.require_group('features')
            variant_group.require_group('features_raw')

            # compute features
            error_flag = self._compute_features(self.compute_features,
                                                self.environment,
                                                distance_cutoff,
                                                variant_group['features'],
                                                variant,
                                                self.logger)

        f5.close()

# ====================================================================================
#
#       PRECOMPUTE THE GRID POINTS
#
# ====================================================================================

    @staticmethod
    def get_grid_center(environment, variant):
        "gets the C-alpha position of the variant residue"

        pdb_path = get_pdb_path(environment.pdb_root, variant.pdb_ac)

        pdb = pdb2sql.pdb2sql(pdb_path)
        try:
            c_alpha_positions = pdb.get_xyz(chainID=variant.chain_id,
                                            resSeq=variant.residue_number,
                                            name="CA")
        finally:
            pdb._close()

        if len(c_alpha_positions) == 0:
            raise ValueError("C-alpha of chain {} residue {} not found in {}"
                             .format(variant.chain_id, variant.residue_number, pdb_path))

        return c_alpha_positions[0]

    def precompute_grid(self,
                        grid_info,
                        contact_distance=10.0,
                        prog_bar=False,
                        time=False,
                        try_sparse=True):

        # name of the hdf5 file
        f5 = h5py.File(self.hdf5, 'a')

        # check all the input PDB files
        variant_names = f5.keys()

        # get the local progress bar
        desc = '{:25s}'.format('Precompute grid points')
        variant_tqdm = tqdm(variant_names, desc=desc, disable=not prog_bar)

        if not prog_bar:
            print(desc, ':', self.hdf5)
            sys.stdout.flush()

        # loop over the data files
        for variant_name in variant_tqdm:

            variant_tqdm.set_postfix(variant=variant_name)

            variant = hdf5data.load_variant(f5[variant_name])

            # compute the data we want on the grid
            gt.GridTools(environment=self.environment,
                         variant_group=f5[variant_name], variant=variant,
                         grid_center=DataGenerator.get_grid_center(self.environment, variant),
                         number_of_points=grid_info['number_of_points'],
                         resolution=grid_info['resolution'],
                         contact_distance=contact_distance,
                         time=time,
                         prog_bar=prog_bar,
                         try_sparse=try_sparse)

        f5.close()

# ====================================================================================
#
#       MAP THE FEATURES TO THE GRID
#
# ====================================================================================

    def map_features(self, grid_info={},
                     cuda=False, gpu_block=None,
                     cuda_kernel='kernel_map.c',
                     cuda_func_name='gaussian',
                     try_sparse=True,
                     reset=False, use_tmpdir=False,
                     time=False,
                     prog_bar=True, grid_prog_bar=False,
                     remove_error=True):
        """Map the feature on a grid of points centered at the interface.

        If features to map are not given, they will be are automatically
        determined for each variant. Otherwise, given features will be mapped
        for all variants (i.e. existing mapped features will be recalculated).

        Args:
            grid_info (dict): Informaton for the grid.
                See deeprank.generate.GridTools.py for details.
            cuda (bool, optional): Use CUDA
            gpu_block (None, optional): GPU block size to be used
            cuda_kernel (str, optional): filename containing CUDA kernel
            cuda_func_name (str, optional): The name of the function in the kernel
            try_sparse (bool, optional): Try to save the grids as sparse format
            reset (bool, optional): remove grids if some are already present
            use_tmpdir (bool, optional): use a scratch directory
            time (bool, optional): time the mapping process
            prog_bar (bool, optional): use tqdm for each variant
            grid_prog_bar (bool, optional): use tqdm for each grid
            remove_error (bool, optional): remove the data that errored

        Example:

        >>> #init the data assembler
        >>> database = DataGenerator(hdf5='1ak4.hdf5')
        >>>
        >>> # map the features
        >>> grid_info = {
        >>>     'number_of_points': [20,20,20],
        >>>     'resolution': [1.,1.,1.],
        >>>     'atomic_densities': {'C':1.7, 'N':1.55, 'O':1.52, 'S':1.8},
        >>> }
        >>>
        >>> database.map_features(grid_info,try_sparse=True,time=False,prog_bar=True)
        """

        logger.debug("preprocess, map features with grid_info={}".format(grid_info))
        logger.debug("preprocess, map features with try_sparse={}".format(try_sparse))

        # default CUDA
        cuda_func = None
        cuda_atomic = None

        # disable CUDA when using MPI
        if self.mpi_comm is not None:
            if self.mpi_comm.Get_size() > 1:
                if cuda:
                    self.logger.warning(
                        'CUDA mapping disabled when using MPI')
                    cuda = False

        # check all the input PDB files
        with h5py.File(self.hdf5, 'r') as f5:
            variant_names = list(f5.keys())

        if len(variant_names) == 0:
            raise ValueError(f'No variants found in {self.hdf5}.')

        ################################################################
        # Check grid_info
        ################################################################
        # fills in the grid data if not provided: default = NONE
        grid_info_ref = copy.deepcopy(grid_info)
        grinfo = ['number_of_points', 'resolution']
        for gr in grinfo:
            if gr not in grid_info:
                grid_info[gr] = None

        # by default we do not map atomic densities
        if 'atomic_densities' not in grid_info:
            grid_info['atomic_densities'] = None

        # fills in the features mode if somes are missing: default = IND
        modes = ['atomic_densities_mode', 'feature_mode']
        for m in modes:
            if m not in grid_info:
                grid_info[m] = 'ind'

        ################################################################
        #
        ################################################################
        # sanity check for cuda
        if cuda and gpu_block is None:  # pragma: no cover
            self.logger.info(
                f'GPU block automatically set to 8 x 8 x 8. '
                f'You can set block size with gpu_block=[n,m,k]')
            gpu_block = [8, 8, 8]

        # initialize cuda
        if cuda:  # pragma: no cover

            # compile cuda module
            npts = grid_info['number_of_points']
            res = grid_info['resolution']
            module = self._compile_cuda_kernel(cuda_kernel, npts, res)

            # get the cuda function for the atomic/residue feature
            cuda_func = self._get_cuda_function(
                module, cuda_func_name)

            # get the cuda function for the atomic densties
            cuda_atomic_name = 'atomic_densities'
            cuda_atomic = self._get_cuda_function(
                module, cuda_atomic_name)

        # get the local progress bar
        desc = '{:25s}'.format('Map Features')
        variant_tqdm = tqdm(variant_names, desc=desc, disable=not prog_bar)

        if not prog_bar:
            self.logger.info(f'{desc}: {self.hdf5}')

        # loop over the data files
        for variant_name in variant_tqdm:

            with h5py.File(self.hdf5, 'a') as f5:

                variant_tqdm.set_postfix(variant=variant_name)

                variant = hdf5data.load_variant(f5[variant_name])
                grid_center = self.get_grid_center(self.environment, variant)

                # Determine which feature to map
                # if feature not given, then determine it for each variant
                if 'feature' not in grid_info_ref:
                    # if we havent mapped anything yet or if we reset
                    if 'mapped_features' not in list(f5[variant_name].keys()) or reset:
                        grid_info['feature'] = list(
                            f5[variant_name + '/features'].keys())

                    # if we have already mapped stuff
                    elif 'mapped_features' in list(f5[variant_name].keys()):

                        # feature name
                        all_feat = list(f5[variant_name + '/features'].keys())

                        # feature already mapped
                        mapped_feat = list(
                            f5[variant_name + '/mapped_features/Feature_ind'].keys())

                        # we select only the feture that were not mapped yet
                        grid_info['feature'] = []
                        for feat_name in all_feat:
                            if not any(map(lambda x: x.startswith(feat_name + '_'),
                                           mapped_feat)):
                                grid_info['feature'].append(feat_name)

                try:
                    # compute the data we want on the grid
                    gt.GridTools(
                        environment=self.environment,
                        variant_group=f5[variant_name],
                        variant=variant,
                        grid_center=grid_center,
                        number_of_points=grid_info['number_of_points'],
                        resolution=grid_info['resolution'],
                        atomic_densities=grid_info['atomic_densities'],
                        atomic_densities_mode=grid_info['atomic_densities_mode'],
                        feature=grid_info['feature'],
                        feature_mode=grid_info['feature_mode'],
                        cuda=cuda,
                        gpu_block=gpu_block,
                        cuda_func=cuda_func,
                        cuda_atomic=cuda_atomic,
                        time=time,
                        prog_bar=grid_prog_bar,
                        try_sparse=try_sparse)

                except:
                    self.map_error.append(variant_name)
                    self.logger.exception("Error during the mapping of {}: {}".format(variant, traceback.format_exc()))

        # remove the variants with issues
        if self.map_error:
            if remove_error:
                with h5py.File(self.hdf5, 'a') as f5:
                    for variant_name in self.map_error:
                        del f5[variant_name]
                self.logger.warning(
                    f"Variants with errored feature mapping are removed:\n"
                    f"{self.map_error}")
            else:
                self.logger.warning(
                    f"The following variants have errored feature mapping:\n"
                    f"{self.map_error}")

# ====================================================================================
#
#       REMOVE DATA FROM THE DATA SET
#
# ====================================================================================

    def remove(self, feature=True, pdb=True, points=True, grid=False):
        """Remove data from the data set.

        Equivalent to the cleandata command line tool. Once the data has been
        removed from the file it is impossible to add new features/targets

        Args:
            feature (bool, optional): Remove the features
            pdb (bool, optional): Remove the pdbs
            points (bool, optional): remove teh grid points
            grid (bool, optional): remove the maps
        """

        self.logger.debug('Remove features')

        # name of the hdf5 file
        f5 = h5py.File(self.hdf5, 'a')

        # get the folder names
        variant_names = f5.keys()

        for variant_name in variant_names:

            variant_group = f5[variant_name]

            if feature and 'features' in variant_group:
                del variant_group['features']
                del variant_group['features_raw']
            if pdb and 'complex' in variant_group and 'native' in variant_group:
                del variant_group['complex']
                del variant_group['native']
            if points and 'grid_points' in variant_group:
                del variant_group['grid_points']
            if grid and 'mapped_features' in variant_group:
                del variant_group['mapped_features']

        f5.close()

        # reclaim the space
        os.system('h5repack %s _tmp.h5py' % self.hdf5)
        os.system('mv _tmp.h5py %s' % self.hdf5)

# ====================================================================================
#
#       Simply tune or test the kernel
#
# ====================================================================================

    def _tune_cuda_kernel(self, grid_info, cuda_kernel='kernel_map.c', func='gaussian'):  # pragma: no cover
        """Tune the CUDA kernel using the kernel tuner
        http://benvanwerkhoven.github.io/kernel_tuner/

        Args:
            grid_info (dict): information for the grid definition
            cuda_kernel (str, optional): file containing the kernel
            func (str, optional): function in the kernel to be used

        Raises:
            ValueError: If the tuner has not been used
        """

        try:
            from kernel_tuner import tune_kernel
        except BaseException:
            print(
                'Install the Kernel Tuner: \n \t\t pip install kernel_tuner')
            print('http://benvanwerkhoven.github.io/kernel_tuner/')

        # fills in the grid data if not provided: default = NONE
        grinfo = ['number_of_points', 'resolution']
        for gr in grinfo:
            if gr not in grid_info:
                raise ValueError(
                    '%s must be specified to tune the kernel')

        # define the grid
        nx, ny, nz = grid_info['number_of_points']
        dx, dy, dz = grid_info['resolution']
        lx, ly, lz = nx * dx, ny * dy, nz * dz

        x = np.linspace(0, lx, nx)
        y = np.linspace(0, ly, ny)
        z = np.linspace(0, lz, nz)

        # create the dictionary containing the tune parameters
        tune_params = OrderedDict()
        tune_params['block_size_x'] = [2, 4, 8, 16, 32]
        tune_params['block_size_y'] = [2, 4, 8, 16, 32]
        tune_params['block_size_z'] = [2, 4, 8, 16, 32]

        # define the final grid
        grid = np.zeros(grid_info['number_of_points'])

        # arguments of the CUDA function
        x0, y0, z0 = np.float32(0), np.float32(0), np.float32(0)
        alpha = np.float32(0)
        args = [alpha, x0, y0, z0, x, y, z, grid]

        # dimensionality
        problem_size = grid_info['number_of_points']

        # get the kernel
        kernel = os.path.dirname(
            os.path.abspath(__file__)) + '/' + cuda_kernel
        kernel_code_template = open(kernel, 'r').read()

        npts = grid_info['number_of_points']
        res = grid_info['resolution']
        kernel_code = kernel_code_template % {
            'nx': npts[0], 'ny': npts[1], 'nz': npts[2], 'RES': np.max(res)}
        tunable_kernel = self._tunable_kernel(kernel_code)

        # tune
        tune_kernel(func, tunable_kernel,
                    problem_size, args, tune_params)

# ====================================================================================
#
#       Simply test the kernel
#
# ====================================================================================

    def _test_cuda(self, grid_info, gpu_block=8, cuda_kernel='kernel_map.c', func='gaussian'):  # pragma: no cover
        """Test the CUDA kernel.

        Args:
            grid_info (dict): Information for the grid definition
            gpu_block (int, optional): GPU block size to be used
            cuda_kernel (str, optional): File containing the kernel
            func (str, optional): function in the kernel to be used

        Raises:
            ValueError: If the kernel has not been installed
        """

        from time import time

        # fills in the grid data if not provided: default = NONE
        grinfo = ['number_of_points', 'resolution']
        for gr in grinfo:
            if gr not in grid_info:
                raise ValueError(
                    '%s must be specified to tune the kernel')

        # get the cuda function
        npts = grid_info['number_of_points']
        res = grid_info['resolution']
        module = self._compile_cuda_kernel(cuda_kernel, npts, res)
        cuda_func = self._get_cuda_function(module, func)

        # define the grid
        nx, ny, nz = grid_info['number_of_points']
        dx, dy, dz = grid_info['resolution']
        lx, ly, lz = nx * dx, ny * dy, nz * dz

        # create the coordinate
        x = np.linspace(0, lx, nx)
        y = np.linspace(0, ly, ny)
        z = np.linspace(0, lz, nz)

        # book memp on the gpu
        x_gpu = gpuarray.to_gpu(x.astype(np.float32))
        y_gpu = gpuarray.to_gpu(y.astype(np.float32))
        z_gpu = gpuarray.to_gpu(z.astype(np.float32))
        grid_gpu = gpuarray.zeros(
            grid_info['number_of_points'], np.float32)

        #  make sure we have three block value
        if not isinstance(gpu_block, list):
            gpu_block = [gpu_block] * 3

        #  get the grid
        gpu_grid = [int(np.ceil(n / b))
                    for b, n in zip(gpu_block, grid_info['number_of_points'])]
        print('GPU BLOCK:', gpu_block)
        print('GPU GRID :', gpu_grid)

        xyz_center = np.random.rand(500, 3).astype(np.float32)
        alpha = np.float32(1)
        t0 = time()
        for xyz in xyz_center:
            x0, y0, z0 = xyz
            cuda_func(alpha, x0, y0, z0, x_gpu, y_gpu, z_gpu, grid_gpu,
                      block=tuple(gpu_block), grid=tuple(gpu_grid))

        print('Done in: %f ms' % ((time() - t0) * 1000))

# ====================================================================================
#
#       Routines needed to handle CUDA
#
# ====================================================================================

    @staticmethod
    def _compile_cuda_kernel(cuda_kernel, npts, res):  # pragma: no cover
        """Compile the cuda kernel.

        Args:
            cuda_kernel (str): filename
            npts (tuple(int)): number of grid points in each direction
            res (tuple(float)): resolution in each direction

        Returns:
            compiler.SourceModule: compiled kernel
        """
        # get the cuda kernel path
        kernel = os.path.dirname(
            os.path.abspath(__file__)) + '/' + cuda_kernel
        kernel_code_template = open(kernel, 'r').read()
        kernel_code = kernel_code_template % {
            'nx': npts[0], 'ny': npts[1], 'nz': npts[2], 'RES': np.max(res)}

        # compile the kernel
        mod = compiler.SourceModule(kernel_code)
        return mod

    @staticmethod
    def _get_cuda_function(module, func_name):  # pragma: no cover
        """Get a single function from the compiled kernel.

        Args:
            module (compiler.SourceModule): compiled kernel module
            func_name (str): Name of the funtion

        Returns:
            func: cuda function
        """
        cuda_func = module.get_function(func_name)
        return cuda_func

    # tranform the kernel to a tunable one
    @staticmethod
    def _tunable_kernel(kernel):  # pragma: no cover
        """Make a tunale kernel.

        Args:
            kernel (str): String of the kernel

        Returns:
            TYPE: tunable kernel
        """
        switch_name = {
            'blockDim.x': 'block_size_x',
            'blockDim.y': 'block_size_y',
            'blockDim.z': 'block_size_z'}
        for old, new in switch_name.items():
            kernel = kernel.replace(old, new)
        return kernel

# ====================================================================================
#
#       FILTER DATASET
#
# ===================================================================================

    def _filter_cplx(self):
        """Filter the name of the complexes."""

        # read the class ID
        with open(self.pdb_select) as f:
            pdb_name = f.readlines()
        pdb_name = [name.split()[0] + '.pdb' for name in pdb_name]

        # create the filters
        tmp_path = []
        for name in pdb_name:
            tmp_path += list(filter(lambda x: name in x,
                                    self.pdb_path))

        # update the pdb_path
        self.pdb_path = tmp_path

# ====================================================================================
#
#       FEATURES ROUTINES
#
# ====================================================================================

    @staticmethod
    def _compute_features(feat_list, environment, distance_cutoff, featgrp, variant, logger):
        """Compute the features.

        Args:
            feat_list (list(str)): list of function name, e.g.,
                ['deeprank.features.ResidueDensity',
                'deeprank.features.PSSM_IC']
            environment (Environment): the environment settings
            distance_cutoff (float): max distance from center to include atoms, default 10.0 Å
            featgrp (str): name of the group where to store the xyz feature
            variant (PdbVariantSelection): the selected variant
            logger (logger): name of logger object

        Return:
            bool: error happened or not
        """
        error_flag = False  # when False: success; when True: failed
        for feat in feat_list:
            try:
                feat_module = importlib.import_module(feat, package=None)
                feat_module.__compute_feature__(environment, distance_cutoff, featgrp, variant)

                for feature_key in featgrp:
                    if np.any(np.isnan(featgrp[feature_key][()])):
                        logger.exception("Got NaN output for feature {} for {}".format(feature_key, variant))
                        error_flag = True
                        break
            except:
                logger.exception("Error while computing {} for {}: {}".format(feat, variant, traceback.format_exc()))
                error_flag = True

        return error_flag

# ====================================================================================
#
#       TARGETS ROUTINES
#
# ====================================================================================

    @staticmethod
    def _compute_targets(targ_list, variant, targrp):
        """Compute the targets.

        Args:
            targ_list (list(str)): list of function name
            pdb_data (bytes): PDB translated in btes
            targrp (str): name of the group where to store the targets
            logger (logger): name of logger object
        """
        for targ in targ_list:
            targ_module = importlib.import_module(targ, package=None)
            targ_module.__compute_target__(variant, targrp)

# ====================================================================================
#
#       ADD PDB FILE
#
# ====================================================================================

    def _add_pdb(self, variant_group, pdbfile, name):
        """Add a pdb to a variant group

        Args:
            variant_group (str): variant group where to add the pdb
            pdbfile (str): psb file to add
            name (str): dataset name in the hdf5 variant group
        """

        # no alignement
        if self.align is None:
            # read the pdb and extract the ATOM lines
            with open(pdbfile, 'r') as fi:
                data = [line.split('\n')[0]
                        for line in fi if line.startswith('ATOM')]

        # some alignement
        elif isinstance(self.align, dict):

            sqldb = self._get_aligned_sqldb(pdbfile, self.align)
            data = sqldb.sql2pdb()

        #  PDB default line length is 80
        #  http://www.wwpdb.org/documentation/file-format
        data = np.array(data).astype('|S78')
        variant_group.create_dataset(name, data=data, compression='lzf', chunks=True)

    # @staticmethod
    def _get_aligned_sqldb(self, pdbfile, dict_align):
        """return a sqldb of the pdb that is aligned as specified in the dict

        Arguments:
            pdbfile {str} -- path ot the pdb
            dict_align {dict} -- dictionanry of options to align the pdb
        """
        if 'selection' not in dict_align.keys():
            dict_align['selection'] = {}

        if 'export' not in dict_align.keys():
            dict_align['export'] = False

        if dict_align['selection'] == 'interface':
            raise ValueError("interface alignment is not supported in this version of deeprank")
        else:

            sqldb = align_along_axis(pdbfile, axis=dict_align['axis'],
                                     export=dict_align['export'],
                                     **dict_align['selection'])

        return sqldb

# ====================================================================================
#
#       AUGMENTED DATA
#
# ====================================================================================

    @staticmethod
    def _get_aligned_rotation_axis_angle(random_seed, dict_align):
        """Returns the axis and angle of rotation for data
           augmentation with aligned complexes

        Arguments:
            random_seed {int} -- random seed of rotation
            dict_align {dict} -- the dict describing the alignement

        Returns:
            list(float): axis of rotation
            float: angle of rotation
        """

        if random_seed is not None:
            np.random.seed(random_seed)

        angle = 2 * np.pi * np.random.rand()

        if 'plane' in dict_align.keys():
            if dict_align['plane'] == 'xy':
                axis = [0., 0., 1.]
            elif dict_align['plane'] == 'xz':
                axis = [0., 1., 0.]
            elif dict_align['plane'] == 'yz':
                axis = [1., 0., 0.]
            else:
                raise ValueError("plane must be xy, xz or yz")

        elif 'axis' in dict_align.keys():
            if dict_align['axis'] == 'x':
                axis = [1., 0., 0.]
            elif dict_align['axis'] == 'y':
                axis = [0., 1., 0.]
            elif dict_align['axis'] == 'z':
                axis = [0., 0., 1.]
            else:
                raise ValueError("axis must be x, y or z")
        else:
            raise ValueError('dict_align must contains plane or axis')

        return axis, angle

    # add a rotated pdb structure to the database
    def _add_aug_pdb(self, variant_group, variant, name, axis, angle):
        """Add augmented pdbs to the dataset.

        Args:
            variant_group (h5py.Group): name of the variant group
            pdbfile (str): pdb file name
            name (str): name of the dataset
            axis (list(float)): axis of rotation
            angle (float): angle of rotation
            dict_align (dict): dict for alignement of the original pdb

        Returns:
            list(float): center of the variant
        """

        pdb_path = get_pdb_path(self.environment.pdb_root, variant.pdb_ac)

        # create the sqldb and extract positions
        if self.align is None:
            sqldb = pdb2sql.pdb2sql(pdb_path)
        else:
            sqldb = self._get_aligned_sqldb(pdb_path, self.align)

        # rotate the positions
        pdb2sql.transform.rot_axis(sqldb, axis, angle)

        # get molecule center
        center = self.get_grid_center(self.environment, variant)

        # get the pdb-format data
        data = sqldb.sql2pdb()
        data = np.array(data).astype('|S78')

        variant_group.create_dataset(name, data=data, compression='lzf', chunks=True)

        # close the db
        sqldb._close()

        return center

    # rotate th xyz-formatted feature in the database

    @staticmethod
    def _rotate_feature(variant_group, axis, angle, center, feat_name='all'):
        """Rotate the raw feature values.

        Args:
            variant group (str): name pf the variant group
            axis (list(float)): axis of rotation
            angle (float): angle of rotation
            center (list(float)): center of rotation
            feat_name (str): name of the feature to rotate or 'all'
        """
        if feat_name == 'all':
            feat = list(variant_group['features'].keys())
        else:
            feat = feat_name
            if not isinstance(feat, list):
                feat = list(feat)

        for fn in feat:

            # extract the data
            data = variant_group['features/' + fn][()]

            # if data not empty
            if data.shape[0] != 0:

                # xyz
                xyz = data[:, :DataGenerator.FEATURE_POSITION_OFFSET]

                # get rotated xyz
                xyz_rot = pdb2sql.transform.rot_xyz_around_axis(
                    xyz, axis, angle, center)

                # put back the data
                variant_group['features/' + fn][:, :DataGenerator.FEATURE_POSITION_OFFSET] = xyz_rot


from xarm.wrapper import XArmAPI
ip = "192.168.1.240"
speed = 50
arm = XArmAPI(ip)
arm.clean_warn()
arm.clean_error()
arm.motion_enable(enable=True)
arm.set_mode(0)
arm.set_state(state=0)
arm.set_servo_angle(angle=[-0.1, -20.1, 0.2, 21.9, 0.1,
                    42.5, 1.4], speed=speed, is_radian=False, wait=False)
arm.disconnect()


class TokenFutures:
    def __init__(self, symbol, binance_client):
        self.symbol = symbol
        self.binance_client = binance_client
        self.leverage = 0
        self.isolated = False
        self.price_info = {
            "open": None,
            "close": None,
            "high": None,
            "low": None,
            "volume": None,
            "quote_asset_volume": None,
            "timestamp": None,
        }
        self.long_info = {
            "target": None,
            "amount": None,
            "target_hit": False,
            "filter": {"MT": False, "NY": False, "VT": False},
        }
        self.short_info = {
            "target": None,
            "amount": None,
            "target_hit": False,
            "filter": {"MT": False, "NY": False, "VT": False},
        }

    def __str__(self):
        return self.symbol


from util import readfile

DAY = 3

R = 3
D = 1

def solve_1(data):
    c = 3
    x = 0
    L = len(data[0])
    for row in data[1:]:
        x += int(row[c] == "#")
        c += R
        c %= L
    return x

def solve_2(data):
    L = len(data[0])
    H = len(data)
    p = 1
    for (right, down) in [(1, 1), (3, 1), (5, 1), (7, 1), (1, 2)]:
        i = down
        x = 0
        c = right
        while i < H:
            row = data[i]
            x += int(row[c] == "#")
            c += right
            c %= L
            i += down
        p *= x
    return p

if __name__ == "__main__":
    with open(f"{DAY}.in", "r") as f:
        data = list(readfile(f, mapper=lambda x: x[0].rstrip()))
        print(solve_1(data))
        print(solve_2(data))


from django.db import models

class CRUDManager(models.Manager):
    def update_product(self, data, pk):
        product = self.get(pk=pk)
        product.name = data['name']
        product.brand = data['brand']
        product.description = data['description']
        product.price = data['price']
        product.sub_category_id = data['sub_category']
        product.save()


from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse

import markdown, pygments
from comments.forms import CommentForm

from .models import Post, Category

def index(request):
    '''
    首页
    '''
    post_list = Post.objects.all()
    return render(request, 'blog/index.html', context={
        'post_list': post_list
    })

def detail(request, pk):
    '''
    详情页
    '''
    post = get_object_or_404(Post, pk=pk)
    post.body = markdown.markdown(post.body,
                                  extensions=[
                                      'markdown.extensions.extra',
                                      'markdown.extensions.codehilite', # 语法高亮拓展
                                      'markdown.extensions.toc', # 自动生成目录
                                  ])
    post.increase_visiting()

    comment_form = CommentForm()
    comment_list = post.comment_set.all()

    # 将文章、表单、以及文章下的评论列表作为模板变量传给 detail.html 模板，以便渲染相应数据。
    return render(request, 'blog/detail.html', context={
        'post': post,
        'comment_form': comment_form,
        'comment_list': comment_list,
    })

def archives(request, year, month):
    '''
    归档
    '''
    post_list = Post.objects.filter(created_time__year=year, created_time__month=month)
    return render(request, 'blog/index.html', context={'post_list': post_list})

def category(request, pk):
    '''
    分类
    '''
    cat = get_object_or_404(Category, pk=pk)
    post_list = Post.objects.filter(category=cat)
    return render(request, 'blog/index.html', context={'post_list': post_list})


#Lucas Wenger
#Project 4 - Make a game
#Due November 30, 2018
#Pseudo-Pokemon game - player earns money by battling trainers and attempts to catch all of the pokemon
#(Pokemon is owned by Nintendo)

'''
IMPORTED PACKAGES/FILES
'''
import sys
from PType import *
from Move import *
from HealingItem import *
from Pokeball import *
from Pokemon import *
from Trainer import *
from Player import *

'''
INITIALIZE PLAYER CLASS
'''
#Player class
Player = Player()

'''
TYPES
'''
#Implemented types
Normal = PType('Normal',None,None)
Grass = PType('Grass',None,None)
Fire = PType('Fire',Grass,None)
Grass.weakness = Fire
Water = PType('Water',Fire,Grass)
Grass.strength = Water
Fire.weakness = Water
Dragon = PType('Dragon',None,None)
Dragon.strength = Dragon
Dragon.weakness = Dragon

'''
MOVES
'''
#Implemented moves
Tackle = Move('Tackle',Normal,5,80)
Burn = Move('Burn',Fire,8,60)
VineWhip = Move('Vine Whip',Grass,5,100)
Bubble = Move('Bubble',Water,6,90)
Scratch = Move('Scratch',Normal,6,70)
Outrage = Move('Outrage',Dragon,50,50)
HyperBeam = Move('Hyper Beam',Normal,100,20)

'''
HEALING ITEMS
'''
#Implemented player healing items
PlayerPotion = HealingItem('Potion','Potions',20,0,10)

Player.healing_items.append(PlayerPotion)

'''
POKEBALLS
'''
#Implemented pokeballs
pokeball = Ball('Pokeball','Pokeballs',15,0,30)
masterball = Ball('Master Ball','Master Balls',1000,0,100)

Player.pokeball_bag.append(pokeball)
Player.pokeball_bag.append(masterball)

'''
POKEMON
'''
#Implemented Pokemon
PlayerCharmander = Pokemon('Charmander',1,Fire,20,[Tackle,Burn],40)
PlayerBulbasaur = Pokemon('Bulbasaur',1,Grass,20,[Tackle,VineWhip],40)
PlayerSquirtle = Pokemon('Squirtle',1,Water,20,[Tackle,Bubble],40)
PlayerPidgey = Pokemon('Pidgey',1,Normal,18,[Tackle,Scratch],30)
PlayerGyrados = Pokemon('Gyrados',1,Water,30,[Tackle,Bubble,HyperBeam],65)
PlayerDragonite = Pokemon('Dragonite',1,Dragon,50,[Outrage,HyperBeam,Scratch],80)

#WILD POKEMON BOX
wild_pokemon = [PlayerCharmander,PlayerBulbasaur,PlayerSquirtle,PlayerPidgey,PlayerGyrados,PlayerDragonite]

'''
TRAINERS
'''
#Implemented trainers and their equipment
JerryBulbasaur = Pokemon('Jerry\'s Bulbasaur',1,Grass,20,[Tackle,VineWhip],0)
JerryPotion = HealingItem('Potion','Potions',15,2,10)
Jerry = Trainer('Jerry',JerryBulbasaur,[JerryPotion],20)

CarlosCharmander = Pokemon('Carlos\' Charmander',1,Fire,20,[Tackle,Burn],0)
CarlosPotion = HealingItem('Potion','Potions',15,2,10)
Carlos = Trainer('Carlos',CarlosCharmander,[CarlosPotion],20)

SallySquirtle = Pokemon('Sally\'s Squirtle',1,Water,20,[Tackle,Bubble],0)
SallyPotion = HealingItem('Potion','Potions',15,2,10)
Sally = Trainer('Sally',SallySquirtle,[SallyPotion],20)

#TRAINER BOOKKEEPING
trainerlist = [Jerry,Carlos,Sally]

'''
FUNCTIONS
'''
def combat(trainer,player=None):
    if player==None:
        player = Player
    print("\nYou have encountered Pokemon Trainer " + trainer.name + "!")
    print(player.active_pokemon.name + " has entered combat with " + trainer.pokemon.name + "!\n")
    player.active_pokemon.temphealth = player.active_pokemon.maxhealth
    trainer.pokemon.temphealth = trainer.pokemon.maxhealth
    while player.active_pokemon.temphealth > 0 and trainer.pokemon.temphealth > 0:
        print(player.active_pokemon.name + " has " + str(player.active_pokemon.temphealth) + " health.")
        print(trainer.pokemon.name + " has " + str(trainer.pokemon.temphealth) + " health.")
        player.playerturn(trainer.pokemon)
        print('\n')
        if player.active_pokemon.temphealth <= 0 or trainer.pokemon.temphealth <= 0:
            break
        trainer.trainerturn(player)
    if player.active_pokemon.temphealth <= 0:
        print("You fainted!")
        if player.money == 0:
            print("You can't pay " + trainer.name + " anything. You walk home in disgrace.")
        elif player.money < trainer.reward:
            print("You can only pay " + trainer.name + " " + str(player.money) + " munny. You walk home in disgrace.")
            player.money = 0
        else:
            print("You pay " + trainer.name + " " + str(trainer.reward) + " munny. " + trainer.name + " thanks you and moves along.")
            player.money -= trainer.reward
    else:
        print("You win!")
        print(trainer.name + " pays you " + str(trainer.reward) + " munny and moves along.")
        player.money += trainer.reward

def catch(pokemon,player=Player):
    answer = ''
    print("A wild " + pokemon.name + " has appeared!")
    while True:
        print("What would you like to do?")
        print("1. Throw pokeball")
        print("2. Run away")
        while answer not in ['1','2']:
            answer = input("\nEnter a number.\n")
        if answer == '1':
            answer = ''
            while True:
                while answer not in range(1,len(player.pokeball_bag)+2):
                    for i in player.pokeball_bag:
                        print(str(player.pokeball_bag.index(i)+1) + ". " + i.name + " -- x" + str(i.amount))
                    print(str(player.pokeball_bag.index(i)+2) + ". " + "Return")
                    try:
                        answer = int(input("Which pokeball would you like to use? Enter a number.\n"))
                        break
                    except ValueError:
                        continue
                if answer in range(1,len(player.pokeball_bag)+1):
                    if player.pokeball_bag[answer-1].amount > 0:
                        player.pokeball_bag[answer-1].amount -= 1
                        catchchance = random.randint(0,100)
                        if catchchance <= player.pokeball_bag[answer-1].catchchance:
                            print("Congratulations! You caught " + pokemon.name + "!")
                            wild_pokemon.remove(pokemon)
                            player.pokemon_list.append(pokemon)
                            return
                        else:
                            print("It escaped from the " + player.pokeball_bag[answer-1].name + "!")
                        answer = 0
                    else:
                        answer = 0
                        print("You don't have any " + player.pokeball_bag[answer-1].plural + " left!")
                elif answer == len(player.pokeball_bag)+1:
                    catch(pokemon)
                escapechance = random.randint(0,100)
                if escapechance <= pokemon.escapechance:
                    print("The pokemon escaped!")
                    return
        if answer == '2':
            print("You ran away!")
            return

def shop(player=Player):
    answer = ''
    print("\nSHOPKEEPER: Welcome to the Pokemon Shop! What would you like to buy?\n")
    print("1. Pokeballs")
    print("2. Healing Items")
    print("3. Leave shop")
    while answer not in ['1','2','3']:
        answer = input("\nEnter a number.\n")
    if answer == '1':
        answer = ''
        while answer not in range(1,len(player.pokeball_bag)+2):
            for i in player.pokeball_bag:
                print(str(player.pokeball_bag.index(i)+1) + ". " + i.name + " -- x" + str(i.amount) + "     " + str(i.cost) + " munny")
            print(str(player.pokeball_bag.index(i)+2) + ". " + "Return")
            print("\nYou have " + str(player.money) + " munny.\n")
            while True:
                try:
                    answer = int(input("What would you like to buy? Enter a number.\n"))
                    break
                except ValueError:
                    continue
            if answer in range(1,len(player.pokeball_bag)+1):
                if (player.money - player.pokeball_bag[answer-1].cost) >= 0:
                    player.pokeball_bag[answer-1].amount += 1
                    player.money -= player.pokeball_bag[answer-1].cost
                    answer = 0
                    print("Thank you!\n")
                else:
                    answer = 0
                    print("You don't have enough munny!")
            elif answer == len(player.pokeball_bag)+1:
                shop()
    elif answer == '2':
        answer = ''
        while answer not in range(1,len(player.healing_items)+2):
            for i in player.healing_items:
                print(str(player.healing_items.index(i)+1) + ". " + i.name + " -- x" + str(i.amount) + "      " + str(i.cost) + " munny")
            print(str(player.healing_items.index(i)+2) + ". " + "Return")
            print("\nYou have " + str(player.money) + " munny.\n")
            while True:
                try:
                    answer = int(input("What would you like to buy? Enter a number.\n"))
                    break
                except ValueError:
                    continue
            if answer in range(1,len(player.healing_items)+1):
                if (player.money - player.healing_items[answer-1].cost) >= 0:
                    player.healing_items[answer-1].amount += 1
                    player.money -= player.healing_items[answer-1].cost
                    answer = 0
                    print("Thank you!\n")
                else:
                    answer = 0
                    print("You don't have enough munny!")
            elif answer == len(player.healing_items)+1:
                shop()
    elif answer == '3':
        print("SHOPKEEPER: Thank you for your business! Come again!")
        return

def viewpokemon(pokemon,player=Player):
    print("\nWhat would you like to do?")
    print("1. View moves")
    print("2. Make active Pokemon")
    print("3. Return")
    answer = ''
    while answer not in ['1','2','3']:
        answer = input("\nEnter a number.\n")
    if answer == '1':
        for i in pokemon.moves:
            print(i.name)
        viewpokemon(pokemon)
    if answer == '2':
        player.active_pokemon = pokemon
        print(pokemon.name + " is now your active Pokemon!")
        viewpokemon(pokemon)
    if answer == '3':
        return

def viewall(player=Player):
    print("What would you like to view?\n")
    print("1. Pokemon")
    print("2. Healing Items")
    print("3. Pokeballs")
    print("4. Return")
    answer = ''
    while answer not in ['1','2','3','4']:
        print("Enter a number.\n")
        answer = input()
    if answer == '1':
        for i in player.pokemon_list:
            if i == player.active_pokemon:
                print(i.name + " (active pokemon)")
            else:
                print(i.name)
        print("\nWhich Pokemon would you like to view?")
        pokemon_list_names = [i.name for i in player.pokemon_list]
        while answer not in ['r'] and answer not in pokemon_list_names:
            answer = input("Enter the name of the Pokemon or enter 'r' to return.\n")
        if answer == 'r':
            viewall(player)
        else:
            for i in pokemon_list_names:
                if answer == i:
                    selectedpokemon = player.pokemon_list[pokemon_list_names.index(i)]
                    viewpokemon(selectedpokemon)
                    viewall()
    if answer == '2':
        for i in player.healing_items:
            print(i.name + " -- x" + str(i.amount))
        while answer not in ['r','m']:
            answer = input("Enter 'r' to return or 'm' to go back to the main menu.\n")
        if answer == 'r':
            viewall()
        if answer == 'm':
            return
    if answer == '3':
        for i in player.pokeball_bag:
            print(i.name + " -- x" + str(i.amount))
        while answer not in ['r','m']:
            answer = input("Enter 'r' to return or 'm' to go back to the menu.\n")
        if answer == 'r':
            viewall()
        if answer == 'm':
            return
    if answer == '4':
        return

'''
GAME
'''
print("PPPPPPPPPPPPPPPPP        OOOOOOOOO     KKKKKKKKK    KKKKKKKEEEEEEEEEEEEEEEEEEEEEEMMMMMMMM               MMMMMMMM     OOOOOOOOO     NNNNNNNN        NNNNNNNN")
print("P::::::::::::::::P     OO:::::::::OO   K:::::::K    K:::::KE::::::::::::::::::::EM:::::::M             M:::::::M   OO:::::::::OO   N:::::::N       N::::::N")
print("P::::::PPPPPP:::::P  OO:::::::::::::OO K:::::::K    K:::::KE::::::::::::::::::::EM::::::::M           M::::::::M OO:::::::::::::OO N::::::::N      N::::::N")
print("PP:::::P     P:::::PO:::::::OOO:::::::OK:::::::K   K::::::KEE::::::EEEEEEEEE::::EM:::::::::M         M:::::::::MO:::::::OOO:::::::ON:::::::::N     N::::::N")
print("  P::::P     P:::::PO::::::O   O::::::OKK::::::K  K:::::KKK  E:::::E       EEEEEEM::::::::::M       M::::::::::MO::::::O   O::::::ON::::::::::N    N::::::N")
print("  P::::P     P:::::PO:::::O     O:::::O  K:::::K K:::::K     E:::::E             M:::::::::::M     M:::::::::::MO:::::O     O:::::ON:::::::::::N   N::::::N")
print("  P::::PPPPPP:::::P O:::::O     O:::::O  K::::::K:::::K      E::::::EEEEEEEEEE   M:::::::M::::M   M::::M:::::::MO:::::O     O:::::ON:::::::N::::N  N::::::N")
print("  P:::::::::::::PP  O:::::O     O:::::O  K:::::::::::K       E:::::::::::::::E   M::::::M M::::M M::::M M::::::MO:::::O     O:::::ON::::::N N::::N N::::::N")
print("  P::::PPPPPPPPP    O:::::O     O:::::O  K:::::::::::K       E:::::::::::::::E   M::::::M  M::::M::::M  M::::::MO:::::O     O:::::ON::::::N  N::::N:::::::N")
print("  P::::P            O:::::O     O:::::O  K::::::K:::::K      E::::::EEEEEEEEEE   M::::::M   M:::::::M   M::::::MO:::::O     O:::::ON::::::N   N:::::::::::N")
print("  P::::P            O:::::O     O:::::O  K:::::K K:::::K     E:::::E             M::::::M    M:::::M    M::::::MO:::::O     O:::::ON::::::N    N::::::::::N")
print("  P::::P            O::::::O   O::::::OKK::::::K  K:::::KKK  E:::::E       EEEEEEM::::::M     MMMMM     M::::::MO::::::O   O::::::ON::::::N     N:::::::::N")
print("PP::::::PP          O:::::::OOO:::::::OK:::::::K   K::::::KEE::::::EEEEEEEE:::::EM::::::M               M::::::MO:::::::OOO:::::::ON::::::N      N::::::::N")
print("P::::::::P           OO:::::::::::::OO K:::::::K    K:::::KE::::::::::::::::::::EM::::::M               M::::::M OO:::::::::::::OO N::::::N       N:::::::N")
print("P::::::::P             OO:::::::::OO   K:::::::K    K:::::KE::::::::::::::::::::EM::::::M               M::::::M   OO:::::::::OO   N::::::N        N::::::N")
print("PPPPPPPPPP               OOOOOOOOO     KKKKKKKKK    KKKKKKKEEEEEEEEEEEEEEEEEEEEEEMMMMMMMM               MMMMMMMM     OOOOOOOOO     NNNNNNNN         NNNNNNN")
print("                                                        _____   __      __           _             ")
print("                                                       |  __ \  \ \    / /          (_)            ")
print("                                                       | |__) |  \ \  / ___ _ __ ___ _  ___  _ __  ")
print("                                                       |  ___/    \ \/ / _ | '__/ __| |/ _ \| '_ \ ")
print("                                                       | |         \  |  __| |  \__ | | (_) | | | |")
print("                                                       |_|          \/ \___|_|  |___|_|\___/|_| |_|")
#ASCII art generator: http://patorjk.com/software/taag/#p=testall&h=3&v=2&f=Crazy&t=P%20Version

user = ''
while user not in ['s','q']:
    user = input("Enter 's' to start the game or 'q' to quit:\n")
    if user == 's':
        continue
    elif user == 'q':
        sys.exit()
while user not in ['n','l']:
    user = input("Enter 'n' to start a new game or 'l' to load an old file:\n")
    #Load game
if user == 'l':
    raise NotImplementedError #ADD
    #New game
elif user == 'n':
        #INTRO
    print("POPLAR: Welcome to the world of Pokemon! My name is Professor Poplar, and I'll be your guide into this new and exciting adventure.")
    playername = input("First thing's first. What can I call you?\n")
    print("POPLAR: Nice to meet you " + playername + "! We are going to have a great adventure!")
    print("Are you familiar with Pokemon?")
    while user not in ['yes','no']:
        user = input("(enter 'yes' or 'no')\n")
    if user == 'no':
        print("POPLAR: Allow me to fill you in! Pokemon are creatures that share this world with us. You will find them all over the place, but they especially love to hide in tall grass.")
        print("You, " + playername + ", are a soon-to-be Pokemon trainer! You are about to begin a journey that will change your life.")
    elif user == 'yes':
        print("POPLAR: Well, then let's get right down to business!")

    while user not in ['Charmander','Bulbasaur','Squirtle','Florble the DESTROYER OF WORLDS, ETERNAL FUHRER, WITNESS OF ETERNITY, THE AEONS TORN']:
        print("\nWho would you like as your first Pokemon?")
        print("\nEnter 'Charmander' for the fire-type")
        print("Enter 'Bulbasaur' for the grass-type")
        print("Enter 'Squirtle' for the water-type")
        user = input("\n")
    print("You have chosen " + user + "!")
    print("\nPOPLAR: Excellent choice! " + user + " was always my favorite too.")
    if user == 'Charmander':
        Player.pokemon_list.append(PlayerCharmander)
        wild_pokemon.remove(PlayerCharmander)
    elif user == 'Bulbasaur':
        Player.pokemon_list.append(PlayerBulbasaur)
        wild_pokemon.remove(PlayerBulbasaur)
    elif user == 'Squirtle':
        Player.pokemon_list.append(PlayerSquirtle)
        wild_pokemon.remove(PlayerSquirtle)
    elif user == 'Florble the DESTROYER OF WORLDS, ETERNAL FUHRER, WITNESS OF ETERNITY, THE AEONS TORN':
        Player.pokemon_list.append(PlayerFlorble)
        wild_pokemon.remove(PlayerFlorble)
    Player.active_pokemon = Player.pokemon_list[0]
    print("Here, take some supplies! They'll help with your journey.\n")
    Player.money += 80
    print("You gained 80 munny!")
    PlayerPotion.amount += 2
    print("You gained 2 potions!")
    pokeball.amount += 4
    print("You gained 4 Pokeballs!")
    masterball.amount += 100
    print("\nAnd here's a little bonus while you're testing the game!")
    print("You gained 100 Master Balls!")
    print("\nPOPLAR: These pokeballs will help you catch pokemon in the wild!")
    print("I've taught you all I can. Go forth, " + playername + ", and become the best trainer of your generation!")
    print("Catch one of every Pokemon!")

#GAME MENU
while user != '6':
    print('-'*100)
    print("\nWHAT WOULD YOU LIKE TO DO?\n")
    print("1. Battle a Trainer")
    print("2. Wild Encounter")
    print("3. Shop")
    print("4. View Pokemon/Supplies")
    print("5. Multiplayer")
    print("6. Quit")
    print('-'*100)

    user = input("\n")

    if user == '1':
        trainernum = random.randint(0,len(trainerlist)-1)
        combat(trainerlist[trainernum])
    if user == '2':
        if wild_pokemon == []:
            print("You have caught all of the pokemon!")
        else:
            pokemonnum = random.randint(0,len(wild_pokemon)-1)
            catch(wild_pokemon[pokemonnum])
    if user == '3':
        shop()
    if user == '4':
        viewall()
    if user == '5':
        print("Multiplayer is currently under development.") #ADD
    if user == '6':
        sys.exit()

'''
TO DO:
- add multiplayer
- add level-up/experience
- add saving
- add evolution
- attack
- defense
- speed
- add story
- add "View Supplies"
- add sound

THINGS I LEARNED:
- classes
    Through this project, I have become very comfortable with classes. I remember when we first learned about them, I wondered how useful they would be.
    Now I understand how helpful they are not just in terms of capability, but for organization as well.
- class interaction
    I understand the usefulness of having classes interact with each other. This allows for better organization and much more concise and readable code.
    I remember often looking at other people's code online and seeing something.something_else.a_third_thing and wondering what it was. Now I know :-).
- order
    I realized that the order in which you load classes and functions is especially important. Initially, I had some trouble initializing the Pokemon and Move classes
    because I was initializing the type class afterward. I realized that the former draws from the latter, so I needed to have the type class initialized first.
- ASCII art generator
    I leared how to use an ASCII art generator and put it into Python.
- ERROR CHECKING
    I had many more errors to sort out than I had anticipated. I have spent probably a solid hour or two at least just fixing bugs and errors. This did lead
    to a greater understanding of coding, but it helped me to realize the importance of frequent checking. If you have a bug and keep building, you may have to
    erase what you've coded since the bug if it draws from the bug. I have a new appreciation for debugging and will hopefully have more grace whenever
    I see a bug in a video game from now on.
- organization
    With code even as large as this game's code, I realized how important it is to STAY ORGANIZED. Once I sorted all of my classes into separate modules and
    sorted my class initializations, it made my code so much easier to work with. Now I understand why video games have so many files; often less out of necessity
    and more out of workability and organization.
- calling a function within itself
    We talked about this in class, but I have realized (especially in the viewall, viewpokemon, and shop functions how useul this can be. It saves cumulatively large amounts
    of coding to just be able to call a function from within the function itself.
- laying a solid foundation
    I realized the massive benefits of having a solid foundation for a game. For instance, I could have designed a game with 3 pokemon, but instead I designed one with
    however many pokemon are in the wild_pokemon list. Using variables instead of solid, immutable values is such a valuable tool. It allows me to add in new items
    or pokemon without much work, and that is definitely worth the extra work it takes to develop a solid and mutable foundation.
- appreciation for game developers
    Making a game is hard. There are so many things to keep track of, so many variables, so many bugs to fix, and so many puzzle pieces to fit together.
    This has both helped video game developing feel like a possible task and given me an appreciation for the dedication and hard work it takes.
    Now I understand why it took 5 years to release Skyrim. With such a massive game, it really is a massive job!
'''


from collections import deque
def solution(land):
    land = deque(land)
    while len(land)>1:
        tmp=land.popleft()
        for i in range(4):
            tmp1=[]
            for j in range(4):
                if i==j:
                    continue
                else:
                    tmp1.append(land[0][i]+tmp[j])
            land[0][i]=max(tmp1)

    return max(list(land[0]))
land = [[1,2,3,5],[5,6,7,8],[4,3,2,1]]
print(solution(land))

코로나 기간 알고/프로그래머스/땅따먹기.py


class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        l={}
        t=[]
        for i in range(len(nums)):
            if (target-nums[i]) in l:
                t.append(i)
                t.append(l[target-nums[i]])
                return t
            else:
                l[nums[i]]=i
        return t


#!/usr/bin/env python

import rospy
from sensor_msgs.msg import Image #For aa kunne lese data fra realsense kamera
from cv_bridge import CvBridge, CvBridgeError #Bro mellom ROS og openCV
import cv2
import numpy as np
from geometry_msgs.msg import Twist #For aa kunne skrive cmd_vel kommandoer

class LineFollower_edgeDetection(object):
    def __init__(self):
        self.image_sub = rospy.Subscriber("/realsense/color/image_raw", Image, self.camera_callback) #Subscriber til realsense bilde stream
        self.bridge_object = CvBridge() #Opretter cv bridge objekt
        self.opticalFlow_left_crop_pub = rospy.Publisher("/raspberry_bot/opticalFlow/left_crop", Image, queue_size=1)
        self.opticalFlow_right_crop_pub = rospy.Publisher("/raspberry_bot/opticalFlow/right_crop", Image, queue_size=1)

    def camera_callback(self,data):
        try:
            cv_image = self.bridge_object.imgmsg_to_cv2(data, desired_encoding="bgr8") #Konverterer bilde til OpenCv format
        except CvBridgeError as e:
            print(e)

        #Klipper ut stream fra hoyre og venstre rad
        #Total height: 480, width: 640
        left_crop = cv_image[250:320,1:140]
        right_crop = cv_image[250:320,500:640]

        #Publiserer bilder slik at egen init_node kan beregne optic flow i bildene
        try:
            opticalFlow_left_crop_msgsFormat = self.bridge_object.cv2_to_imgmsg(left_crop, "bgr8")
            opticalFlow_right_crop_msgsFormat = self.bridge_object.cv2_to_imgmsg(right_crop, "bgr8")
            self.opticalFlow_left_crop_pub.publish(opticalFlow_left_crop_msgsFormat)
            self.opticalFlow_right_crop_pub.publish(opticalFlow_right_crop_msgsFormat)
        except CvBridgeError as e:
            print(e)

def main():
    rospy.init_node('line_follower_opticalFlow_node', anonymous=True)
    line_follower_edgeDetection_objetct = LineFollower_edgeDetection()
    try:
        rospy.spin()
    except KeyboardInterrupt:
        print("Shutting down")

if __name__=='__main__':
    main()


# SWEA 10200
# 유튜브 구독자 최대값, 최소값 구하기
T = int(input())
# 여러개의 테스트 케이스가 주어지므로, 각각을 처리합니다.
for test_case in range(1, T + 1):
    # 입력 값 받기
    N, A, B = map(int, input().split())
    # 결과 변수 준비
    maximum = 0
    minimum = 0

    # 최대 값은 한 그룹이 다른 그룹의 부분 집합일때 작은 집합
    if A >= B:
        maximum = B
    else:
        maximum = A

    # 최소 값은 둘을 합한 뒤 전체의 차
    if N >= A + B:
        minimum = 0
    else:
        minimum = (A + B) - N

    print('#{} {} {}'.format(test_case, maximum, minimum))


# 从上往下打印出二叉树的每个节点，同层节点从左至右打印。

class Solution:
    # 返回从上到下每个节点值列表，例：[1,2,3]
    def PrintFromTopToBottom(self, root):
        result = []
        # 如果根节点为空
        if not root:
            return result
        # 将根节点放入列表中
        q = [root]
        # 当ｑ列表不为空
        while len(q):
            # 　将ｑ列表的第一个元素赋值给新节点
            node = q.pop(0)
            # 将节点的值添加到结果列表中
            result.append(node.val)
            # 如果节点有左子树
            if node.left:
                # 将节点的左子树放入ｑ列表
                q.append(node.left)
            if node.right:
                # 将节点的右子树放入ｑ列表
                q.append(node.right)
        return result


from pyten.tools import create  # Import the problem creation function

problem = 'basic'  # Define Problem As Basic Tensor Completion Problem
siz = [20, 20, 20]  # Size of the Created Synthetic Tensor
r = [4, 4, 4]  # Rank of the Created Synthetic Tensor
miss = 0.8  # Missing Percentage
tp = 'CP'  # Define Solution Format of the Created Synthetic Tensor As 'CP decomposition'
[X1, Omega1, sol1] = create(problem, siz, r, miss, tp)
print("Omega1 shape: ", Omega1.shape)
print(X1.data)
# print(Omega1)

# Basic Tensor Completion with methods: CP-ALS,Tucker-ALS, FaLRTC, SiLRTC, HaLRTC, TNCP
from pyten.method import *

r = 4  # Rank for CP-based methods
R = [4, 4, 4]  # Rank for tucker-based methods
# CP-ALS
[T1, rX1] = cp_als(X1, r, Omega1)  # if no missing data just omit Omega1 by using [T1,rX1]=cp_als.cp_als(X1,r)
# print sol1.totensor().data
# print rX1.data

# Tucker-ALS
[T2, rX2] = tucker_als(X1, R, Omega1)  # if no missing data just omit Omega1
# FalRTC, SiLRTC, HaLRTC
rX3 = falrtc(X1, Omega1)
rX4 = silrtc(X1, Omega1)
rX5 = halrtc(X1, Omega1)
# TNCP
self1 = TNCP(X1, Omega1, rank=r)
self1.run()

# Error Testing
from pyten.tools import tenerror

realX = sol1.totensor()
[Err1, ReErr11, ReErr21] = tenerror(rX1, realX, Omega1)
[Err2, ReErr12, ReErr22] = tenerror(rX2, realX, Omega1)
[Err3, ReErr13, ReErr23] = tenerror(rX3, realX, Omega1)
[Err4, ReErr14, ReErr24] = tenerror(rX4, realX, Omega1)
[Err5, ReErr15, ReErr25] = tenerror(rX5, realX, Omega1)
[Err6, ReErr16, ReErr26] = tenerror(self1.X, realX, Omega1)
# print ('\n', 'The Relative Error of the Six Methods are:', ReErr21, ReErr22, ReErr23, ReErr24, ReErr25, ReErr26)
print ('\n', 'The frobenius norm of Error of the Six Methods are:', Err1, Err2, Err3, Err4, Err5, Err6)


import database_util
import mailling_util

import cv2
import DetectChars
import CheckPlates

showSteps = False

SCALAR_BLACK = (0.0, 0.0, 0.0)
SCALAR_WHITE = (255.0, 255.0, 255.0)
SCALAR_YELLOW = (0.0, 255.0, 255.0)
SCALAR_GREEN = (0.0, 255.0, 0.0)
SCALAR_RED = (0.0, 0.0, 255.0)

def main():
    DetectChars.loadKNNDataAndTrainKNN()
    path=input()

    imgOriginalScene  = cv2.imread("Images/"+path)

    listOfPossiblePlates = CheckPlates.detectPlatesInScene(imgOriginalScene)
    listOfPossiblePlates = DetectChars.detectCharsInPlates(listOfPossiblePlates)

    #cv2.imshow("imgOriginalScene", imgOriginalScene)

    if len(listOfPossiblePlates) == 0:
        print("\nno license plates were detected\n")
    else:

        listOfPossiblePlates.sort(key = lambda possiblePlate: len(possiblePlate.strChars), reverse = True)

        licPlate = listOfPossiblePlates[0]

       # cv2.imshow("imgPlate", licPlate.imgPlate)
       # cv2.imshow("imgThresh", licPlate.imgThresh)

        if len(licPlate.strChars) == 0:
            print("\nno characters were detected\n\n")
            return

        print("\nlicense plate read from image = " + licPlate.strChars + "\n")
        if database_util.query(licPlate.strChars)==0:
            print("intrusion is detected alerting the security system through email!!!")
            mailling_util.email(licPlate.strChars)
        else:
            print("safe to move!!!")
    cv2.waitKey(0)

    return
if __name__ == "__main__":
    main()