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)
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)
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__]
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())
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)
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())
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
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())
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
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
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)
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