示例#1
0
def machine_dependent_call_modifier(formatter=None, comm=None, env=None):
    """ Placement modifications for MPI processes. 

        Nodefile is re-written with one hostname per line and per processor
        (rather than  per host, with 'slots=n' arguments indicating the number of
        procs per node). Finally, the environment variable ``PBS_NODEFILE`` is
        modified to point to the new nodefile. 

        .. note:: 

           Also, the hostname were shortened to exclude cx1.hpc.imperial.ac.uk
           domain name in :py:function:`~pylada.modify_global_comm`. 
    """
    from pylada import logger
    if len(getattr(comm, 'machines', [])) == 0:
        return ""

    # modify nodefile
    nodefile = comm.nodefile()
    with open(nodefile, 'w') as file:
        for key, value in comm.machines.items():
            stg = '\n'.join([key] * value) + '\n'
            logger.debug(
                "config/cx1: nodefile: key: \"%s\"  value: \"%s\"  stg: \"%s\"" \
                    % (key, value, stg))
            file.write(stg)
    # modify environment variable
    env['PBS_NODEFILE'] = nodefile

    return "PBS_NODEFILE={0}".format(nodefile)
示例#2
0
def machine_dependent_call_modifier(formatter=None, comm=None, env=None):
    """ Placement modifications for MPI processes. 

        Nodefile is re-written with one hostname per line and per processor
        (rather than  per host, with 'slots=n' arguments indicating the number of
        procs per node). Finally, the environment variable ``PBS_NODEFILE`` is
        modified to point to the new nodefile. 

        .. note:: 

           Also, the hostname were shortened to exclude cx1.hpc.imperial.ac.uk
           domain name in :py:function:`~pylada.modify_global_comm`. 
    """
    from pylada import logger

    if len(getattr(comm, "machines", [])) == 0:
        return ""

    # modify nodefile
    nodefile = comm.nodefile()
    with open(nodefile, "w") as file:
        for key, value in comm.machines.items():
            stg = "\n".join([key] * value) + "\n"
            logger.debug('config/cx1: nodefile: key: "%s"  value: "%s"  stg: "%s"' % (key, value, stg))
            file.write(stg)
    # modify environment variable
    env["PBS_NODEFILE"] = nodefile

    return "PBS_NODEFILE={0}".format(nodefile)
示例#3
0
 def wrapped(*args, **kwargs):
     logger.debug('tools/init make_cached entry: method: %s' % method.__name__)
     if not hasattr(args[0], '_properties_cache'):
         setattr(args[0], '_properties_cache', {})
     cache = getattr(args[0], '_properties_cache')
     if method.__name__ not in cache:
         cache[method.__name__] = method(*args, **kwargs)
     logger.debug('tools/init make_cached: set method: %s' % method.__name__)
     return cache[method.__name__]
示例#4
0
def machine_dependent_call_modifier_nodefiles(formatter=None, comm=None, env=None):
    """ Version of machine_dependent_call_modifier that creates a nodefile """
    import logging
    from pylada import logger
    if len(getattr(comm, 'machines', [])) != 0:
        nfile = comm.nodefile()
        formatter['placement'] = "-machinefile {0}".format(nfile)
        if logger.isEnabledFor(logging.debug):
            logger.debug("config/mpi: machine_dep_call_mod: nodefile: \"%s\"" % nfile)
            with open(nfile) as fin:
                logger.debug("config/mpi: machine_dep_call_mod: nodefile contents: \"%s\"" %fin.read())
示例#5
0
def remove_pyladarunning_marker(outdir):
    """ Creates a marker file in output directory. """
    from os.path import exists, join
    from os import remove
    path = join(outdir, '.pylada_is_running')
    if exists(path):
        try:
            remove(path)
        except OSError:
            pass
    logger.debug('tools/init: rem_run_mark: is_run outdir: %s' % outdir)
