diff --git a/.gitignore b/.gitignore index 8657568..9680353 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ *.swp ~* *~ +build/ +dist/ .project *.egg* .DS_Store diff --git a/.travis.yml b/.travis.yml index 3ece177..3b05b9f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ matrix: - python: 3.6 env: TEST_TYPE="version" - python: 3.6 - env: TEST_TYPE="pytest" TEST_AIIDA_BACKEND="django" + env: TEST_TYPE="pytest" TEST_AIIDA_BACKEND="django" PYPI_DEPLOY=true - python: 2.7 env: TEST_TYPE="pytest" TEST_AIIDA_BACKEND="django" - python: 3.6 @@ -81,3 +81,14 @@ script: # after_success: # - coveralls + +deploy: + - provider: pypi + distributions: "sdist bdist_wheel" + user: cjsewell + password: + secure: Jrqfy5jCez0BKUi5dBi+pXxRBlmF24T53ZL6FumhKOTXgsjk8hp723GGJ4iJwdmnDBPQX029cJ0ebwZrNjTCI538TNDQaT+f2B7pUp0HG5qreWhbYQaMEU20wPvvyIbiI3Wei87PvouV2OHLelWj9/Q8Gy7GHQr1+66g/H83E27WgG56Qer3HcGCcUHZ03iByq640eF1ipqMs276pI9/tzeiCPqnOo5tlB2Nb+5McKtMoBnB8zn8930FxkYY3BijZWTN2fiEMx9ezDQDutGiZ6HU9c8ZJhw4A0pAETVIbdZAWcHyqnAknruX3q4eb8JKzKCEMBSiH4YS/0Sl5In4jTkrhTCiWLr5ViNDor9P+XNW6pgNS1BSdePj+gQ+E1v/+DMgnHJ4pcch2aILYAvF7R8txxEc+5xVtalGK0KjNXs+2eK/57Fqpozy2SnqfqlV1DeS+fo+/LAvyl0p7Gxt/mJCQgpHZOrcEsfW0PC2WsVmn0E5n7HAivTd3tqeY8z97KtQHiKGNa0SMzUvO7Gt9AxXtXG1sdQBJgdCO9iYpsk37WIlCsmzJM+w9cxbU69/QkKLPvr+PSxy6A3FeWKKonJ6uSlALl6rljOqutDlAKjIrsfSlykm9UPBS6yy2Ist2YJF6YRmeiaEA+3MGArZONaEmxOiau06BmtzFSMf7CM= + on: + branch: master + tags: true + condition: $PYPI_DEPLOY = true diff --git a/README.md b/README.md index 945b45f..dcf40a3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ [![Build Status](https://travis-ci.org/abelcarreras/aiida-lammps.svg?branch=master)](https://travis-ci.org/abelcarreras/aiida-lammps) +[![PyPI](https://img.shields.io/pypi/v/aiida-lammps.svg)](https://pypi.python.org/pypi/aiida-lammps/) # AiiDA LAMMPS plugin @@ -14,6 +15,7 @@ Note: `lammps.combinate` requires `aiida-phonopy` (https://github.com/abelcarrer plugin to work, DynaPhoPy can be found in: https://github.com/abelcarreras/aiida-phonopy - [AiiDA LAMMPS plugin](#AiiDA-LAMMPS-plugin) + - [Installation](#Installation) - [Built-in Potential Support](#Built-in-Potential-Support) - [Examples](#Examples) - [Code Setup](#Code-Setup) @@ -23,6 +25,14 @@ plugin to work, DynaPhoPy can be found in: https://github.com/abelcarreras/aiida - [Optimisation Calculation](#Optimisation-Calculation) - [MD Calculation](#MD-Calculation) +## Installation + +To install from pypi: + +```shell +pip install aiida-lammps +``` + ## Built-in Potential Support - EAM diff --git a/aiida_lammps/__init__.py b/aiida_lammps/__init__.py index 342dbf2..7c5a130 100644 --- a/aiida_lammps/__init__.py +++ b/aiida_lammps/__init__.py @@ -1 +1 @@ -__version__ = "0.4.0b3" +__version__ = "0.4.1b3" diff --git a/aiida_lammps/calculations/lammps/__init__.py b/aiida_lammps/calculations/lammps/__init__.py index 65a5c7e..d829b06 100644 --- a/aiida_lammps/calculations/lammps/__init__.py +++ b/aiida_lammps/calculations/lammps/__init__.py @@ -110,7 +110,7 @@ class BaseLammpsCalculation(CalcJob): _DEFAULT_OUTPUT_INFO_FILE_NAME = "system_info.dump" _DEFAULT_OUTPUT_RESTART_FILE_NAME = 'lammps.restart' - _retrieve_list = ['log.lammps'] + _retrieve_list = [] _retrieve_temporary_list = [] _cmdline_params = ['-in', _INPUT_FILE_NAME] _stdout_name = None @@ -123,6 +123,8 @@ def define(cls, spec): help='lammps potential') spec.input('parameters', valid_type=Dict, help='the parameters', required=False) + spec.input('metadata.options.cell_transform_filename', + valid_type=six.string_types, default="cell_transform.npy") spec.input('metadata.options.output_filename', valid_type=six.string_types, default=cls._DEFAULT_OUTPUT_FILE_NAME) spec.input('metadata.options.trajectory_name', @@ -199,8 +201,11 @@ def prepare_for_submission(self, tempfolder): potential_txt = self.inputs.potential.get_potential_file() # Setup structure - structure_txt = generate_lammps_structure(self.inputs.structure, - self.inputs.potential.atom_style) + structure_txt, struct_transform = generate_lammps_structure( + self.inputs.structure, self.inputs.potential.atom_style) + + with open(tempfolder.get_abs_path(self.options.cell_transform_filename), 'w+b') as handle: + np.save(handle, struct_transform) if "parameters" in self.inputs: parameters = self.inputs.parameters @@ -255,7 +260,9 @@ def prepare_for_submission(self, tempfolder): calcinfo = CalcInfo() calcinfo.uuid = self.uuid - calcinfo.retrieve_list = self._retrieve_list + calcinfo.retrieve_list = self._retrieve_list + [ + self.options.output_filename, + self.options.cell_transform_filename] calcinfo.retrieve_temporary_list = self._retrieve_temporary_list calcinfo.codes_info = [codeinfo] diff --git a/aiida_lammps/common/generate_structure.py b/aiida_lammps/common/generate_structure.py index 8d3fbdd..a093663 100644 --- a/aiida_lammps/common/generate_structure.py +++ b/aiida_lammps/common/generate_structure.py @@ -1,84 +1,35 @@ """ creation of the structure file content - -The code here is largely adapted from https://github.com/andeplane/cif2cell-lammps -ESPInterfaces.LAMMPSFile """ -from math import cos, sin, atan2 -import ase +# import ase import numpy as np -def get_vector(value): - return [float(v) for v in value] - - -def get_matrix(value): - return [[float(v) for v in vec] for vec in value] - - -def mvmult3(mat, vec): - """ matrix-vector multiplication """ - w = [0., 0., 0.] - for i in range(3): - t = 0 - for j in range(3): - t = t + mat[j][i] * vec[j] - w[i] = t - return w - - -def cartesian_to_frac(lattice, ccoords): - """convert cartesian coordinate to fractional +def transform_cell(cell): + """transform the cell to an orientation, compatible with LAMMPS - Parameters - ---------- - lattice: list - 3x3 array of lattice vectors - ccoord: list - Nx3 cartesian coordinates + LAMMPS requires the simulation cell to be in the format of a + lower triangular matrix (right-handed basis). + Therefore the cell and positions may require rotation and inversion. + See https://lammps.sandia.gov/doc/Howto_triclinic.html - Returns - ------- - list: - Nx3 array of fractional coordinate + :param cell: (3x3) lattice + :returns: (new_cell, transform) """ - det3 = np.linalg.det - latt_tr = np.transpose(lattice) - det_latt_tr = np.linalg.det(latt_tr) - - fcoords = [] - for ccoord in ccoords: - a = (det3([[ccoord[0], latt_tr[0][1], latt_tr[0][2]], - [ccoord[1], latt_tr[1][1], latt_tr[1][2]], - [ccoord[2], latt_tr[2][1], latt_tr[2][2]]])) / det_latt_tr - b = (det3([[latt_tr[0][0], ccoord[0], latt_tr[0][2]], - [latt_tr[1][0], ccoord[1], latt_tr[1][2]], - [latt_tr[2][0], ccoord[2], latt_tr[2][2]]])) / det_latt_tr - c = (det3([[latt_tr[0][0], latt_tr[0][1], ccoord[0]], - [latt_tr[1][0], latt_tr[1][1], ccoord[1]], - [latt_tr[2][0], latt_tr[2][1], ccoord[2]]])) / det_latt_tr - - fcoords.append([a, b, c]) - - return fcoords - - -def is_not_zero(value): - return not np.isclose(value, 0) - - -def round_by(value, round_dp): - if round_dp is None: - return value - return round(value, round_dp) + cell = np.array(cell) + transform, upper_tri = np.linalg.qr(cell.T, mode="complete") + new_cell = np.transpose(upper_tri) + # LAMMPS also requires positive values on the diagonal of the, + # so invert cell if necessary + inversion = np.eye(3) + for i in range(3): + if new_cell[i][i] < 0.0: + inversion[i][i] = -1.0 + new_cell = np.dot(inversion, new_cell.T).T + transform = np.dot(transform, inversion.T).T -class AtomSite(object): - def __init__(self, kind_name, cartesian, fractional=None): - self.kind_name = kind_name - self.cartesian = cartesian - self.fractional = fractional + return new_cell, transform def generate_lammps_structure(structure, @@ -100,17 +51,22 @@ def generate_lammps_structure(structure, docstring : str docstring to put at top of file + Returns + ------- + str: content + the structure file content + numpy.array: transform + the transformation matrix applied to the structure cell and coordinates + """ if atom_style not in ['atomic', 'charge']: raise ValueError("atom_style must be in ['atomic', 'charge']") if charge_dict is None: charge_dict = {} - atom_sites = [AtomSite(site.kind_name, site.position) - for site in structure.sites] # mapping of atom kind_name to id number kind_name_id_map = {} - for site in atom_sites: + for site in structure.sites: if site.kind_name not in kind_name_id_map: kind_name_id_map[site.kind_name] = len(kind_name_id_map) + 1 # mapping of atom kind_name to mass @@ -118,80 +74,22 @@ def generate_lammps_structure(structure, filestring = "" filestring += "# {}\n\n".format(docstring) - filestring += "{0} atoms\n".format(len(atom_sites)) + filestring += "{0} atoms\n".format(len(structure.sites)) filestring += "{0} atom types\n\n".format(len(kind_name_id_map)) - lattice = get_matrix(structure.cell) - - # As per https://lammps.sandia.gov/doc/Howto_triclinic.html, - # if the lattice does not conform to a regular parallelpiped - # then it must first be rotated - - if is_not_zero(lattice[0][1]) or is_not_zero(lattice[0][2]) or is_not_zero(lattice[1][2]): - rotated_cell = True - for site in atom_sites: - site.fractional = cartesian_to_frac(lattice, [site.cartesian])[0] - # creating the cell from its lengths and angles, - # generally ensures that it is in a compatible orientation - atoms = ase.Atoms(cell=structure.cell_lengths + structure.cell_angles) - lattice = get_matrix(atoms.cell) - else: - rotated_cell = False - - if is_not_zero(lattice[0][1]): - theta = atan2(-lattice[0][1], lattice[0][0]) - rot_matrix = get_matrix([ - [cos(theta), sin(theta), 0], - [-sin(theta), cos(theta), 0], - [0, 0, 1] - ]) - lattice[0] = get_vector(mvmult3(rot_matrix, lattice[0])) - lattice[1] = get_vector(mvmult3(rot_matrix, lattice[1])) - lattice[2] = get_vector(mvmult3(rot_matrix, lattice[2])) - - if is_not_zero(lattice[0][2]): - theta = atan2(-lattice[0][2], lattice[0][0]) - rot_matrix = get_matrix([ - [cos(theta), sin(theta), 0], - [0, 1, 0], - [-sin(theta), cos(theta), 0] - ]) - lattice[0] = get_vector(mvmult3(rot_matrix, lattice[0])) - lattice[1] = get_vector(mvmult3(rot_matrix, lattice[1])) - lattice[2] = get_vector(mvmult3(rot_matrix, lattice[2])) + atoms = structure.get_ase() + cell, coord_transform = transform_cell(atoms.cell) + positions = np.transpose(np.dot(coord_transform, np.transpose(atoms.positions))) - if is_not_zero(lattice[1][2]): - theta = atan2(-lattice[1][2], lattice[1][1]) - rot_matrix = get_matrix([ - [1, 0, 0], - [0, cos(theta), sin(theta)], - [0, -sin(theta), cos(theta)] - ]) - lattice[0] = get_vector(mvmult3(rot_matrix, lattice[0])) - lattice[1] = get_vector(mvmult3(rot_matrix, lattice[1])) - lattice[2] = get_vector(mvmult3(rot_matrix, lattice[2])) + if round_dp: + cell = np.round(cell, round_dp) + 0. + positions = np.round(positions, round_dp) + 0. - if is_not_zero(lattice[0][1]) or is_not_zero(lattice[0][2]) or is_not_zero(lattice[1][2]) or lattice[0][0] < 1e-9 or lattice[1][1] < 1e-9 or lattice[2][2] < 1e-9: - raise ValueError( - "Error in triclinic box: {}\n" - "Vectors should follow these rules: " - "https://lammps.sandia.gov/doc/Howto_triclinic.html".format(lattice)) - - a = round_by(lattice[0][0], round_dp) - b = round_by(lattice[1][1], round_dp) - c = round_by(lattice[2][2], round_dp) - - filestring += "0.0 {0:20.10f} xlo xhi\n".format(a) - filestring += "0.0 {0:20.10f} ylo yhi\n".format(b) - filestring += "0.0 {0:20.10f} zlo zhi\n".format(c) - - xy = round_by(lattice[1][0], round_dp) - xz = round_by(lattice[2][0], round_dp) - yz = round_by(lattice[2][1], round_dp) - - if is_not_zero(xy) or is_not_zero(xz) or is_not_zero(yz): - filestring += "{0:20.10f} {1:20.10f} {2:20.10f} xy xz yz\n\n".format( - xy, xz, yz) + filestring += "0.0 {0:20.10f} xlo xhi\n".format(cell[0][0]) + filestring += "0.0 {0:20.10f} ylo yhi\n".format(cell[1][1]) + filestring += "0.0 {0:20.10f} zlo zhi\n".format(cell[2][2]) + filestring += "{0:20.10f} {1:20.10f} {2:20.10f} xy xz yz\n\n".format( + cell[1][0], cell[2][0], cell[2][1]) filestring += 'Masses\n\n' for kind_name in sorted(list(kind_name_id_map.keys())): @@ -201,12 +99,7 @@ def generate_lammps_structure(structure, filestring += "Atoms\n\n" - for site_index, site in enumerate(atom_sites): - if rotated_cell: - pos = get_vector(mvmult3(lattice, site.fractional)) - else: - pos = site.cartesian - pos = [round_by(v, round_dp) for v in pos] + for site_index, (pos, site) in enumerate(zip(positions, structure.sites)): kind_id = kind_name_id_map[site.kind_name] @@ -218,75 +111,6 @@ def generate_lammps_structure(structure, filestring += "{0} {1} {2} {3:20.10f} {4:20.10f} {5:20.10f}\n".format( site_index + 1, kind_id, charge, pos[0], pos[1], pos[2]) else: - raise ValueError('atom_style') - - return filestring - - -def old_generate_lammps_structure(structure, atom_style): - """ this is the deprecated method, used before 0.3.0b3, - stored here for prosperity. - - This method can create erroneous structures for triclinic cells - """ - import numpy as np - - types = [site.kind_name for site in structure.sites] - - type_index_unique = np.unique(types, return_index=True)[1] - count_index_unique = np.diff(np.append(type_index_unique, [len(types)])) - - atom_index = [] - for i, index in enumerate(count_index_unique): - atom_index += [i for j in range(index)] - - masses = [site.mass for site in structure.kinds] - positions = [site.position for site in structure.sites] - - number_of_atoms = len(positions) - - lammps_data_file = 'Generated using dynaphopy\n\n' - lammps_data_file += '{0} atoms\n\n'.format(number_of_atoms) - lammps_data_file += '{0} atom types\n\n'.format(len(masses)) - - cell = np.array(structure.cell) - - a = np.linalg.norm(cell[0]) - b = np.linalg.norm(cell[1]) - c = np.linalg.norm(cell[2]) - - alpha = np.arccos(np.dot(cell[1], cell[2]) / (c * b)) - gamma = np.arccos(np.dot(cell[1], cell[0]) / (a * b)) - beta = np.arccos(np.dot(cell[2], cell[0]) / (a * c)) - - xhi = a - xy = b * np.cos(gamma) - xz = c * np.cos(beta) - yhi = np.sqrt(pow(b, 2) - pow(xy, 2)) - yz = (b * c * np.cos(alpha) - xy * xz) / yhi - zhi = np.sqrt(pow(c, 2) - pow(xz, 2) - pow(yz, 2)) - - xhi = xhi + max(0, 0, xy, xz, xy + xz) - yhi = yhi + max(0, 0, yz) - - lammps_data_file += '\n{0:20.10f} {1:20.10f} xlo xhi\n'.format(0, xhi) - lammps_data_file += '{0:20.10f} {1:20.10f} ylo yhi\n'.format(0, yhi) - lammps_data_file += '{0:20.10f} {1:20.10f} zlo zhi\n'.format(0, zhi) - lammps_data_file += '{0:20.10f} {1:20.10f} {2:20.10f} xy xz yz\n\n'.format( - xy, xz, yz) - - lammps_data_file += 'Masses\n\n' - - for i, mass in enumerate(masses): - lammps_data_file += '{0} {1:20.10f} \n'.format(i + 1, mass) - - lammps_data_file += '\nAtoms\n\n' - for i, row in enumerate(positions): - if atom_style == 'charge': - lammps_data_file += '{0} {1} 0.0 {2:20.10f} {3:20.10f} {4:20.10f}\n'.format( - i + 1, atom_index[i] + 1, row[0], row[1], row[2]) - else: - lammps_data_file += '{0} {1} {2:20.10f} {3:20.10f} {4:20.10f}\n'.format( - i + 1, atom_index[i] + 1, row[0], row[1], row[2]) + raise ValueError('atom_style unknown: {}'.format(atom_style)) - return lammps_data_file + return filestring, coord_transform diff --git a/aiida_lammps/tests/test_generate_structure.py b/aiida_lammps/tests/test_generate_structure.py index 5288c70..772f029 100644 --- a/aiida_lammps/tests/test_generate_structure.py +++ b/aiida_lammps/tests/test_generate_structure.py @@ -11,5 +11,5 @@ ]) def test_generate(db_test_app, get_structure_data, structure, file_regression): structure = get_structure_data(structure) - file_regression.check(six.ensure_text( - generate_lammps_structure(structure, round_dp=8))) + text, transform = generate_lammps_structure(structure, round_dp=8) + file_regression.check(six.ensure_text(text)) diff --git a/aiida_lammps/tests/test_generate_structure/test_generate_Fe_.txt b/aiida_lammps/tests/test_generate_structure/test_generate_Fe_.txt index e749747..a97698d 100644 --- a/aiida_lammps/tests/test_generate_structure/test_generate_Fe_.txt +++ b/aiida_lammps/tests/test_generate_structure/test_generate_Fe_.txt @@ -6,6 +6,8 @@ 0.0 2.8481160000 xlo xhi 0.0 2.8481160000 ylo yhi 0.0 2.8481160000 zlo zhi + 0.0000000000 0.0000000000 0.0000000000 xy xz yz + Masses 1 55.8450000000 diff --git a/aiida_lammps/tests/test_generate_structure/test_generate_pyrite_.txt b/aiida_lammps/tests/test_generate_structure/test_generate_pyrite_.txt index e15b880..4f89112 100644 --- a/aiida_lammps/tests/test_generate_structure/test_generate_pyrite_.txt +++ b/aiida_lammps/tests/test_generate_structure/test_generate_pyrite_.txt @@ -6,6 +6,8 @@ 0.0 5.3800000000 xlo xhi 0.0 5.3800000000 ylo yhi 0.0 5.3800000000 zlo zhi + 0.0000000000 0.0000000000 0.0000000000 xy xz yz + Masses 1 55.8450000000 diff --git a/setup.json b/setup.json index b092f0e..346c416 100644 --- a/setup.json +++ b/setup.json @@ -1,8 +1,8 @@ { "name": "aiida-lammps", - "version": "0.4.0b3", + "version": "0.4.1b3", "description": "AiiDA plugin for LAMMPS", - "url": "https://github.com/abelcarreras/aiida_extensions", + "url": "https://github.com/abelcarreras/aiida-lammps", "author": "Abel Carreras", "author_email": "abelcarreras83@gmail.com", "license": "MIT license", @@ -10,7 +10,7 @@ "aiida-core==1.0.0b3", "numpy", "packaging", - "dateutils", + "python-dateutil", "jsonschema", "six", "ase>=3.12.0,<4.0.0"