diff --git a/Testing/Data/UnitTest/toluene_molecule_LoadCRYSTAL17.out.md5 b/Testing/Data/UnitTest/toluene_molecule_LoadCRYSTAL17.out.md5 new file mode 100644 index 0000000000000000000000000000000000000000..ad0ef41e824577999c11d33d4baec2df970975ed --- /dev/null +++ b/Testing/Data/UnitTest/toluene_molecule_LoadCRYSTAL17.out.md5 @@ -0,0 +1 @@ +fbcbdd6fc11f4e74751257626ce6d03f diff --git a/Testing/Data/UnitTest/toluene_molecule_LoadCRYSTAL17_atomic_displacements_data_0.txt.md5 b/Testing/Data/UnitTest/toluene_molecule_LoadCRYSTAL17_atomic_displacements_data_0.txt.md5 new file mode 100644 index 0000000000000000000000000000000000000000..790eae8b899411b9c1f8af49aadb9ab5993d438d --- /dev/null +++ b/Testing/Data/UnitTest/toluene_molecule_LoadCRYSTAL17_atomic_displacements_data_0.txt.md5 @@ -0,0 +1 @@ +5963d231bfed4a4fdc3d9a27f6903026 diff --git a/Testing/Data/UnitTest/toluene_molecule_LoadCRYSTAL17_data.txt.md5 b/Testing/Data/UnitTest/toluene_molecule_LoadCRYSTAL17_data.txt.md5 new file mode 100644 index 0000000000000000000000000000000000000000..9c0d4e946b8c531c6dfe16f732e87d75e79bdc40 --- /dev/null +++ b/Testing/Data/UnitTest/toluene_molecule_LoadCRYSTAL17_data.txt.md5 @@ -0,0 +1 @@ +dcedfbda8976f92bc8775e1e1aa15362 diff --git a/docs/source/release/v4.3.0/indirect_geometry.rst b/docs/source/release/v4.3.0/indirect_geometry.rst index da4c2242c5a0d8e6716d7bfa96c89aa3125fe02e..1e7455c3e72a4378db5959788c5dec1d3f50cd0d 100644 --- a/docs/source/release/v4.3.0/indirect_geometry.rst +++ b/docs/source/release/v4.3.0/indirect_geometry.rst @@ -9,4 +9,10 @@ Indirect Geometry Changes putting new features at the top of the section, followed by improvements, followed by bug fixes. -:ref:`Release 4.3.0 <v4.3.0>` \ No newline at end of file + +BugFixes +######## + +- The Abins file parser no longer fails to read data from non-periodic vibration calculations performed with CRYSTAL17. + +:ref:`Release 4.3.0 <v4.3.0>` diff --git a/scripts/AbinsModules/GeneralAbInitioParser.py b/scripts/AbinsModules/GeneralAbInitioParser.py index 871dfe53b38310b7fce45ebc78d983ad5d15669b..0cc3d00d42285346a2df1f760c293d34b0accf60 100644 --- a/scripts/AbinsModules/GeneralAbInitioParser.py +++ b/scripts/AbinsModules/GeneralAbInitioParser.py @@ -7,6 +7,7 @@ from __future__ import (absolute_import, division, print_function) import six +import re import AbinsModules @@ -17,18 +18,40 @@ class GeneralAbInitioParser(object): def __init__(self): pass - def find_first(self, file_obj=None, msg=None): + def find_first(self, file_obj=None, msg=None, regex=None): """ Finds the first line with msg. Moves file current position to the next line. :param file_obj: file object from which we read - :param msg: keyword to find + :type file_obj: BinaryIO + :param msg: keyword to find (exact match) + :type msg: str + :param regex: regular expression to find (use *instead of* msg option). + This string will be compiled to a Python re.Pattern . + :type regex: str """ if not six.PY2: - msg = bytes(msg, "utf8") - while not self.file_end(file_obj=file_obj): - line = file_obj.readline() - if line.strip() and msg in line: - return line + if msg is not None: + msg = bytes(msg, "utf8") + if regex is not None: + regex = bytes(regex, "utf8") + + if msg and regex: + raise ValueError("msg or regex should be provided, not both") + elif msg: + while not self.file_end(file_obj=file_obj): + line = file_obj.readline() + if line.strip() and msg in line: + return line + raise ValueError("'{}' not found".format(msg)) + elif regex: + test = re.compile(regex) + while not self.file_end(file_obj=file_obj): + line = file_obj.readline() + if test.match(line): + return(line) + raise ValueError("'{}' not found".format(regex)) + else: + raise ValueError("No msg or regex provided: nothing to match") def find_last(self, file_obj=None, msg=None): """ diff --git a/scripts/AbinsModules/GeneralLoadAbInitioTester.py b/scripts/AbinsModules/GeneralLoadAbInitioTester.py index 5d6d26f129af3421196653572380bb3a77414039..08688c7f809dff764e6c5adb0124e17c5e4d6fc2 100644 --- a/scripts/AbinsModules/GeneralLoadAbInitioTester.py +++ b/scripts/AbinsModules/GeneralLoadAbInitioTester.py @@ -14,11 +14,16 @@ class GeneralLoadAbInitioTester(object): _loaders_extensions = {"LoadCASTEP": "phonon", "LoadCRYSTAL": "out", "LoadDMOL3": "outmol", "LoadGAUSSIAN": "log"} - # noinspection PyMethodMayBeStatic - def _prepare_data(self, filename=None): - """Reads reference values from ASCII file.""" + @staticmethod + def _prepare_data(seedname): + """Reads reference values from ASCII files - with open(AbinsModules.AbinsTestHelpers.find_file(filename + "_data.txt")) as data_file: + :param seedname: Reference data will read from the file {seedname}_data.txt, except for the atomic displacements + which are read from files {seedname}_atomic_displacements_data_{I}.txt, where {I} are k-point indices. + :type seedname: str + """ + + with open(AbinsModules.AbinsTestHelpers.find_file(seedname + "_data.txt")) as data_file: correct_data = json.loads(data_file.read().replace("\n", " ")) num_k = len(correct_data["datasets"]["k_points_data"]["weights"]) @@ -26,9 +31,9 @@ class GeneralLoadAbInitioTester(object): array = {} for k in range(num_k): - temp = np.loadtxt( - AbinsModules.AbinsTestHelpers.find_file( - filename + "_atomic_displacements_data_%s.txt" % k)).view(complex).reshape(-1) + temp = np.loadtxt(AbinsModules.AbinsTestHelpers.find_file( + "{seedname}_atomic_displacements_data_{k}.txt".format(seedname=seedname, k=k)) + ).view(complex).reshape(-1) total_size = temp.size num_freq = int(total_size / (atoms * 3)) array[str(k)] = temp.reshape(atoms, num_freq, 3) @@ -123,13 +128,16 @@ class GeneralLoadAbInitioTester(object): data = self._read_ab_initio(loader=loader, filename=name, extension=extension) # get correct data - correct_data = self._prepare_data(filename=name) + correct_data = self._prepare_data(name) # check read data self._check_reader_data(correct_data=correct_data, data=data, filename=name, extension=extension) # check loaded data - self._check_loader_data(correct_data=correct_data, input_ab_initio_filename=name, extension=extension, loader=loader) + self._check_loader_data(correct_data=correct_data, + input_ab_initio_filename=name, + extension=extension, + loader=loader) def _read_ab_initio(self, loader=None, filename=None, extension=None): """ @@ -146,15 +154,15 @@ class GeneralLoadAbInitioTester(object): read_filename = AbinsModules.AbinsTestHelpers.find_file(filename=filename + "." + extension.upper()) ab_initio_reader = loader(input_ab_initio_filename=read_filename) - data = self._get_reader_data(ab_initio_reader=ab_initio_reader) + data = self._get_reader_data(ab_initio_reader) # test validData method self.assertEqual(True, ab_initio_reader._clerk._valid_hash()) return data - # noinspection PyMethodMayBeStatic - def _get_reader_data(self, ab_initio_reader=None): + @staticmethod + def _get_reader_data(ab_initio_reader): """ :param ab_initio_reader: object of type GeneralAbInitioProgram :returns: read data @@ -165,3 +173,46 @@ class GeneralLoadAbInitioTester(object): } data["datasets"].update({"unit_cell": ab_initio_reader._clerk._data["unit_cell"]}) return data + + @classmethod + def save_ab_initio_test_data(cls, ab_initio_reader, seedname): + """ + Write ab initio calculation data to JSON file for use in test cases + + :param ab_initio_reader: Reader after import of external calculation + :type ab_initio_reader: AbinsModules.GeneralAbInitioProgram + :param filename: Seed for text files for JSON output. Data will be written to the file {seedname}_data.txt, + except for the atomic displacements which are written to files {seedname}_atomic_displacements_data_{I}.txt, + where {I} are k-point indices. + :type filename: str + + """ + + data = cls._get_reader_data(ab_initio_reader) + # Unpack advanced parameters into a dictionary for ease of comparison later. + # It might be wise to remove this and store escaped strings for consistency, + # but for now we don't want to disturb the old test data so follow its format. + data["attributes"]["advanced_parameters"] = json.loads(data["attributes"]["advanced_parameters"]) + + displacements = data["datasets"]["k_points_data"].pop("atomic_displacements") + for i, eigenvector in displacements.items(): + with open('{seedname}_atomic_displacements_data_{i}.txt'.format(seedname=seedname, i=i), 'wt') as f: + eigenvector.flatten().view(float).tofile(f, sep=' ') + + with open('{seedname}_data.txt'.format(seedname=seedname), 'wt') as f: + json.dump(cls._arrays_to_lists(data), f, indent=4, sort_keys=True) + + @classmethod + def _arrays_to_lists(cls, mydict): + """Recursively convert numpy arrays in a nested dict to lists (i.e. valid JSON) + + Returns a processed *copy* of the input dictionary: in-place values will not be altered.""" + clean_dict = {} + for key, value in mydict.items(): + if isinstance(value, np.ndarray): + clean_dict[key] = value.tolist() + elif isinstance(value, dict): + clean_dict[key] = cls._arrays_to_lists(value) + else: + clean_dict[key] = value + return clean_dict diff --git a/scripts/AbinsModules/LoadCRYSTAL.py b/scripts/AbinsModules/LoadCRYSTAL.py index 2c05bd5ded8ff6a5a03a484ecf14bf418f92ddc7..f82e8668975d8c5afc5947dbba6475b628fd0a5d 100644 --- a/scripts/AbinsModules/LoadCRYSTAL.py +++ b/scripts/AbinsModules/LoadCRYSTAL.py @@ -153,7 +153,7 @@ class LoadCRYSTAL(AbinsModules.GeneralAbInitioProgram): """ coord_lines = [] self._parser.find_first(file_obj=file_obj, - msg="ATOM X(ANGSTROM) Y(ANGSTROM) Z(ANGSTROM)") + regex=r".*\s+ATOM\s+X\(ANGSTROM\)\s+Y\(ANGSTROM\)\s+Z\(ANGSTROM\)\s*$") file_obj.readline() # Line: ******************************************************************************* diff --git a/scripts/test/AbinsLoadCRYSTALTest.py b/scripts/test/AbinsLoadCRYSTALTest.py index 8d794d91e8fb538c024ee43a75f41c313756bb4f..4a4968e658ab4733012b6595124f34fc2e348cb6 100644 --- a/scripts/test/AbinsLoadCRYSTALTest.py +++ b/scripts/test/AbinsLoadCRYSTALTest.py @@ -17,7 +17,7 @@ class AbinsLoadCRYSTALTest(unittest.TestCase, AbinsModules.GeneralLoadAbInitioTe # *************************** USE CASES ******************************************** # =================================================================================== - # | Use cases: Gamma point calculation for CRYSTAL | + # | Use cases: Gamma point calculation for CRYSTAL | # =================================================================================== _gamma_crystal = "crystalB3LYP_LoadCRYSTAL" _set_crystal = "crystal_set_key_LoadCRYSTAL" @@ -28,7 +28,12 @@ class AbinsLoadCRYSTALTest(unittest.TestCase, AbinsModules.GeneralLoadAbInitioTe _molecule = "toluene_molecule_LoadCRYSTAL" # =================================================================================== - # | Use cases: Phonon dispersion calculation for CRYSTAL | + # | Use case: Molecular calculation with CRYSTAL17 | + # =================================================================================== + _molecule17 = "toluene_molecule_LoadCRYSTAL17" + + # =================================================================================== + # | Use cases: Phonon dispersion calculation for CRYSTAL | # =================================================================================== _phonon_dispersion_v1 = "mgo-GX_LoadCRYSTAL" _phonon_dispersion_v2 = "MgO-222-DISP_LoadCRYSTAL" @@ -40,6 +45,9 @@ class AbinsLoadCRYSTALTest(unittest.TestCase, AbinsModules.GeneralLoadAbInitioTe def test_molecule(self): self.check(name=self._molecule, loader=AbinsModules.LoadCRYSTAL) + def test_molecule17(self): + self.check(name=self._molecule17, loader=AbinsModules.LoadCRYSTAL) + def test_phonon_dispersion_crystal(self): self.check(name=self._phonon_dispersion_v1, loader=AbinsModules.LoadCRYSTAL) self.check(name=self._phonon_dispersion_v2, loader=AbinsModules.LoadCRYSTAL)