示例#6
0
def machine_dependent_call_modifier(formatter=None, comm=None, env=None):
    """ Machine dependent modifications. 

        This is a fairly catch all place to put machine dependent stuff for mpi
        calls, including mpi placement.

        The formatter used to format the :py:data:`~pylada.mpirun_exe` string is
        passed as the first argument. It can be modified *in-place* for machine
        dependent stuff, or for mpi placement. The latter case occurs only if
        ``comm`` has a non-empty ``machines`` attribute. In that case,
        :py:attr:`~pylada.process.mpi.machines` is a dictionary mapping the
        hostnames to the number of procs on that host. Finally, an dictionary
        containing the environment variables can also be passed. It should be
        modified *in-place*.

        By default, the 'placement' value of the formatter is modified to reflect
        the nodefile of a specific mpi placement. This occurs only if
        mpi-placement is requested (eg `comm.machines` exists and is not empty).

        This function is called only from :py:function:`pylada.launch_program`. If
        calls fail, it is a good idea to copy :py:function:`pylada.launch_program`
        into your $HOME/.pylada and debug it from there.

        :param dict formatter:
          Dictionary used in formatting the command line of
          :py:function:`~pylada.launch`. It should be modified *in-place*.
        :param comm:
          Communicator used in this particular calculation. At this point in
          :py:function:`~pylada.launch_program`, dictionary data from the
          communicator have been copied to the formatter. It is passed here in
          case its attributes :py:attr:`~pylada.process.mpi.Communicator.machines`
          or the nodefile returned by
          :py:method:`~pylada.process.mpi.Communicator.nodefile`
          is needed. However, the communicator itself should not be modified.
        :type comm: :py:class:`~pylada.process.mpi.Communicator`
        :param dict env: 
          Dictionary of environment variables in which to run the call.

        :return: ignored
    """
    import logging
    from pylada import logger
    if len(getattr(comm, 'machines', [])) != 0:
        nfile = comm.nodefile()
        formatter['placement'] = "-machinefile {0}".format(nfile)
        logger.debug("config/mpi: machine_dep_call_mod: nodefile: \"%s\"" % nfile)
        if logger.isEnabledFor(logging.debug):
            with open(nfile) as fin:
                fin.write("config/mpi: machine_dep_call_mod: nodefile contents: \"%s\"" %
                          fin.read())
示例#7
0
def exec_input(script,
               global_dict=None,
               local_dict=None,
               paths=None,
               name=None):
    """ Executes input script and returns local dictionary (as namespace instance). """
    # stuff to import into script.
    from os import environ
    from os.path import abspath, expanduser
    from math import pi
    from numpy import array, matrix, dot, sqrt, abs, ceil
    from numpy.linalg import norm, det
    from .. import crystal
    from . import Input
    from pylada import logger
    import quantities

    logger.debug("misc/init: exec_input: entry")
    # Add some names to execution environment.
    if global_dict is None:
        global_dict = {}
    global_dict.update({
        "environ": environ,
        "pi": pi,
        "array": array,
        "matrix": matrix,
        "dot": dot,
        "norm": norm,
        "sqrt": sqrt,
        "ceil": ceil,
        "abs": abs,
        "det": det,
        "expanduser": expanduser,
        "load": load
    })
    for key, value in quantities.__dict__.items():
        if key[0] != '_' and key not in global_dict:
            global_dict[key] = value
    for key in crystal.__all__:
        global_dict[key] = getattr(crystal, key)
    if local_dict is None:
        local_dict = {}
    # Executes input script.
    logger.debug('misc/init: exec_input: ========== start script ==========')
    logger.debug(script)
    logger.debug('misc/init: exec_input: ========== end script ==========')
    exec(script, global_dict, local_dict)

    # Makes sure expected paths are absolute.
    if paths is not None:
        for path in paths:
            if path not in local_dict:
                continue
            local_dict[path] = abspath(expanduser(local_dict[path]))

    if name is None:
        name = 'None'
    result = Input(name)
    result.update(local_dict)
    return result
示例#8
0
def machine_dependent_call_modifier_nodefiles(formatter=None,
                                              comm=None,
                                              env=None):
    """ Version of machine_dependent_call_modifier that creates a nodefile """
    import logging
    from pylada import logger
    if len(getattr(comm, 'machines', [])) != 0:
        nfile = comm.nodefile()
        formatter['placement'] = "-machinefile {0}".format(nfile)
        if logger.isEnabledFor(logging.debug):
            logger.debug("config/mpi: machine_dep_call_mod: nodefile: \"%s\"" %
                         nfile)
            with open(nfile) as fin:
                logger.debug(
                    "config/mpi: machine_dep_call_mod: nodefile contents: \"%s\""
                    % fin.read())
