def classify(self, calc, symprec=None):
        '''
        Reasons on normalization, invokes hierarchy API and prepares calc for saving
        NB: this is the PUBLIC method
        @returns tilde_obj, error
        '''
        error = None
        symbols = calc.structures[-1].get_chemical_symbols()
        calc.info['formula'] = self.formula(symbols)
        calc.info['cellpar'] = cell_to_cellpar(calc.structures[-1].cell).tolist()
        if calc.info['input']:
            try:
                calc.info['input'] = str(calc.info['input'], errors='ignore')
            except:
                pass

        # applying filter: todo
        if (calc.info['finished'] == 0x1 and self.settings['skip_unfinished']) or \
           (not calc.info['energy'] and self.settings['skip_notenergy']):
            return None, 'data do not satisfy the active filter'

        # naive elements extraction
        fragments = re.findall(r'([A-Z][a-z]?)(\d*[?:.\d+]*)?', calc.info['formula'])
        for fragment in fragments:
            if fragment[0] == 'X':
                continue
            calc.info['elements'].append(fragment[0])
            calc.info['contents'].append(int(fragment[1])) if fragment[1] else calc.info['contents'].append(1)

        # extend hierarchy with modules
        for C_obj in self.Classifiers:
            try:
                calc = C_obj['classify'](calc)
            except:
                exc_type, exc_value, exc_tb = sys.exc_info()
                error = "Fatal error during classification:\n %s" % "".join(traceback.format_exception( exc_type, exc_value, exc_tb ))
                return None, error

        # chemical ratios
        if not len(calc.info['standard']):
            if len(calc.info['elements']) == 1: calc.info['expanded'] = 1
            if not calc.info['expanded']:
                calc.info['expanded'] = reduce(gcd, calc.info['contents'])
            for n, i in enumerate([x//calc.info['expanded'] for x in calc.info['contents']]):
                if i == 1:
                    calc.info['standard'] += calc.info['elements'][n]
                else:
                    calc.info['standard'] += calc.info['elements'][n] + str(i)
        if not calc.info['expanded']:
            del calc.info['expanded']

        calc.info['nelem'] = len(calc.info['elements'])
        if calc.info['nelem'] > 13:
            calc.info['nelem'] = 13
        calc.info['natom'] = len(symbols)

        # periodicity
        if calc.info['periodicity'] == 0:
            calc.info['periodicity'] = 0x4
        elif calc.info['periodicity'] == -1:
            calc.info['periodicity'] = 0x5

        # general calculation type reasoning
        if (calc.structures[-1].get_initial_charges() != 0).sum():
            calc.info['calctypes'].append(0x4) # numpy count_nonzero implementation
        if (calc.structures[-1].get_initial_magnetic_moments() != 0).sum():
            calc.info['calctypes'].append(0x5)
        if calc.phonons['modes']:
            calc.info['calctypes'].append(0x6)
        if calc.phonons['ph_k_degeneracy']:
            calc.info['calctypes'].append(0x7)
        if calc.phonons['dielectric_tensor']:
            calc.info['calctypes'].append(0x8) # CRYSTAL-only!
        if len(calc.tresholds) > 1:
            calc.info['calctypes'].append(0x3)
            calc.info['optgeom'] = True
        if calc.electrons['dos'] or calc.electrons['bands']:
            calc.info['calctypes'].append(0x2)
        if calc.info['energy']:
            calc.info['calctypes'].append(0x1)
        calc.info['spin'] = 0x2 if calc.info['spin'] else 0x1

        # TODO: standardize
        if 'vac' in calc.info:
            if 'X' in symbols:
                calc.info['techs'].append('vacancy defect: ghost')
            else:
                calc.info['techs'].append('vacancy defect: void space')

        calc.info['lata'] = round(calc.info['cellpar'][0], 3)
        calc.info['latb'] = round(calc.info['cellpar'][1], 3)
        calc.info['latc'] = round(calc.info['cellpar'][2], 3)
        calc.info['latalpha'] = round(calc.info['cellpar'][3], 2)
        calc.info['latbeta'] = round(calc.info['cellpar'][4], 2)
        calc.info['latgamma'] = round(calc.info['cellpar'][5], 2)

        # invoke symmetry finder
        found = SymmetryHandler(calc, symprec)
        if found.error:
            return None, found.error

        calc.info['sg'] = found.i
        calc.info['ng'] = found.n
        calc.info['symmetry'] = found.symmetry
        calc.info['spg'] = "%s &mdash; %s" % (found.n, found.i)
        calc.info['pg'] = found.pg
        calc.info['dg'] = found.dg

        # phonons
        if calc.phonons['dfp_magnitude']: calc.info['dfp_magnitude'] = round(calc.phonons['dfp_magnitude'], 3)
        if calc.phonons['dfp_disps']: calc.info['dfp_disps'] = len(calc.phonons['dfp_disps'])
        if calc.phonons['modes']:
            calc.info['n_ph_k'] = len(calc.phonons['ph_k_degeneracy']) if calc.phonons['ph_k_degeneracy'] else 1

        #calc.info['rgkmax'] = calc.electrons['rgkmax'] # LAPW

        # electronic properties reasoning by bands
        if calc.electrons['bands']:
            if calc.electrons['bands'].is_conductor():
                calc.info['etype'] = 0x2
                calc.info['bandgap'] = 0.0
                calc.info['bandgaptype'] = 0x1
            else:
                try:
                    gap, is_direct = calc.electrons['bands'].get_bandgap()
                except ElectronStructureError as e:
                    calc.electrons['bands'] = None
                    calc.warning(e.value)
                else:
                    calc.info['etype'] = 0x1
                    calc.info['bandgap'] = round(gap, 2)
                    calc.info['bandgaptype'] = 0x2 if is_direct else 0x3

        # electronic properties reasoning by DOS
        if calc.electrons['dos']:
            try: gap = round(calc.electrons['dos'].get_bandgap(), 2)
            except ElectronStructureError as e:
                calc.electrons['dos'] = None
                calc.warning(e.value)
            else:
                if calc.electrons['bands']: # check coincidence
                    if abs(calc.info['bandgap'] - gap) > 0.2:
                        calc.warning('Bans gaps in DOS and bands data differ considerably! The latter will be considered.')
                else:
                    calc.info['bandgap'] = gap
                    if gap:
                        calc.info['etype'] = 0x1
                    else:
                        calc.info['etype'] = 0x2
                        calc.info['bandgaptype'] = 0x1

        # TODO: beware to add something new to an existing item!
        # TODO2: unknown or absent?
        for entity in self.hierarchy:
            if entity['creates_topic'] and not entity['optional'] and not calc.info.get(entity['source']):
                if entity['enumerated']:
                    calc.info[ entity['source'] ] = [0x0] if entity['multiple'] else 0x0
                else:
                    calc.info[ entity['source'] ] = ['none'] if entity['multiple'] else 'none'

        calc.benchmark() # this call must be at the very end of parsing

        return calc, error
Reasons on normalization, invokes hierarchy API and prepares calc for saving
        NB: this is the PUBLIC method
        @returns tilde_obj, error