def check_isotopes_substitution(self, atoms=None, masses=None, approximate=False): """ Updates atomic mass in case of isotopes. :param atoms: dictionary with atoms to check :param masses: atomic masses read from an ab initio file :param approximate: whether or not look for isotopes in the approximated way """ num_atoms = len(atoms) eps = AbinsModules.AbinsConstants.MASS_EPS if approximate: isotopes_found = [ abs(round(atoms["atom_%s" % i]["mass"]) - round(masses[i])) > eps for i in range(num_atoms) ] else: isotopes_found = [ abs(atoms["atom_%s" % i]["mass"] - masses[i]) > eps for i in range(num_atoms) ] if any(isotopes_found): for i in range(num_atoms): if isotopes_found[i]: z_num = Atom( symbol=atoms["atom_{}".format(i)]["symbol"]).z_number a_num = int(round(masses[i])) try: temp = Atom(a_number=a_num, z_number=z_num).mass atoms["atom_{}".format(i)]["mass"] = temp # no mass for isotopes available; assume no isotopic substitution for this atom except RuntimeError: pass
def _get_cross_section(self, protons_number=None, nucleons_number=None): """ Calculates cross section for the given element. :param protons_number: number of protons in the given type fo atom :param nucleons_number: number of nucleons in the given type of atom :returns: cross section for that element """ if nucleons_number is not None: try: atom = Atom(a_number=nucleons_number, z_number=protons_number) # isotopes are not implemented for all elements so use different constructor in that cases except RuntimeError: atom = Atom(z_number=protons_number) else: atom = Atom(z_number=protons_number) cross_section = None if self._scale_by_cross_section == 'Incoherent': cross_section = atom.neutron()["inc_scatt_xs"] elif self._scale_by_cross_section == 'Coherent': cross_section = atom.neutron()["coh_scatt_xs"] elif self._scale_by_cross_section == 'Total': cross_section = atom.neutron()["tot_scatt_xs"] return cross_section
def _read_coord_block(self, file_obj=None, xdisp=None, ydisp=None, zdisp=None, part="real"): """ Parses block with coordinates. :param file_obj: file object from which we read :param xdisp: list with x coordinates which we update :param ydisp: list with y coordinates which we update :param zdisp: list with z coordinates which we update """ self._parser.move_to(file_obj=file_obj, msg=" x ") atom_mass = None while not self._parser.file_end(file_obj=file_obj): pos = file_obj.tell() line = file_obj.readline() if line.strip(): line = line.strip(b"\n").split() if b"x" in line: symbol = str(line[0].decode("utf-8").capitalize()) atom_mass = Atom(symbol=symbol).mass for item in line[2:]: self._parse_item(item=item, container=xdisp, part=part, mass=atom_mass) elif b"y" in line: for item in line[1:]: self._parse_item(item=item, container=ydisp, part=part, mass=atom_mass) elif b"z" in line: for item in line[1:]: self._parse_item(item=item, container=zdisp, part=part, mass=atom_mass) atom_mass = None else: file_obj.seek(pos) break
def _atom_type_s(self, num_atoms=None, mass=None, s_data=None, atoms_data=None, element_symbol=None, temp_s_atom_data=None, s_atom_data=None, substitution=None): """ Helper function for calculating S for the given type of atom :param num_atoms: number of atoms in the system :param s_data: Precalculated S for all atoms and quantum orders :type s_data: abins.SData :param atoms_data: Atomic position/mass data :type atoms_data: abins.AtomsData :param element_symbol: label for the type of atom :param temp_s_atom_data: helper array to accumulate S (inner loop over quantum order); does not transport information but is used in-place to save on time instantiating large arrays. :param s_atom_data: helper array to accumulate S (outer loop over atoms); does not transport information but is used in-place to save on time instantiating large arrays. :param substitution: True if isotope substitution and False otherwise """ from abins.constants import MASS_EPS atom_workspaces = [] s_atom_data.fill(0.0) element = Atom(symbol=element_symbol) for atom_index in range(num_atoms): if (atoms_data[atom_index]["symbol"] == element_symbol and abs(atoms_data[atom_index]["mass"] - mass) < MASS_EPS): temp_s_atom_data.fill(0.0) for order in range(1, self._max_event_order + 1): order_indx = order - 1 temp_s_order = s_data[atom_index]["order_%s" % order] temp_s_atom_data[order_indx] = temp_s_order s_atom_data += temp_s_atom_data # sum S over the atoms of the same type total_s_atom_data = np.sum(s_atom_data, axis=0) nucleons_number = int(round(mass)) if substitution: atom_workspaces.append(self._create_workspace(atom_name=str(nucleons_number) + element_symbol, s_points=np.copy(total_s_atom_data), optional_name="_total", protons_number=element.z_number, nucleons_number=nucleons_number)) atom_workspaces.append(self._create_workspace(atom_name=str(nucleons_number) + element_symbol, s_points=np.copy(s_atom_data), protons_number=element.z_number, nucleons_number=nucleons_number)) else: atom_workspaces.append(self._create_workspace(atom_name=element_symbol, s_points=np.copy(total_s_atom_data), optional_name="_total", protons_number=element.z_number)) atom_workspaces.append(self._create_workspace(atom_name=element_symbol, s_points=np.copy(s_atom_data), protons_number=element.z_number)) return atom_workspaces
def _read_atomic_coordinates(self, file_obj=None, data=None, masses_from_file=None): """ Reads atomic coordinates from .outmol DMOL3 file. :param file_obj: file object from which we read :param data: Python dictionary to which atoms data should be added :param masses_from_file: masses read from an ab initio output file """ atoms = {} atom_indx = 0 self._parser.find_first(file_obj=file_obj, msg="$coordinates") end_msgs = ["$end", "----------------------------------------------------------------------"] while not self._parser.block_end(file_obj=file_obj, msg=end_msgs): line = file_obj.readline() entries = line.split() symbol = str(entries[0].decode("utf-8").capitalize()) atom = Atom(symbol=symbol) # We change unit of atomic displacements from atomic length units to Angstroms au2ang = ATOMIC_LENGTH_2_ANGSTROM float_type = FLOAT_TYPE atoms["atom_{}".format(atom_indx)] = {"symbol": symbol, "mass": atom.mass, "sort": atom_indx, "coord": np.asarray(entries[1:]).astype(dtype=float_type) * au2ang} atom_indx += 1 self.check_isotopes_substitution(atoms=atoms, masses=masses_from_file) data["atoms"] = atoms
def _create_atoms_data(self, data=None, coord_lines=None, atoms_masses=None): """ Creates Python dictionary with atoms data which can be easily converted to AbinsData object. :param atoms_masses: atom masses from output ab-initio file :param data: Python dictionary to which found atoms data should be added :param coord_lines: list with information about atoms """ data.update({"atoms": dict()}) for i, line in enumerate(coord_lines): l = line.split() symbol = str(l[2].decode("utf-8").capitalize()) atom = Atom(symbol=symbol) data["atoms"]["atom_{}".format(i)] = { "symbol": symbol, "mass": atom.mass, "sort": i, "coord": np.asarray(l[3:6]).astype( dtype=AbinsModules.AbinsConstants.FLOAT_TYPE) } self.check_isotopes_substitution(atoms=data["atoms"], masses=atoms_masses, approximate=True)
def _atom_type_s(self, num_atoms=None, mass=None, s_data_extracted=None, element_symbol=None, temp_s_atom_data=None, s_atom_data=None, substitution=None): """ Helper function for calculating S for the given type of atom :param num_atoms: number of atoms in the system :param s_data_extracted: data with all S :param element_symbol: label for the type of atom :param temp_s_atom_data: helper array to accumulate S (inner loop over quantum order); does not transport information but is used in-place to save on time instantiating large arrays. :param s_atom_data: helper array to accumulate S (outer loop over atoms); does not transport information but is used in-place to save on time instantiating large arrays. :param substitution: True if isotope substitution and False otherwise """ from AbinsModules.AbinsConstants import MASS_EPS atom_workspaces = [] s_atom_data.fill(0.0) element = Atom(symbol=element_symbol) for atom in range(num_atoms): if (self._extracted_ab_initio_data["atom_%s" % atom]["symbol"] == element_symbol and abs(self._extracted_ab_initio_data["atom_%s" % atom]["mass"] - mass) < MASS_EPS): temp_s_atom_data.fill(0.0) for order in range(AbinsModules.AbinsConstants.FUNDAMENTALS, self._num_quantum_order_events + AbinsModules.AbinsConstants.S_LAST_INDEX): order_indx = order - AbinsModules.AbinsConstants.PYTHON_INDEX_SHIFT temp_s_order = s_data_extracted["atom_%s" % atom]["s"]["order_%s" % order] temp_s_atom_data[order_indx] = temp_s_order s_atom_data += temp_s_atom_data # sum S over the atoms of the same type total_s_atom_data = np.sum(s_atom_data, axis=0) nucleons_number = int(round(mass)) if substitution: atom_workspaces.append(self._create_workspace(atom_name=str(nucleons_number) + element_symbol, s_points=np.copy(total_s_atom_data), optional_name="_total", protons_number=element.z_number, nucleons_number=nucleons_number)) atom_workspaces.append(self._create_workspace(atom_name=str(nucleons_number) + element_symbol, s_points=np.copy(s_atom_data), protons_number=element.z_number, nucleons_number=nucleons_number)) else: atom_workspaces.append(self._create_workspace(atom_name=element_symbol, s_points=np.copy(total_s_atom_data), optional_name="_total", protons_number=element.z_number)) atom_workspaces.append(self._create_workspace(atom_name=element_symbol, s_points=np.copy(s_atom_data), protons_number=element.z_number)) return atom_workspaces
def _parse_phonon_file_header(self, f_handle): """ Reads information from the header of a <>.phonon file :param f_handle: handle to the file. :returns: List of ions in file as list of tuple of (ion, mode number) """ file_data = {"atoms": {}} masses_from_file = [] while True: line = f_handle.readline() if not line: raise IOError("Could not find any header information.") if 'Number of ions' in line: self._num_atoms = int(line.strip().split()[-1]) elif 'Number of branches' in line: self._num_phonons = int(line.strip().split()[-1]) elif 'Number of wavevectors' in line: self._num_k = int(line.strip().split()[-1]) elif 'Unit cell vectors' in line: file_data['unit_cell'] = self._parse_phonon_unit_cell_vectors(f_handle) elif 'Fractional Co-ordinates' in line: if self._num_atoms is None: raise IOError("Failed to parse file. Invalid file header.") # Extract the mode number for each of the ion in the data file for _ in range(self._num_atoms): line = f_handle.readline() line_data = line.strip().split() indx = int(line_data[0]) - 1 # -1 to convert to zero based indexing # only name of element in the name if ":" not in line_data[4]: symbol = line_data[4] # D,T etc else: # possible scenarios: # H:D1 # H:D2 symbol = "H" masses_from_file.append(float(line_data[5])) ion = {"symbol": symbol, "coord": np.array(float(line_data[1]) * file_data['unit_cell'][0] + float(line_data[2]) * file_data['unit_cell'][1] + float(line_data[3]) * file_data['unit_cell'][2]), # at the moment it is a dummy parameter, it will mark symmetry equivalent atoms "sort": indx, "mass": Atom(symbol=symbol).mass} file_data["atoms"].update({"atom_%s" % indx: ion}) self.check_isotopes_substitution(atoms=file_data["atoms"], masses=masses_from_file) if 'END header' in line: if self._num_atoms is None or self._num_phonons is None: raise IOError("Failed to parse file. Invalid file header.") return file_data
def get_cross_section(scattering: str = 'Total', nucleons_number: Optional[int] = None, *, protons_number: int) -> float: """ Calculates cross section for the given element. :param scattering: Type of cross-section: 'Incoherent', 'Coherent' or 'Total' :param protons_number: number of protons in the given type fo atom :param nucleons_number: number of nucleons in the given type of atom :returns: cross section for that element """ if nucleons_number is not None: try: atom = Atom(a_number=nucleons_number, z_number=protons_number) # isotopes are not implemented for all elements so use different constructor in that cases except RuntimeError: logger.warning( f"Could not find data for isotope {nucleons_number}, " f"using default values for {protons_number} protons.") atom = Atom(z_number=protons_number) else: atom = Atom(z_number=protons_number) scattering_keys = { 'Incoherent': 'inc_scatt_xs', 'Coherent': 'coh_scatt_xs', 'Total': 'tot_scatt_xs' } return atom.neutron()[scattering_keys[scattering]]
def _create_workspaces(self, atoms_symbols=None, atom_numbers=None, s_data=None): """ Creates workspaces for all types of atoms. Creates both partial and total workspaces for given types of atoms. :param atoms_symbols: atom types (i.e. element symbols) for which S should be created. :type iterable of str: :param atom_numbers: indices of individual atoms for which S should be created. (One-based numbering; 1 <= I <= NUM_ATOMS) :type iterable of int: :param s_data: dynamical factor data :type AbinsModules.SData.SData :returns: workspaces for list of atoms types, S for the particular type of atom """ from AbinsModules.AbinsConstants import MASS_EPS, ONLY_ONE_MASS s_data_extracted = s_data.extract() # Create appropriately-shaped arrays to be used in-place by _atom_type_s - avoid repeated slow instantiation shape = [self._num_quantum_order_events] shape.extend(list(s_data_extracted["atom_0"]["s"]["order_1"].shape)) s_atom_data = np.zeros(shape=tuple(shape), dtype=AbinsModules.AbinsConstants.FLOAT_TYPE) temp_s_atom_data = np.copy(s_atom_data) num_atoms = len( [key for key in s_data_extracted.keys() if "atom" in key]) masses = self._get_masses_table(num_atoms) result = [] if atoms_symbols is not None: for symbol in atoms_symbols: sub = (len(masses[symbol]) > ONLY_ONE_MASS or abs(Atom(symbol=symbol).mass - masses[symbol][0]) > MASS_EPS) for m in masses[symbol]: result.extend( self._atom_type_s(num_atoms=num_atoms, mass=m, s_data_extracted=s_data_extracted, element_symbol=symbol, temp_s_atom_data=temp_s_atom_data, s_atom_data=s_atom_data, substitution=sub)) if atom_numbers is not None: for atom_number in atom_numbers: result.extend( self._atom_number_s(atom_number=atom_number, s_data_extracted=s_data_extracted, s_atom_data=s_atom_data)) return result
def _atom_number_s(self, atom_number=None, s_data_extracted=None, s_atom_data=None): """ Helper function for calculating S for the given atomic index :param atom_number: One-based index of atom in s_data e.g. 1 to select first element 'atom_1' :type atom_number: int :param s_data_extracted: Collection of precalculated S for all atoms and quantum orders, obtained from extract() method of abins.SData object. :type s_data: dict :param s_atom_data: helper array to accumulate S (outer loop over atoms); does not transport information but is used in-place to save on time instantiating large arrays. First dimension is quantum order; following dimensions should match arrays in s_data. :type s_atom_data: numpy.ndarray :param :returns: mantid workspaces of S for atom (total) and individual quantum orders :returntype: list of Workspace2D """ atom_workspaces = [] s_atom_data.fill(0.0) internal_atom_label = "atom_%s" % (atom_number - 1) output_atom_label = "%s_%d" % (abins.constants.ATOM_PREFIX, atom_number) symbol = self._extracted_ab_initio_data[internal_atom_label]["symbol"] z_number = Atom(symbol=symbol).z_number for i, order in enumerate( range( abins.constants.FUNDAMENTALS, self._num_quantum_order_events + abins.constants.S_LAST_INDEX)): s_atom_data[i] = s_data_extracted[internal_atom_label]["s"][ "order_%s" % order] total_s_atom_data = np.sum(s_atom_data, axis=0) atom_workspaces = [] atom_workspaces.append( self._create_workspace(atom_name=output_atom_label, s_points=np.copy(total_s_atom_data), optional_name="_total", protons_number=z_number)) atom_workspaces.append( self._create_workspace(atom_name=output_atom_label, s_points=np.copy(s_atom_data), protons_number=z_number)) return atom_workspaces
def _create_workspaces(self, atoms_symbols=None, s_data=None): """ Creates workspaces for all types of atoms. Creates both partial and total workspaces for all types of atoms. :param atoms_symbols: list of atom types for which S should be created :param s_data: dynamical factor data of type SData :returns: workspaces for list of atoms types, S for the particular type of atom """ s_data_extracted = s_data.extract() shape = [self._num_quantum_order_events] shape.extend(list(s_data_extracted["atom_0"]["s"]["order_1"].shape)) s_atom_data = np.zeros(shape=tuple(shape), dtype=AbinsModules.AbinsConstants.FLOAT_TYPE) shape.pop(0) num_atoms = len( [key for key in s_data_extracted.keys() if "atom" in key]) temp_s_atom_data = np.copy(s_atom_data) result = [] masses = {} for i in range(num_atoms): symbol = self._extracted_ab_initio_data["atom_%s" % i]["symbol"] mass = self._extracted_ab_initio_data["atom_%s" % i]["mass"] if symbol not in masses: masses[symbol] = set() masses[symbol].add(mass) one_m = AbinsModules.AbinsConstants.ONLY_ONE_MASS eps = AbinsModules.AbinsConstants.MASS_EPS # convert set to list to fix order for s in masses: masses[s] = sorted(list(set(masses[s]))) for symbol in atoms_symbols: sub = len(masses[symbol]) > one_m or abs( Atom(symbol=symbol).mass - masses[symbol][0]) > eps for m in masses[symbol]: result.extend( self._atom_type_s(num_atoms=num_atoms, mass=m, s_data_extracted=s_data_extracted, element_symbol=symbol, temp_s_atom_data=temp_s_atom_data, s_atom_data=s_atom_data, substitution=sub)) return result
def create_workspaces(self, atoms_symbols=None, atom_numbers=None, *, s_data, atoms_data, max_quantum_order): """ Creates workspaces for all types of atoms. Creates both partial and total workspaces for given types of atoms. :param atoms_symbols: atom types (i.e. element symbols) for which S should be created. :type iterable of str: :param atom_numbers: indices of individual atoms for which S should be created. (One-based numbering; 1 <= I <= NUM_ATOMS) :type iterable of int: :param s_data: dynamical factor data :type abins.SData :param atoms_data: atom positions/masses :type abins.AtomsData: :param max_quantum_order: maximum quantum order to include :type int: :returns: workspaces for list of atoms types, S for the particular type of atom """ from abins.constants import FLOAT_TYPE, MASS_EPS, ONLY_ONE_MASS # Create appropriately-shaped arrays to be used in-place by _atom_type_s - avoid repeated slow instantiation shape = [max_quantum_order] shape.extend(list(s_data[0]["order_1"].shape)) s_atom_data = np.zeros(shape=tuple(shape), dtype=FLOAT_TYPE) temp_s_atom_data = np.copy(s_atom_data) num_atoms = len(s_data) masses = self.get_masses_table(atoms_data) result = [] if atoms_symbols is not None: for symbol in atoms_symbols: sub = (len(masses[symbol]) > ONLY_ONE_MASS or abs(Atom(symbol=symbol).mass - masses[symbol][0]) > MASS_EPS) for m in masses[symbol]: result.extend(self._atom_type_s(num_atoms=num_atoms, mass=m, s_data=s_data, atoms_data=atoms_data, element_symbol=symbol, temp_s_atom_data=temp_s_atom_data, s_atom_data=s_atom_data, substitution=sub)) if atom_numbers is not None: for atom_number in atom_numbers: result.extend(self._atom_number_s(atom_number=atom_number, s_data=s_data, s_atom_data=s_atom_data, atoms_data=atoms_data)) return result
def _read_atomic_coordinates(self, file_obj=None, data=None, masses_from_file=None): """ Reads atomic coordinates from .log GAUSSIAN file. :param file_obj: file object from which we read :param data: Python dictionary to which atoms data should be added :param masses_from_file: masses read from an ab initio output file """ atoms = {} atom_indx = 0 end_msgs = [ "---------------------------------------------------------------------" ] header_lines = 5 # Input orientation: # --------------------------------------------------------------------- # Center Atomic Atomic Coordinates (Angstroms) # Number Number Type X Y Z # --------------------------------------------------------------------- for i in range(header_lines): file_obj.readline() while not self._parser.block_end(file_obj=file_obj, msg=end_msgs): line = file_obj.readline() entries = line.split() z_number = int(entries[1]) atom = Atom(z_number=z_number) coord = np.asarray([float(i) for i in entries[3:6]]) atoms["atom_{}".format(atom_indx)] = { "symbol": atom.symbol, "mass": atom.mass, "sort": atom_indx, "coord": coord } atom_indx += 1 self.check_isotopes_substitution(atoms=atoms, masses=masses_from_file, approximate=True) self._num_atoms = len(atoms) data["atoms"] = atoms
def _atom_number_s(self, *, atom_number, s_data, s_atom_data, atoms_data): """ Helper function for calculating S for the given atomic index :param atom_number: One-based index of atom in s_data e.g. 1 to select first element 'atom_1' :type atom_number: int :param s_data: Precalculated S for all atoms and quantum orders :type s_data: abins.SData :param s_atom_data: helper array to accumulate S (outer loop over atoms); does not transport information but is used in-place to save on time instantiating large arrays. First dimension is quantum order; following dimensions should match arrays in s_data. :type s_atom_data: numpy.ndarray :param :returns: mantid workspaces of S for atom (total) and individual quantum orders :returntype: list of Workspace2D """ from abins.constants import ATOM_PREFIX, FUNDAMENTALS atom_workspaces = [] s_atom_data.fill(0.0) output_atom_label = "%s_%d" % (ATOM_PREFIX, atom_number) symbol = atoms_data[atom_number - 1]["symbol"] z_number = Atom(symbol=symbol).z_number for i, order in enumerate( range(FUNDAMENTALS, self._max_event_order + 1)): s_atom_data[i] = s_data[atom_number - 1]["order_%s" % order] total_s_atom_data = np.sum(s_atom_data, axis=0) atom_workspaces = [] atom_workspaces.append( self._create_workspace(atom_name=output_atom_label, s_points=np.copy(total_s_atom_data), optional_name="_total", protons_number=z_number)) atom_workspaces.append( self._create_workspace(atom_name=output_atom_label, s_points=np.copy(s_atom_data), protons_number=z_number)) return atom_workspaces
def _create_atoms_data(self, data=None, coord_lines=None): """ Creates Python dictionary with atoms data which can be easily converted to AbinsData object. :return: Python dictionary which can easily be converted to AbinsData object """ data.update({"atoms": dict()}) for i, line in enumerate(coord_lines): l = line.split() symbol = str(l[2].capitalize()) atom = Atom(symbol=symbol) data["atoms"]["atom_%s" % i] = { "symbol": symbol, "mass": atom.mass, "sort": i, "fract_coord": np.asarray(l[3:6]).astype( dtype=AbinsModules.AbinsConstants.FLOAT_TYPE) }
def create_kpoints_data_helper(self, atomic_displacements=None, atomic_coordinates=None, row=None, column=None, freq_num=None, row_width=6): """ Computes normalisation constant for displacements and builds a block of coordinates. :param atomic_displacements: list with atomic displacements :param atomic_coordinates: list with atomic coordinates :param row: number of atomic_displacements row to parse :param column: number of atomic_displacements column to parse :param freq_num: number of mode (frequency) :param row_width: current width of row to parse """ xdisp = atomic_displacements[0] ydisp = atomic_displacements[1] zdisp = atomic_displacements[2] atom_num = -1 # Compute normalisation constant for displacements # and build block of normalised coordinates. normalised_coordinates = [] norm_const1 = 0. for line in atomic_coordinates: atom_num += 1 l = line.split() indx = row * len( atomic_coordinates) * 6 + atom_num * row_width + column if indx <= len(xdisp) - 1: x = xdisp[indx] y = ydisp[indx] z = zdisp[indx] norm_const1 += (x * x.conjugate() + y * y.conjugate() + z * z.conjugate()).real normalised_coordinates += [[ atom_num + 1, l[2], int(l[1]), x, y, z ]] # Normalise displacements and multiply displacements by sqrt(mass)-> xn, yn, zn xn = [] yn = [] zn = [] norm_const1 = sqrt(norm_const1) norm = 0.0 for item in normalised_coordinates: atom = Atom(symbol=str(item[1]).capitalize()) mass = atom.mass x = item[3] / norm_const1 * sqrt(mass) y = item[4] / norm_const1 * sqrt(mass) z = item[5] / norm_const1 * sqrt(mass) xn += [x] yn += [y] zn += [z] norm += (x * x.conjugate() + y * y.conjugate() + z * z.conjugate()).real # Normalise displacements normf = 0.0 ii = -1 # noinspection PyAssignmentToLoopOrWithParameter local_displacements = [] for _ in normalised_coordinates: ii += 1 x = xn[ii] / sqrt(norm) y = yn[ii] / sqrt(norm) z = zn[ii] / sqrt(norm) normf += (x * x.conjugate() + y * y.conjugate() + z * z.conjugate()).real local_displacements.append([x, y, z]) logger.debug("Mode {0} normalised to {1}".format( str(freq_num + 1), str(normf))) return local_displacements