示例#9
0
def exec_input(script, global_dict=None, local_dict=None,
               paths=None, name=None):
    """ Executes input script and returns local dictionary (as namespace instance). """
    # stuff to import into script.
    from os import environ
    from os.path import abspath, expanduser
    from math import pi
    from numpy import array, matrix, dot, sqrt, abs, ceil
    from numpy.linalg import norm, det
    from .. import crystal
    from . import Input
    from pylada import logger
    import quantities

    logger.debug("misc/init: exec_input: entry")
    # Add some names to execution environment.
    if global_dict is None:
        global_dict = {}
    global_dict.update({"environ": environ, "pi": pi, "array": array, "matrix": matrix, "dot": dot,
                        "norm": norm, "sqrt": sqrt, "ceil": ceil, "abs": abs,  "det": det,
                        "expanduser": expanduser, "load": load})
    for key, value in quantities.__dict__.items():
        if key[0] != '_' and key not in global_dict:
            global_dict[key] = value
    for key in crystal.__all__:
        global_dict[key] = getattr(crystal, key)
    if local_dict is None:
        local_dict = {}
    # Executes input script.
    logger.debug('misc/init: exec_input: ========== start script ==========')
    logger.debug(script)
    logger.debug('misc/init: exec_input: ========== end script ==========')
    exec(script, global_dict, local_dict)

    # Makes sure expected paths are absolute.
    if paths is not None:
        for path in paths:
            if path not in local_dict:
                continue
            local_dict[path] = abspath(expanduser(local_dict[path]))

    if name is None:
        name = 'None'
    result = Input(name)
    result.update(local_dict)
    return result
