diff --git a/.ci_support/environment-mini.yml b/.ci_support/environment-mini.yml new file mode 100644 index 00000000..8ba0a3f3 --- /dev/null +++ b/.ci_support/environment-mini.yml @@ -0,0 +1,9 @@ +channels: +- conda-forge +dependencies: +- hatchling +- hatch-vcs +- ase =3.28.0 +- numpy =2.4.3 +- pandas =3.0.2 +- scipy =1.17.1 \ No newline at end of file diff --git a/.ci_support/environment-old.yml b/.ci_support/environment-old.yml index 3657537f..928e4354 100644 --- a/.ci_support/environment-old.yml +++ b/.ci_support/environment-old.yml @@ -8,3 +8,4 @@ dependencies: - numpy =1.26.0 - pandas =2.0.3 - scipy =1.11.1 +- structuretoolkit =0.0.30 \ No newline at end of file diff --git a/.ci_support/environment.yml b/.ci_support/environment.yml index 7df967fb..36c0b962 100644 --- a/.ci_support/environment.yml +++ b/.ci_support/environment.yml @@ -8,3 +8,4 @@ dependencies: - numpy =2.4.3 - pandas =3.0.2 - scipy =1.17.1 +- structuretoolkit =0.0.42 diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index aae62549..232a52ae 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -83,7 +83,7 @@ jobs: python-version: '3.14' miniforge-version: latest condarc-file: .condarc - environment-file: .ci_support/environment.yml + environment-file: .ci_support/environment-mini.yml - name: Test shell: bash -l {0} timeout-minutes: 30 diff --git a/pyproject.toml b/pyproject.toml index 8fd84a13..58e74263 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ Repository = "https://github.com/pyiron/lammpsparser" [project.optional-dependencies] h5md = ["h5py==3.16.0"] +compatibility = ["structuretoolkit==0.0.42"] [tool.hatch.build] include = [ diff --git a/src/lammpsparser/__pycache__/__init__.cpython-312.pyc b/src/lammpsparser/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..f7152686 Binary files /dev/null and b/src/lammpsparser/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/lammpsparser/__pycache__/_version.cpython-312.pyc b/src/lammpsparser/__pycache__/_version.cpython-312.pyc new file mode 100644 index 00000000..be083bb9 Binary files /dev/null and b/src/lammpsparser/__pycache__/_version.cpython-312.pyc differ diff --git a/src/lammpsparser/__pycache__/output.cpython-312.pyc b/src/lammpsparser/__pycache__/output.cpython-312.pyc new file mode 100644 index 00000000..368b380c Binary files /dev/null and b/src/lammpsparser/__pycache__/output.cpython-312.pyc differ diff --git a/src/lammpsparser/__pycache__/output_raw.cpython-312.pyc b/src/lammpsparser/__pycache__/output_raw.cpython-312.pyc new file mode 100644 index 00000000..db3da2f0 Binary files /dev/null and b/src/lammpsparser/__pycache__/output_raw.cpython-312.pyc differ diff --git a/src/lammpsparser/__pycache__/potential.cpython-312.pyc b/src/lammpsparser/__pycache__/potential.cpython-312.pyc new file mode 100644 index 00000000..1cc81f87 Binary files /dev/null and b/src/lammpsparser/__pycache__/potential.cpython-312.pyc differ diff --git a/src/lammpsparser/__pycache__/structure.cpython-312.pyc b/src/lammpsparser/__pycache__/structure.cpython-312.pyc new file mode 100644 index 00000000..49b03e3e Binary files /dev/null and b/src/lammpsparser/__pycache__/structure.cpython-312.pyc differ diff --git a/src/lammpsparser/__pycache__/units.cpython-312.pyc b/src/lammpsparser/__pycache__/units.cpython-312.pyc new file mode 100644 index 00000000..7fa50abe Binary files /dev/null and b/src/lammpsparser/__pycache__/units.cpython-312.pyc differ diff --git a/src/lammpsparser/compatibility/__pycache__/__init__.cpython-312.pyc b/src/lammpsparser/compatibility/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..b56a1d4f Binary files /dev/null and b/src/lammpsparser/compatibility/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/lammpsparser/compatibility/__pycache__/calculate.cpython-312.pyc b/src/lammpsparser/compatibility/__pycache__/calculate.cpython-312.pyc new file mode 100644 index 00000000..3fcd47b1 Binary files /dev/null and b/src/lammpsparser/compatibility/__pycache__/calculate.cpython-312.pyc differ diff --git a/src/lammpsparser/compatibility/__pycache__/constraints.cpython-312.pyc b/src/lammpsparser/compatibility/__pycache__/constraints.cpython-312.pyc new file mode 100644 index 00000000..c4580c2e Binary files /dev/null and b/src/lammpsparser/compatibility/__pycache__/constraints.cpython-312.pyc differ diff --git a/src/lammpsparser/compatibility/__pycache__/file.cpython-312.pyc b/src/lammpsparser/compatibility/__pycache__/file.cpython-312.pyc new file mode 100644 index 00000000..115db7d8 Binary files /dev/null and b/src/lammpsparser/compatibility/__pycache__/file.cpython-312.pyc differ diff --git a/src/lammpsparser/compatibility/__pycache__/structure.cpython-312.pyc b/src/lammpsparser/compatibility/__pycache__/structure.cpython-312.pyc new file mode 100644 index 00000000..e038f9a7 Binary files /dev/null and b/src/lammpsparser/compatibility/__pycache__/structure.cpython-312.pyc differ diff --git a/tests/__pycache__/test_compatibility_calculate.cpython-312.pyc b/tests/__pycache__/test_compatibility_calculate.cpython-312.pyc new file mode 100644 index 00000000..d15a1eb0 Binary files /dev/null and b/tests/__pycache__/test_compatibility_calculate.cpython-312.pyc differ diff --git a/tests/__pycache__/test_compatibility_file.cpython-312.pyc b/tests/__pycache__/test_compatibility_file.cpython-312.pyc new file mode 100644 index 00000000..0e687ee8 Binary files /dev/null and b/tests/__pycache__/test_compatibility_file.cpython-312.pyc differ diff --git a/tests/__pycache__/test_compatibility_structure.cpython-312.pyc b/tests/__pycache__/test_compatibility_structure.cpython-312.pyc new file mode 100644 index 00000000..f17cf9c6 Binary files /dev/null and b/tests/__pycache__/test_compatibility_structure.cpython-312.pyc differ diff --git a/tests/__pycache__/test_constraints.cpython-312.pyc b/tests/__pycache__/test_constraints.cpython-312.pyc new file mode 100644 index 00000000..083595ba Binary files /dev/null and b/tests/__pycache__/test_constraints.cpython-312.pyc differ diff --git a/tests/__pycache__/test_output.cpython-312.pyc b/tests/__pycache__/test_output.cpython-312.pyc new file mode 100644 index 00000000..9a8f3e46 Binary files /dev/null and b/tests/__pycache__/test_output.cpython-312.pyc differ diff --git a/tests/__pycache__/test_output_raw.cpython-312.pyc b/tests/__pycache__/test_output_raw.cpython-312.pyc new file mode 100644 index 00000000..29ed4221 Binary files /dev/null and b/tests/__pycache__/test_output_raw.cpython-312.pyc differ diff --git a/tests/__pycache__/test_potential.cpython-312.pyc b/tests/__pycache__/test_potential.cpython-312.pyc new file mode 100644 index 00000000..38778acb Binary files /dev/null and b/tests/__pycache__/test_potential.cpython-312.pyc differ diff --git a/tests/__pycache__/test_structure.cpython-312.pyc b/tests/__pycache__/test_structure.cpython-312.pyc new file mode 100644 index 00000000..a165eac2 Binary files /dev/null and b/tests/__pycache__/test_structure.cpython-312.pyc differ diff --git a/tests/__pycache__/test_units.cpython-312.pyc b/tests/__pycache__/test_units.cpython-312.pyc new file mode 100644 index 00000000..0c339338 Binary files /dev/null and b/tests/__pycache__/test_units.cpython-312.pyc differ diff --git a/tests/test_compatibility_calculate.py b/tests/test_compatibility_calculate.py new file mode 100644 index 00000000..ae55912f --- /dev/null +++ b/tests/test_compatibility_calculate.py @@ -0,0 +1,292 @@ +import unittest +import warnings +import numpy as np +from ase.build import bulk + +from lammpsparser.compatibility.calculate import ( + calc_md, + calc_minimize, + _set_initial_velocity, + _pressure_to_lammps, + _get_rotation_matrix, + _modify_structure_to_allow_requested_deformation, + _is_isotropic_hydrostatic, +) + + +class TestCalcMd(unittest.TestCase): + def test_delta_temp_deprecated(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = calc_md(temperature=500.0, delta_temp=0.1) + self.assertTrue(any("delta_temp" in str(warning.message) for warning in w)) + self.assertIsInstance(result, list) + + def test_delta_press_deprecated(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = calc_md(temperature=500.0, pressure=0.0, delta_press=1.0) + self.assertTrue(any("delta_press" in str(warning.message) for warning in w)) + self.assertIsInstance(result, list) + + def test_too_many_temperatures_raises(self): + with self.assertRaises(ValueError): + calc_md(temperature=[100.0, 200.0, 300.0]) + + def test_npt_temperature_zero_raises(self): + with self.assertRaises(ValueError): + calc_md(temperature=0.0, pressure=0.0) + + def test_npt_list_pressure(self): + result = calc_md( + temperature=500.0, + pressure=[0.0, 0.0, 0.0, None, None, None], + ) + self.assertIsInstance(result, list) + self.assertTrue(any("npt" in line for line in result)) + + def test_npt_list_pressure_with_shear(self): + result = calc_md( + temperature=500.0, + pressure=[0.0, 0.0, 0.0, 1.0, 0.0, 0.0], + ) + self.assertIsInstance(result, list) + + def test_nvt_temperature_zero_raises(self): + with self.assertRaises(ValueError): + calc_md(temperature=0.0) + + def test_tloop(self): + result = calc_md(temperature=500.0, tloop=5) + self.assertTrue(any("tloop 5" in line for line in result)) + + def test_nve_with_langevin_warns(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = calc_md(langevin=True) + self.assertTrue(any("Langevin" in str(warning.message) for warning in w)) + self.assertIsInstance(result, list) + + def test_npt_langevin(self): + result = calc_md( + temperature=500.0, + pressure=0.0, + langevin=True, + ) + self.assertIsInstance(result, list) + self.assertTrue(any("nph" in line for line in result)) + self.assertTrue(any("langevin" in line for line in result)) + + def test_nvt_langevin(self): + result = calc_md( + temperature=500.0, + langevin=True, + ) + self.assertIsInstance(result, list) + self.assertTrue(any("langevin" in line for line in result)) + + def test_invalid_units(self): + with self.assertRaises(NotImplementedError): + calc_md(temperature=500.0, units="invalid_units") + + def test_invalid_seed(self): + with self.assertRaises(ValueError): + calc_md(temperature=500.0, seed=-1) + + def test_two_temperatures(self): + result = calc_md(temperature=[300.0, 600.0]) + self.assertIsInstance(result, list) + + +class TestCalcMinimize(unittest.TestCase): + def test_minimize_no_structure_with_pressure_raises(self): + with self.assertRaises(ValueError): + calc_minimize(structure=None, pressure=0.0) + + def test_minimize_with_structure_pressure(self): + structure = bulk("Al", a=4.0, cubic=True) + result, _ = calc_minimize(structure=structure, pressure=0.0) + self.assertIsInstance(result, list) + + def test_minimize_with_list_pressure(self): + structure = bulk("Al", a=4.0, cubic=True) + result, _ = calc_minimize( + structure=structure, pressure=[0.0, 0.0, 0.0, None, None, None] + ) + self.assertIsInstance(result, list) + + +class TestSetInitialVelocity(unittest.TestCase): + def test_basic(self): + result = _set_initial_velocity(temperature=300.0) + self.assertIn("velocity all create", result) + self.assertNotIn("dist gaussian", result) + self.assertNotIn("sum yes", result) + self.assertNotIn("mom no", result) + self.assertNotIn("rot no", result) + + def test_gaussian(self): + result = _set_initial_velocity(temperature=300.0, gaussian=True) + self.assertIn("dist gaussian", result) + + def test_append_value(self): + result = _set_initial_velocity(temperature=300.0, append_value=True) + self.assertIn("sum yes", result) + + def test_no_lin_momentum(self): + result = _set_initial_velocity(temperature=300.0, zero_lin_momentum=False) + self.assertIn("mom no", result) + + def test_no_rot_momentum(self): + result = _set_initial_velocity(temperature=300.0, zero_rot_momentum=False) + self.assertIn("rot no", result) + + def test_all_options(self): + result = _set_initial_velocity( + temperature=300.0, + append_value=True, + zero_lin_momentum=False, + zero_rot_momentum=False, + ) + self.assertIn("sum yes", result) + self.assertIn("mom no", result) + self.assertIn("rot no", result) + + +class TestPressureToLammps(unittest.TestCase): + def test_scalar(self): + result = _pressure_to_lammps(pressure=1.0, rotation_matrix=None) + self.assertIsInstance(result, float) + + def test_too_long_raises(self): + with self.assertRaises(ValueError): + _pressure_to_lammps( + pressure=[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0], + rotation_matrix=None, + ) + + def test_all_none_raises(self): + with self.assertRaises(ValueError): + _pressure_to_lammps( + pressure=[None, None], + rotation_matrix=None, + ) + + def test_ortho_rotation_no_rotation_applied(self): + result = _pressure_to_lammps( + pressure=[1.0, 1.0, 1.0, 0.0, 0.0, 0.0], + rotation_matrix=np.eye(3), + ) + self.assertIsInstance(result, list) + self.assertEqual(len(result), 6) + + def test_non_ortho_rotation_with_none_raises(self): + rotation_matrix = np.array([[0.0, 1.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]) + with self.assertRaises(ValueError): + _pressure_to_lammps( + pressure=[1.0, 2.0, 3.0, None, None, None], + rotation_matrix=rotation_matrix, + ) + + def test_non_ortho_rotation_applied(self): + rotation_matrix = np.array([[0.0, 1.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]) + result = _pressure_to_lammps( + pressure=[1.0, 2.0, 3.0, 0.0, 0.0, 0.0], + rotation_matrix=rotation_matrix, + ) + self.assertIsInstance(result, list) + self.assertEqual(len(result), 6) + + def test_list_with_none(self): + result = _pressure_to_lammps( + pressure=[1.0, 1.0, 1.0, None, None, None], + rotation_matrix=None, + ) + self.assertIsInstance(result, list) + self.assertEqual(len(result), 6) + + +class TestIsIsotropicHydrostatic(unittest.TestCase): + def test_isotropic_shear_none(self): + self.assertTrue(_is_isotropic_hydrostatic([1.0, 1.0, 1.0, None, None, None])) + + def test_isotropic_shear_zero(self): + self.assertTrue(_is_isotropic_hydrostatic([1.0, 1.0, 1.0, 0.0, 0.0, 0.0])) + + def test_non_isotropic(self): + self.assertFalse(_is_isotropic_hydrostatic([1.0, 2.0, 3.0, None, None, None])) + + def test_non_isotropic_shear(self): + self.assertFalse(_is_isotropic_hydrostatic([1.0, 1.0, 1.0, 1.0, 0.0, 0.0])) + + +class TestGetRotationMatrix(unittest.TestCase): + def test_no_structure_warns(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + rotation_matrix, structure = _get_rotation_matrix( + structure=None, pressure=[1.0, 1.0, 1.0] + ) + self.assertIsNone(rotation_matrix) + self.assertIsNone(structure) + self.assertTrue( + any("No structure set" in str(warning.message) for warning in w) + ) + + def test_with_structure(self): + structure = bulk("Al", a=4.0, cubic=True) + rotation_matrix, _ = _get_rotation_matrix(structure=structure, pressure=0.0) + self.assertIsNotNone(rotation_matrix) + + +class TestModifyStructure(unittest.TestCase): + def test_scalar_pressure_unchanged(self): + structure = bulk("Al", a=4.0, cubic=True) + result = _modify_structure_to_allow_requested_deformation( + structure=structure, pressure=0.0 + ) + self.assertIs(result, structure) + + def test_diagonal_pressure_unchanged(self): + structure = bulk("Al", a=4.0, cubic=True) + result = _modify_structure_to_allow_requested_deformation( + structure=structure, pressure=[0.0, 0.0, 0.0, None, None, None] + ) + self.assertIs(result, structure) + + def test_non_diagonal_pressure_skews_cell(self): + structure = bulk("Al", a=4.0, cubic=True) + result = _modify_structure_to_allow_requested_deformation( + structure=structure, pressure=[0.0, 0.0, 0.0, 1.0, 0.0, 0.0] + ) + self.assertIsNot(result, structure) + + def test_non_diagonal_pressure_already_skewed(self): + structure = bulk("Al") + cell = structure.cell.array.copy() + cell[0, 1] = cell[0, 0] * 0.6 + structure.set_cell(cell, scale_atoms=False) + result = _modify_structure_to_allow_requested_deformation( + structure=structure, pressure=[0.0, 0.0, 0.0, 1.0, 0.0, 0.0] + ) + self.assertIsNotNone(result) + + def test_non_diagonal_prism_attr_error_warns(self): + from unittest.mock import MagicMock + + structure = bulk("Al", a=4.0, cubic=True) + mock_prism = MagicMock() + mock_prism.is_skewed.side_effect = AttributeError("no is_skewed") + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = _modify_structure_to_allow_requested_deformation( + structure=structure, + pressure=[0.0, 0.0, 0.0, 1.0, 0.0, 0.0], + prism=mock_prism, + ) + self.assertIs(result, structure) + self.assertTrue(any("constraining" in str(warning.message) for warning in w)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_compatibility_structure.py b/tests/test_compatibility_structure.py new file mode 100644 index 00000000..ba3007e8 --- /dev/null +++ b/tests/test_compatibility_structure.py @@ -0,0 +1,86 @@ +import unittest +import numpy as np +from unittest.mock import MagicMock +from ase.build import bulk +from ase.atoms import Atoms + +try: + from lammpsparser.compatibility.structure import ( + LammpsStructureCompatibility, + get_bonds, + ) + + skip_structuretoolkit_test = False +except ImportError: + skip_structuretoolkit_test = True + + +@unittest.skipIf(skip_structuretoolkit_test, "structuretoolkit not available") +class TestLammpsStructureCompatibilityInit(unittest.TestCase): + def test_init_defaults(self): + lsc = LammpsStructureCompatibility() + self.assertIsNone(lsc.structure) + self.assertEqual(lsc._molecule_ids, []) + + def test_init_with_params(self): + lsc = LammpsStructureCompatibility( + bond_dict={ + "Al": { + "element_list": ["Al"], + "cutoff_list": [3.0], + "max_bond_list": [2], + "bond_type_list": [1], + "angle_type_list": [None], + } + }, + units="metal", + atom_type="full", + ) + self.assertIsNone(lsc.structure) + + def test_structure_getter(self): + lsc = LammpsStructureCompatibility() + self.assertIsNone(lsc.structure) + structure = bulk("Al", a=4.0, cubic=True) + lsc._structure = structure + self.assertIs(lsc.structure, structure) + + +@unittest.skipIf(skip_structuretoolkit_test, "structuretoolkit not available") +class TestLammpsStructureCompatibilitySetterAtomic(unittest.TestCase): + def test_structure_setter_atomic(self): + lsc = LammpsStructureCompatibility(atom_type="atomic") + lsc._el_eam_lst = ["Al"] + structure = bulk("Al", a=4.0, cubic=True) + # atom_type attribute defaults to None, so else branch (atomic) is used + lsc.structure = structure + self.assertIs(lsc._structure, structure) + self.assertIn("Atoms", lsc._string_input) + + def test_structure_setter_charge(self): + lsc = LammpsStructureCompatibility(atom_type="charge") + lsc._el_eam_lst = ["Fe"] + lsc.atom_type = "charge" + structure = Atoms("Fe1", positions=np.zeros((1, 3)), cell=np.eye(3)) + structure.set_initial_charges(np.ones(1) * 1.5) + lsc.structure = structure + self.assertIs(lsc._structure, structure) + self.assertIn("Atoms", lsc._string_input) + self.assertIn("1.500000", lsc._string_input) + + +@unittest.skipIf(skip_structuretoolkit_test, "structuretoolkit not available") +class TestGetBonds(unittest.TestCase): + def test_get_bonds_error(self): + structure = bulk("Al", a=4.05, cubic=True).repeat([2, 2, 1]) + # get_bonds may fail due to incompatible structuretoolkit version; + # either result or TypeError is acceptable + try: + result = get_bonds(structure=structure, max_shells=1) + self.assertIsNotNone(result) + except TypeError: + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_output.py b/tests/test_output.py index 6f1ee7ee..037ce298 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -12,6 +12,14 @@ from lammpsparser.structure import UnfoldingPrism +try: + import h5py + + skip_h5py_test = False +except ImportError: + skip_h5py_test = True + + class TestLammpsOutput(unittest.TestCase): def setUp(self): self.static_folder = os.path.abspath(os.path.join(__file__, "..", "static")) @@ -332,6 +340,7 @@ def test_full_job_output(self): ) ) + @unittest.skipIf(skip_h5py_test, "h5py not available") def test_full_job_output_h5(self): test_folder = os.path.join(self.static_folder, "full_job_h5") structure_ni = bulk("Ni", cubic=True) @@ -580,6 +589,81 @@ def test_mean_values_non_ortho(self): self.assertTrue("mean_bar" in generic_keys_lst) self.assertTrue("mean_pressures" in pressure_dict.keys()) + def test_mean_values_ortho_prism(self): + # Use ortho prism to cover lines 311-312 (mean_pressures rotation with ortho cell) + structure = bulk("Al", a=4.0, cubic=True) + ortho_prism = UnfoldingPrism(cell=structure.cell) + generic_keys_lst, pressure_dict, df = _collect_output_log( + file_name=os.path.join(self.static_folder, "mean_values", "log.lammps"), + prism=ortho_prism, + ) + self.assertTrue("mean_pressures" in pressure_dict.keys()) + + def test_parse_output_with_computes(self): + # Cover line 95: computes in dump (mean_dump/dump.out has c_test column) + import warnings + + structure = bulk("Al", a=3.52) + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + output = parse_lammps_output_files( + working_directory=os.path.join(self.static_folder, "mean_dump"), + structure=structure, + potential_elements=["Al"], + units="metal", + ) + self.assertIn("test", output["generic"]) + + def test_parse_dump_h5md_non_ortho_raises(self): + # Cover line 135: h5md file with non-ortho prism raises RuntimeError + cell = np.array([[1.0, 1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]) + non_ortho_prism = UnfoldingPrism(cell=cell) + h5_file = os.path.join(self.static_folder, "full_job_h5", "dump.h5") + with self.assertRaises(RuntimeError): + _parse_dump( + dump_h5_full_file_name=h5_file, + dump_out_full_file_name="", + prism=non_ortho_prism, + structure=bulk("Ni", cubic=True), + potential_elements=["Ni"], + ) + + def test_parse_output_extra_log_column(self): + # Cover line 114: column in log that is not in generic_keys_lst goes to hdf_lammps + import tempfile + import warnings + + structure = bulk("Al", a=4.0, cubic=True) + log_content = ( + "LAMMPS log\n" + " Step Temp KinEng PotEng TotEng" + " Pxx Pxy Pxz Pyy Pyz" + " Pzz Volume\n" + " 0 0.0 0.0 -17.8 -17.8 0.0 0.0 0.0 0.0 0.0 0.0 43.6\n" + "Loop time of 0.0 on 1 procs for 0 steps with 4 atoms\n" + ) + with tempfile.TemporaryDirectory() as tmpdir: + # Write log file with extra "KinEng" column + log_path = os.path.join(tmpdir, "log.lammps") + with open(log_path, "w") as f: + f.write(log_content) + # Copy dump.out from full_job into tmpdir + import shutil + + dump_src = os.path.join(self.static_folder, "full_job", "dump.out") + shutil.copy(dump_src, os.path.join(tmpdir, "dump.out")) + + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + output = parse_lammps_output_files( + working_directory=tmpdir, + structure=structure, + potential_elements=["Al"], + units="metal", + ) + # "KinEng" column is not a generic key, so it goes to hdf_lammps + self.assertIn("KinEng", output["lammps"]) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_structure.py b/tests/test_structure.py index 5376e760..85d745da 100644 --- a/tests/test_structure.py +++ b/tests/test_structure.py @@ -227,6 +227,26 @@ def test_skewed_cell(self): up = UnfoldingPrism(cell=cell) self.assertFalse(np.all(np.isclose(up.A, cell))) + def test_unfolding_prism_bool_pbc(self): + structure = bulk("Al", a=4.05) + up = UnfoldingPrism(cell=structure.cell, pbc=True) + self.assertIsNotNone(up) + up_false = UnfoldingPrism(cell=structure.cell, pbc=False) + self.assertIsNotNone(up_false) + + def test_unfolding_prism_folding_edge_cases(self): + # Create a cell with a large xy component to trigger folding branches + a = 3.52 + cell = np.array( + [ + [a, 0.0, 0.0], + [a * 0.6, a, 0.0], + [0.0, a * 0.6, a], + ] + ) + up = UnfoldingPrism(cell=cell, pbc=(True, True, True)) + self.assertIsNotNone(up) + def test_structure_charge(self): atoms = Atoms("Fe1", positions=np.zeros((1, 3)), cell=np.eye(3)) atoms.set_initial_charges(charges=np.ones(len(atoms)) * 2.0)