示例#10
0
def icsd_cif_a(filename):
    """ Reads lattice from the ICSD \*cif files.

        It will not work in the case of other \*cif.
        It is likely to produce wrong output if the site occupations are fractional.
        If the occupation is > 0.5 it will treat it as 1 and
        in the case occupation < 0.5 it will treat it as 0 and
        it will accept all occupation = 0.5 as 1 and create a mess!
    """
    from pylada import logger
    import re
    from os.path import basename
    from numpy.linalg import norm
    from numpy import array, transpose
    from numpy import pi, sin, cos, sqrt, dot

    lines = open(filename, 'r').readlines()
    logger.info("crystal/read: icsd_cif_a: %s" % filename)

    sym_big = 0
    sym_end = 0
    pos_big = 0
    pos_end = 0

    for l in lines:
        x = l.split()
        if len(x) > 0:
                    # CELL
            if x[0] == '_cell_length_a':
                if '(' in x[-1]:
                    index = x[-1].index('(')
                else:
                    index = len(x[-1])
                a = float(x[-1][:index])

            if x[0] == '_cell_length_b':
                if '(' in x[-1]:
                    index = x[-1].index('(')
                else:
                    index = len(x[-1])
                b = float(x[-1][:index])

            if x[0] == '_cell_length_c':
                if '(' in x[-1]:
                    index = x[-1].index('(')
                else:
                    index = len(x[-1])
                c = float(x[-1][:index])

            if x[0] == '_cell_angle_alpha':
                if '(' in x[-1]:
                    index = x[-1].index('(')
                else:
                    index = len(x[-1])
                alpha = float(x[-1][:index])

            if x[0] == '_cell_angle_beta':
                if '(' in x[-1]:
                    index = x[-1].index('(')
                else:
                    index = len(x[-1])
                beta = float(x[-1][:index])

            if x[0] == '_cell_angle_gamma':
                if '(' in x[-1]:
                    index = x[-1].index('(')
                else:
                    index = len(x[-1])
                gamma = float(x[-1][:index])

        # SYMMETRY OPERATIONS

        if len(x) > 0 and x[0] == '_symmetry_equiv_pos_as_xyz':
            sym_big = lines.index(l)

        if len(x) > 0 and x[0] == '_atom_type_symbol':
            sym_end = lines.index(l)

        # WYCKOFF POSITIONS

        if len(x) > 0 and x[0] == '_atom_site_attached_hydrogens':
            pos_big = lines.index(l)

        if len(x) > 0 and x[0] == '_atom_site_B_iso_or_equiv':
            pos_big = lines.index(l)

        if len(x) > 0 and x[0] == '_atom_site_U_iso_or_equiv':
            pos_big = lines.index(l)

        if len(x) > 0 and x[0] == '_atom_site_0_iso_or_equiv':
            pos_big = lines.index(l)

        # if pos_end == 0 and l in ['\n', '\r\n'] and lines.index(l) > pos_big:
        if pos_end == 0 and pos_big > 0 \
                and (l in ['\n', '\r\n'] or l.startswith('#')) \
                and lines.index(l) > pos_big:
            pos_end = lines.index(l)

    # _symmetry_equiv_pos_* lines are like:
    #     1     'x, x-y, -z+1/2'
    logger.debug("crystal/read: icsd_cif_a: sym_big: %s" % sym_big)
    logger.debug("crystal/read: icsd_cif_a: sym_end: %s" % sym_end)

    symm_ops = ['(' + x.split()[1][1:] + x.split()[2] + x.split()[3][:-1] + ')'
                for x in lines[sym_big + 1:sym_end - 1]]
    logger.debug("crystal/read: icsd_cif_a: symm_ops a: %s" % symm_ops)
    # ['(x,x-y,-z+1/2)', '(-x+y,y,-z+1/2)', ...]

    # Insert decimal points after integers
    symm_ops = [re.sub(r'(\d+)', r'\1.', x) for x in symm_ops]
    logger.debug("crystal/read: icsd_cif_a: symm_ops b: %s" % symm_ops)
    # ['(x,x-y,-z+1./2.)', '(-x+y,y,-z+1./2.)', ...]

    # _atom_site_* lines are like:
    #   Mo1 Mo4+ 2 c 0.3333 0.6667 0.25 1. 0
    logger.debug("crystal/read: icsd_cif_a: pos_big: %s" % pos_big)
    logger.debug("crystal/read: icsd_cif_a: pos_end: %s" % pos_end)
    wyckoff = [[x.split()[0], [x.split()[4], x.split()[5], x.split()[6]], x.split()[7]]
               for x in lines[pos_big + 1:pos_end]]
    logger.debug("crystal/read: icsd_cif_a: wyckoff a: %s" % wyckoff)
    # [['Mo1', ['0.3333', '0.6667', '0.25'], '1.'], ['S1', ['0.3333', '0.6667', '0.621(4)'], '1.']]

    wyckoff = [w for w in wyckoff if int(float(w[-1][:4]) + 0.5) != 0]
    logger.debug("crystal/read: icsd_cif_a: wyckoff b: %s" % wyckoff)
    # [['Mo1', ['0.3333', '0.6667', '0.25'], '1.'], ['S1', ['0.3333', '0.6667', '0.621(4)'], '1.']]

    # Setting up a good wyckoff list

    for w in wyckoff:
        # Strip trailing numerals from w[0] == 'Mo1'
        pom = 0
        for i in range(len(w[0])):
            try:
                int(w[0][i])
                if pom == 0:
                    pom = i
            except:
                pass

        w[0] = w[0][:pom]

        # Strip trailing standard uncertainty, if any, from w[1], ..., w[3]
        for i in range(3):
            if '(' in w[1][i]:
                index = w[1][i].index('(')
            else:
                index = len(w[1][i])
            w[1][i] = float(w[1][i][:index])

        # Delete w[4]
        del w[-1]
    ##########################################

    # List of unique symbols ["Mo", "S"]
    symbols = list({w[0] for w in wyckoff})
    logger.debug("crystal/read: icsd_cif_a: symbols: %s" % symbols)

    # List of position vectors for each symbol
    positions = [[] for i in range(len(symbols))]

    for w in wyckoff:
        symbol = w[0]
        x, y, z = w[1][0], w[1][1], w[1][2]
        logger.debug("symbol: %s  x: %s   y: %s  z: %s" % (symbol, x, y, z))
        for i in range(len(symm_ops)):
            # Set pom = new position based on symmetry transform
            pom = list(eval(symm_ops[i]))
            logger.debug("i: %s  pom a: %s" % (i, pom))
            # [0.3333, -0.3334, 0.25]

            # Move positions to range [0,1]:
            for j in range(len(pom)):
                if pom[j] < 0.:
                    pom[j] = pom[j] + 1.
                if pom[j] >= 0.999:
                    pom[j] = pom[j] - 1.
            logger.debug("i: %s   pom b: %s" % (i, pom))
            # [0.3333, 0.6666, 0.25]

            # If pom is not in positions[symbol], append pom
            if not any(norm(array(u) - array(pom)) < 0.01 for u in positions[symbols.index(symbol)]):
                ix = symbols.index(symbol)
                positions[ix].append(pom)
                logger.debug("new positions for %s: %s" % (symbol, repr(positions[ix])))

    ################ CELL ####################

    a1 = a * array([1., 0., 0.])
    a2 = b * array([cos(gamma * pi / 180.), sin(gamma * pi / 180.), 0.])
    c1 = c * cos(beta * pi / 180.)
    c2 = c / sin(gamma * pi / 180.) * (-cos(beta * pi / 180.) *
                                       cos(gamma * pi / 180.) + cos(alpha * pi / 180.))
    a3 = array([c1, c2, sqrt(c**2 - (c1**2 + c2**2))])
    cell = array([a1, a2, a3])
    logger.debug("crystal/read: icsd_cif_a: a1: %s" % a1)
    logger.debug("crystal/read: icsd_cif_a: a2: %s" % a2)
    logger.debug("crystal/read: icsd_cif_a: a3: %s" % a3)
    ##########################################

    from pylada.crystal import Structure, primitive
    logger.debug("crystal/read: icsd_cif_a: cell: %s" % cell)

    structure = Structure(
        transpose(cell),
        scale=1,
        name=basename(filename))

    for i in range(len(symbols)):
        logger.debug("crystal/read: icsd_cif_a: i: %s  symbol: %s  len(position): %i" % (
            i,  symbols[i], len(positions[i])
        ))
        # crystal/read: i:  0   symbol:  Mo   len position:  2

        for j in range(len(positions[i])):
            atpos = dot(transpose(cell), positions[i][j])
            logger.debug("j: %s  pos: %s" % (j, positions[i][j]))
            logger.debug("atpos: " % atpos)
            #  j:  0   pos:  [0.3333, 0.6666000000000001, 0.25]
            #  atpos:  [  6.32378655e-16   1.81847148e+00   3.07500000e+00]

            structure.add_atom(atpos[0], atpos[1], atpos[2], symbols[i])

    logger.info("crystal/read: icsd_cif_a: structure: %s" % structure)

    prim = primitive(structure)
    logger.info("crystal/read: icsd_cif_a: primitive structure: %s" % prim)

    return prim
示例#11
0
def add_pyladarunning_marker(outdir):
    """ Creates a marker file in output directory. """
    from os.path import join
    file = open(join(outdir, '.pylada_is_running'), 'w')
    file.close()
    logger.debug('tools/init: add_run_mark: is_run outdir: %s' % outdir)
示例#12
0
def icsd_cif_a(filename, make_primitive=True):
    """ Reads lattice from the ICSD \*cif files.

        It will not work in the case of other \*cif.
        It is likely to produce wrong output if the site occupations are fractional.
        If the occupation is > 0.5 it will treat it as 1 and
        in the case occupation < 0.5 it will treat it as 0 and
        it will accept all occupation = 0.5 as 1 and create a mess!
    """
    from pylada import logger
    import re
    from copy import deepcopy
    from os.path import basename
    from numpy.linalg import norm
    from numpy import array, transpose
    from numpy import pi, sin, cos, sqrt, dot

    lines = open(filename, 'r').readlines()
    logger.info("crystal/read: icsd_cif_a: %s" % filename)

    sym_big = 0
    sym_end = 0
    pos_big = 0
    pos_end = 0

    for l in lines:
        x = l.split()
        if len(x) > 0:
                    # CELL
            if x[0] == '_cell_length_a':
                if '(' in x[-1]:
                    index = x[-1].index('(')
                else:
                    index = len(x[-1])
                a = float(x[-1][:index])

            if x[0] == '_cell_length_b':
                if '(' in x[-1]:
                    index = x[-1].index('(')
                else:
                    index = len(x[-1])
                b = float(x[-1][:index])

            if x[0] == '_cell_length_c':
                if '(' in x[-1]:
                    index = x[-1].index('(')
                else:
                    index = len(x[-1])
                c = float(x[-1][:index])

            if x[0] == '_cell_angle_alpha':
                if '(' in x[-1]:
                    index = x[-1].index('(')
                else:
                    index = len(x[-1])
                alpha = float(x[-1][:index])

            if x[0] == '_cell_angle_beta':
                if '(' in x[-1]:
                    index = x[-1].index('(')
                else:
                    index = len(x[-1])
                beta = float(x[-1][:index])

            if x[0] == '_cell_angle_gamma':
                if '(' in x[-1]:
                    index = x[-1].index('(')
                else:
                    index = len(x[-1])
                gamma = float(x[-1][:index])

        # SYMMETRY OPERATIONS

        if len(x) > 0 and x[0] == '_symmetry_equiv_pos_as_xyz':
            sym_big = lines.index(l)

        if len(x) > 0 and x[0] == '_atom_type_symbol':
            sym_end = lines.index(l)

        # WYCKOFF POSITIONS

        if len(x) > 0 and x[0] == '_atom_site_attached_hydrogens':
            pos_big = lines.index(l)

        if len(x) > 0 and x[0] == '_atom_site_B_iso_or_equiv':
            pos_big = lines.index(l)

        if len(x) > 0 and x[0] == '_atom_site_U_iso_or_equiv':
            pos_big = lines.index(l)

        if len(x) > 0 and x[0] == '_atom_site_0_iso_or_equiv':
            pos_big = lines.index(l)

        # if pos_end == 0 and l in ['\n', '\r\n'] and lines.index(l) > pos_big:
        if pos_end == 0 and pos_big > 0 \
                and (l in ['\n', '\r\n'] or l.startswith('#')) \
                and lines.index(l) > pos_big:
            pos_end = lines.index(l)

    # _symmetry_equiv_pos_* lines are like:
    #     1     'x, x-y, -z+1/2'
    logger.debug("crystal/read: icsd_cif_a: sym_big: %s" % sym_big)
    logger.debug("crystal/read: icsd_cif_a: sym_end: %s" % sym_end)

    symm_ops = ['(' + x.split()[1][1:] + x.split()[2] + x.split()[3][:-1] + ')'
                for x in lines[sym_big + 1:sym_end - 1]]
    logger.debug("crystal/read: icsd_cif_a: symm_ops a: %s" % symm_ops)
    # ['(x,x-y,-z+1/2)', '(-x+y,y,-z+1/2)', ...]

    # Insert decimal points after integers
    symm_ops = [re.sub(r'(\d+)', r'\1.', x) for x in symm_ops]
    logger.debug("crystal/read: icsd_cif_a: symm_ops b: %s" % symm_ops)
    # ['(x,x-y,-z+1./2.)', '(-x+y,y,-z+1./2.)', ...]

    # _atom_site_* lines are like:
    #   Mo1 Mo4+ 2 c 0.3333 0.6667 0.25 1. 0
    logger.debug("crystal/read: icsd_cif_a: pos_big: %s" % pos_big)
    logger.debug("crystal/read: icsd_cif_a: pos_end: %s" % pos_end)
    wyckoff = [[x.split()[0], [x.split()[4], x.split()[5], x.split()[6]], x.split()[7]]
               for x in lines[pos_big + 1:pos_end]]
    logger.debug("crystal/read: icsd_cif_a: wyckoff a: %s" % wyckoff)
    # [['Mo1', ['0.3333', '0.6667', '0.25'], '1.'], ['S1', ['0.3333', '0.6667', '0.621(4)'], '1.']]

    wyckoff = [w for w in wyckoff if int(float(w[-1][:4]) + 0.5) != 0]
    logger.debug("crystal/read: icsd_cif_a: wyckoff b: %s" % wyckoff)
    # [['Mo1', ['0.3333', '0.6667', '0.25'], '1.'], ['S1', ['0.3333', '0.6667', '0.621(4)'], '1.']]

    # Setting up a good wyckoff list

    for w in wyckoff:
        # Strip trailing numerals from w[0] == 'Mo1'
        pom = 0
        for i in range(len(w[0])):
            try:
                int(w[0][i])
                if pom == 0:
                    pom = i
            except:
                pass

        w[0] = w[0][:pom]

        # Strip trailing standard uncertainty, if any, from w[1], ..., w[3]
        for i in range(3):
            if '(' in w[1][i]:
                index = w[1][i].index('(')
            else:
                index = len(w[1][i])
            w[1][i] = float(w[1][i][:index])

        # Delete w[4]
        del w[-1]
    ##########################################

    # List of unique symbols ["Mo", "S"]
    symbols = list({w[0] for w in wyckoff})
    logger.debug("crystal/read: icsd_cif_a: symbols: %s" % symbols)

    # List of position vectors for each symbol
    positions = [[] for i in range(len(symbols))]

    for w in wyckoff:
        symbol = w[0]
        x, y, z = w[1][0], w[1][1], w[1][2]
        logger.debug("symbol: %s  x: %s   y: %s  z: %s" % (symbol, x, y, z))
        for i in range(len(symm_ops)):
            # Set pom = new position based on symmetry transform
            pom = list(eval(symm_ops[i]))
            logger.debug("i: %s  pom a: %s" % (i, pom))
            # [0.3333, -0.3334, 0.25]

            # Move positions to range [0,1]:
            for j in range(len(pom)):
                if pom[j] < 0.:
                    pom[j] = pom[j] + 1.
                if pom[j] >= 0.999:
                    pom[j] = pom[j] - 1.
            logger.debug("i: %s   pom b: %s" % (i, pom))
            # [0.3333, 0.6666, 0.25]

            # If pom is not in positions[symbol], append pom
            if not any(norm(array(u) - array(pom)) < 0.01 for u in positions[symbols.index(symbol)]):
                ix = symbols.index(symbol)
                positions[ix].append(pom)
                logger.debug("new positions for %s: %s" % (symbol, repr(positions[ix])))

    ################ CELL ####################

    a1 = a * array([1., 0., 0.])
    a2 = b * array([cos(gamma * pi / 180.), sin(gamma * pi / 180.), 0.])
    c1 = c * cos(beta * pi / 180.)
    c2 = c / sin(gamma * pi / 180.) * (-cos(beta * pi / 180.) *
                                       cos(gamma * pi / 180.) + cos(alpha * pi / 180.))
    a3 = array([c1, c2, sqrt(c**2 - (c1**2 + c2**2))])
    cell = array([a1, a2, a3])
    logger.debug("crystal/read: icsd_cif_a: a1: %s" % a1)
    logger.debug("crystal/read: icsd_cif_a: a2: %s" % a2)
    logger.debug("crystal/read: icsd_cif_a: a3: %s" % a3)
    ##########################################

    from pylada.crystal import Structure, primitive
    logger.debug("crystal/read: icsd_cif_a: cell: %s" % cell)

    structure = Structure(
        transpose(cell),
        scale=1,
        name=basename(filename))

    for i in range(len(symbols)):
        logger.debug("crystal/read: icsd_cif_a: i: %s  symbol: %s  len(position): %i" % (
            i,  symbols[i], len(positions[i])
        ))
        # crystal/read: i:  0   symbol:  Mo   len position:  2

        for j in range(len(positions[i])):
            atpos = dot(transpose(cell), positions[i][j])
            logger.debug("j: %s  pos: %s" % (j, positions[i][j]))
            logger.debug("atpos: " % atpos)
            #  j:  0   pos:  [0.3333, 0.6666000000000001, 0.25]
            #  atpos:  [  6.32378655e-16   1.81847148e+00   3.07500000e+00]

            structure.add_atom(atpos[0], atpos[1], atpos[2], symbols[i])

    logger.info("crystal/read: icsd_cif_a: structure: %s" % structure)

    if make_primitive:
        prim = primitive(structure)
    else:
        prim = deepcopy(structure)
    logger.info("crystal/read: icsd_cif_a: primitive structure: %s" % prim)

    return prim