From eae7624fee3aea47582a69997930da2cd683f934 Mon Sep 17 00:00:00 2001 From: bpuchala Date: Wed, 6 Dec 2023 13:50:03 -0500 Subject: [PATCH 01/11] Add lattice methods: reciprocal, volume, lengths_and_angles, from_lengths_and_angles; fix Prim treatment of site labels --- CHANGELOG.md | 4 + include/casm/crystallography/Lattice.hh | 8 +- python/src/xtal.cpp | 89 ++++++++++++++++--- python/tests/test_lattice.py | 55 ++++++++++++ python/tests/test_prim.py | 50 +++++++++++ src/casm/crystallography/Lattice.cc | 56 +++++++++--- src/casm/crystallography/Site.cc | 5 +- .../crystallography/io/BasicStructureIO.cc | 4 + 8 files changed, 240 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 399cc4f..80d6f8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fix CASM::xtal::make_primitive, which was not copying unique_names. This also fixes the output of libcasm.xtal.make_primitive which was losing the occ_dof list as a result. +- Fix JSON output of xtal::BasicStructure site label ### Changed @@ -24,6 +25,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add options to the BCC and FCC structure factory functions in libcasm.xtal.structures to make the conventional cubic unit cells. - Add to libcasm.xtal: StructureAtomInfo namedtuple, and methods sort_structure_by_atom_info, sort_structure_by_atom_type, sort_structure_by_atom_coordinate_frac, and sort_structure_by_atom_coordinate_cart - Add to libcasm.xtal: substitute_structure_species +- Add to libcasm.xtal.Prim: method labels, constructor parameter `labels` +- Add to libcasm.xtal.Lattice: methods reciprocal, volume, lengths_and_angles, from_lengths_and_angles + ## [2.0a8] - 2023-11-15 diff --git a/include/casm/crystallography/Lattice.hh b/include/casm/crystallography/Lattice.hh index 7633768..d73f78d 100644 --- a/include/casm/crystallography/Lattice.hh +++ b/include/casm/crystallography/Lattice.hh @@ -33,15 +33,17 @@ class Lattice : public Comparisons> { Lattice(Eigen::Ref const &vec1, Eigen::Ref const &vec2, - Eigen::Ref const &vec3, double xtal_tol = TOL, - bool force = false); + Eigen::Ref const &vec3, double xtal_tol = TOL); /// Construct Lattice from a matrix of lattice vectors, where lattice vectors /// are columns ///(e.g., lat_mat is equivalent to coord_trans[FRAC]) Lattice(Eigen::Ref const &lat_mat = Eigen::Matrix3d::Identity(), - double xtal_tol = TOL, bool force = false); + double xtal_tol = TOL); + + static Lattice from_lengths_and_angles(std::vector lengths_and_angles, + double xtal_tol = TOL); /// \brief Construct FCC primitive cell of unit volume static Lattice fcc(double tol = TOL); diff --git a/python/src/xtal.cpp b/python/src/xtal.cpp index b902099..f083427 100644 --- a/python/src/xtal.cpp +++ b/python/src/xtal.cpp @@ -296,7 +296,8 @@ std::shared_ptr make_prim( std::vector const &global_dof = std::vector{}, std::map const &molecules = std::map{}, - std::string title = std::string("prim")) { + std::string title = std::string("prim"), + std::optional> labels = std::nullopt) { // validation if (coordinate_frac.rows() != 3) { throw std::runtime_error("Error in make_prim: coordinate_frac.rows() != 3"); @@ -311,6 +312,21 @@ std::shared_ptr make_prim( "Error in make_prim: local_dof.size() && " "coordinate_frac.cols() != occ_dof.size()"); } + if (labels.has_value() && labels.value().size() != coordinate_frac.cols()) { + throw std::runtime_error( + "Error in make_prim: labels.has_value() && " + "labels.value().size() != coordinate_frac.cols()"); + } + if (labels.has_value()) { + for (Index i = 0; i < labels.value().size(); ++i) { + if (labels.value()[i] < 0) { + std::stringstream msg; + msg << "Error in make_prim: labels.value()[" << i + << "] < 0 (=" << labels.value()[i] << ")"; + throw std::runtime_error(msg.str()); + } + } + } // construct prim auto shared_prim = std::make_shared(lattice); @@ -343,6 +359,10 @@ std::shared_ptr make_prim( } xtal::Site site{coord, site_occ, site_dofsets}; + + if (labels.has_value()) { + site.set_label(labels.value()[b]); + } prim.push_back(site, FRAC); } prim.set_unique_names(occ_dof); @@ -566,6 +586,15 @@ std::map get_prim_molecules( return molecules; } +std::vector get_prim_labels( + std::shared_ptr const &prim) { + std::vector labels; + for (auto const &site : prim->basis()) { + labels.push_back(site.label()); + } + return labels; +} + std::shared_ptr make_within( std::shared_ptr const &init_prim) { auto prim = std::make_shared(*init_prim); @@ -1101,9 +1130,8 @@ PYBIND11_MODULE(_xtal, m) { .. _`Lattice Canonical Form`: https://prisms-center.github.io/CASMcode_docs/formats/lattice_canonical_form/ )pbdoc") - .def(py::init(), - "Construct a Lattice", py::arg("column_vector_matrix"), - py::arg("tol") = TOL, py::arg("force") = false, R"pbdoc( + .def(py::init(), "Construct a Lattice", + py::arg("column_vector_matrix"), py::arg("tol") = TOL, R"pbdoc( .. rubric:: Constructor @@ -1120,6 +1148,34 @@ PYBIND11_MODULE(_xtal, m) { "Returns the tolerance used for crystallographic comparisons.") .def("set_tol", &xtal::Lattice::set_tol, py::arg("tol"), "Set the tolerance used for crystallographic comparisons.") + .def("reciprocal", &xtal::Lattice::reciprocal, + "Return the reciprocal lattice") + .def( + "lengths_and_angles", + [](xtal::Lattice const &self) -> std::vector { + std::vector v; + v.push_back(self.length(0)); + v.push_back(self.length(1)); + v.push_back(self.length(2)); + v.push_back(self.angle(0)); + v.push_back(self.angle(1)); + v.push_back(self.angle(2)); + return v; + }, + R"pbdoc( + Return the lattice vector lengths and angles, + :math:`[a, b, c, \alpha, \beta, \gamma]`. + )pbdoc") + .def("volume", &xtal::Lattice::volume, R"pbdoc( + Return the signed volume of the unit cell. + )pbdoc") + .def_static("from_lengths_and_angles", + &xtal::Lattice::from_lengths_and_angles, + py::arg("lengths_and_angles"), py::arg("tol") = TOL, + R"pbdoc( + Construct a Lattice from the lattice vector lengths and angles, + :math:`[a, b, c, \alpha, \beta, \gamma]` + )pbdoc") .def(py::self < py::self, "Sorts lattices by how canonical the lattice vectors are") .def(py::self <= py::self, @@ -1745,6 +1801,7 @@ PYBIND11_MODULE(_xtal, m) { py::arg("global_dof") = std::vector{}, py::arg("occupants") = std::map{}, py::arg("title") = std::string("prim"), + py::arg("labels") = std::nullopt, R"pbdoc( .. _prim-init: @@ -1772,18 +1829,20 @@ PYBIND11_MODULE(_xtal, m) { global_dof : list[:class:`DoFSetBasis`], default=[] Global continuous DoF allowed for the entire crystal. occupants : dict[str,:class:`Occupant`], default=[] - :class:`Occupant` allowed in the crystal. The keys are labels - ('orientation names') used in the occ_dof parameter. This may - include isotropic atoms, vacancies, atoms with fixed anisotropic - properties, and molecular occupants. A seperate key and value is - required for all species with distinct anisotropic properties - (i.e. "H2_xy", "H2_xz", and "H2_yz" for distinct orientations, - or "A.up", and "A.down" for distinct collinear magnetic spins, - etc.). + :class:`Occupant` allowed in the crystal. The keys are names + used in the occ_dof parameter. This may include isotropic atoms, + vacancies, atoms with fixed anisotropic properties, and molecular + occupants. A seperate key and value is required for all species + with distinct anisotropic properties (i.e. "H2_xy", "H2_xz", and + "H2_yz" for distinct orientations, or "A.up", and "A.down" for + distinct collinear magnetic spins, etc.). title : str, default="prim" A title for the prim. When the prim is used to construct a cluster expansion, this must consist of alphanumeric characters and underscores only. The first character may not be a number. + labels : Optional[list[int]] = None + If provided, an integer for each basis site, greater than or equal to zero, + that distinguishes otherwise identical sites. )pbdoc") .def("lattice", &get_prim_lattice, "Returns the lattice, as a copy.") .def("coordinate_frac", &get_prim_coordinate_frac, @@ -1793,8 +1852,7 @@ PYBIND11_MODULE(_xtal, m) { "Returns the basis site positions, as columns of a 2d array, in " "Cartesian coordinates") .def("occ_dof", &get_prim_occ_dof, - "Returns the labels (orientation names) of occupants allowed on " - "each basis site") + "Returns the names of occupants allowed on each basis site") .def("local_dof", &get_prim_local_dof, "Returns the continuous DoF allowed on each basis site") .def( @@ -1802,6 +1860,9 @@ PYBIND11_MODULE(_xtal, m) { "Returns the continuous DoF allowed for the entire crystal structure") .def("occupants", &get_prim_molecules, "Returns the :class:`Occupant` allowed in the crystal.") + .def("labels", &get_prim_labels, + "Returns the integer label associated with each basis site. If no " + "labels were provided, it will be a list of -1.") .def_static( "from_dict", [](const nlohmann::json &data, double xtal_tol) { diff --git a/python/tests/test_lattice.py b/python/tests/test_lattice.py index 01048e8..55edd6c 100644 --- a/python/tests/test_lattice.py +++ b/python/tests/test_lattice.py @@ -187,3 +187,58 @@ def test_is_equivalent_superlattice_of(): ) assert is_equivalent_superlattice_of is True assert np.allclose(S2, point_group[p].matrix() @ S1 @ T) + + +def test_construct_from_lattice_parameters(): + from math import degrees + + a = 1.0 + b = 1.3 + c = 1.6 + alpha = 90 + beta = 120 + gamma = 130 + lengths_and_angles = [a, b, c, alpha, beta, gamma] + lat = xtal.Lattice.from_lengths_and_angles(lengths_and_angles) + assert isinstance(lat, xtal.Lattice) + + L = lat.column_vector_matrix() + + assert math.isclose(np.linalg.norm(L[:, 0]), a) + assert math.isclose(np.linalg.norm(L[:, 1]), b) + assert math.isclose(np.linalg.norm(L[:, 2]), c) + assert math.isclose(degrees(np.arccos(np.dot(L[:, 0], L[:, 1]) / (a * b))), gamma) + assert math.isclose(degrees(np.arccos(np.dot(L[:, 0], L[:, 2]) / (a * c))), beta) + assert math.isclose(degrees(np.arccos(np.dot(L[:, 1], L[:, 2]) / (b * c))), alpha) + + assert np.allclose( + np.array(lengths_and_angles), + np.array(lat.lengths_and_angles()), + ) + + +def test_repiprocal(): + x = 1.0 / math.sqrt(2.0) + L = np.array( + [ + [0.0, x, x], + [x, 0.0, x], + [x, x, 0.0], + ] + ).transpose() + fcc_lattice = xtal.Lattice(L) + + vol = np.dot(L[:, 0], np.cross(L[:, 1], L[:, 2])) + b1 = np.cross(L[:, 1], L[:, 2]) * (2 * math.pi) / vol + b2 = np.cross(L[:, 2], L[:, 0]) * (2 * math.pi) / vol + b3 = np.cross(L[:, 0], L[:, 1]) * (2 * math.pi) / vol + + assert np.isclose(fcc_lattice.volume(), vol) + + reciprocal_lattice = fcc_lattice.reciprocal() + L_recip = reciprocal_lattice.column_vector_matrix() + + assert np.isclose(reciprocal_lattice.volume(), ((2 * math.pi) ** 3) / vol) + assert np.allclose(L_recip[:, 0], b1) + assert np.allclose(L_recip[:, 1], b2) + assert np.allclose(L_recip[:, 2], b3) diff --git a/python/tests/test_prim.py b/python/tests/test_prim.py index b32b66c..1254138 100644 --- a/python/tests/test_prim.py +++ b/python/tests/test_prim.py @@ -56,6 +56,7 @@ def check_lial(prim): ) assert np.allclose(frac_coords, prim.coordinate_frac(), 1e-4, 1e-4) is True assert prim.occ_dof() == [["Li"], ["Li"], ["Al"], ["Al"]] + assert prim.labels() == [-1] * 4 def check_lial_with_occ_dofs(prim_with_occ_dofs, occ_dofs): @@ -71,6 +72,7 @@ def check_lial_with_occ_dofs(prim_with_occ_dofs, occ_dofs): is True ) assert prim_with_occ_dofs.occ_dof() == occ_dofs + assert prim_with_occ_dofs.labels() == [-1] * 4 def test_prim_from_poscar(shared_datadir): @@ -349,3 +351,51 @@ def test_from_json(): prim_global_dof = prim.global_dof() assert len(prim_global_dof) == 1 assert prim_global_dof[0].dofname() == "Hstrain" + + +def test_prim_with_labels(): + lattice = xtal.Lattice(np.eye(3)) + coordinate_frac = np.array( + [ + [0.0, 0.0, 0.0], + [0.0, 0.5, 0.5], + [0.5, 0.0, 0.5], + [0.5, 0.5, 0.0], + ] + ).transpose() + occ_dof = [ + ["A", "B"], + ["B", "A"], + ["A", "B"], + ["A", "B"], + ] + + ## No basis site labels + prim = xtal.Prim( + lattice=lattice, + coordinate_frac=coordinate_frac, + occ_dof=occ_dof, + ) + factor_group = xtal.make_factor_group(prim) + assert len(factor_group) == 48 * 4 + assert prim.labels() == [-1] * 4 + data = prim.to_dict() + assert len(data["basis"]) == 4 + for site in data["basis"]: + assert "label" not in site + + ## Add basis site labels + prim = xtal.Prim( + lattice=lattice, + coordinate_frac=coordinate_frac, + occ_dof=occ_dof, + labels=[0, 1, 1, 1], + ) + factor_group = xtal.make_factor_group(prim) + assert len(factor_group) == 48 + assert prim.labels() == [0, 1, 1, 1] + + data = prim.to_dict() + assert len(data["basis"]) == 4 + for site in data["basis"]: + assert "label" in site diff --git a/src/casm/crystallography/Lattice.cc b/src/casm/crystallography/Lattice.cc index a680f0b..fb93a7b 100644 --- a/src/casm/crystallography/Lattice.cc +++ b/src/casm/crystallography/Lattice.cc @@ -11,15 +11,9 @@ namespace xtal { Lattice::Lattice(Eigen::Ref const &vec1, Eigen::Ref const &vec2, - Eigen::Ref const &vec3, double xtal_tol, - bool force) + Eigen::Ref const &vec3, double xtal_tol) : m_tol(xtal_tol) { m_lat_mat << vec1, vec2, vec3; - if (!force && m_lat_mat.determinant() < 0) { - // this->make_right_handed(); - // throw std::runtime_error("Attempted to construct a left-handed lattice. - // Try again or override if you know what you're doing"); - } m_inv_lat_mat = m_lat_mat.inverse(); } @@ -74,13 +68,49 @@ std::vector const &Lattice::skew_transforms() { /// are columns ///(e.g., lat_mat is equivalent to lat_column_mat()) Lattice::Lattice(const Eigen::Ref &lat_mat, - double xtal_tol, bool force) - : m_lat_mat(lat_mat), m_inv_lat_mat(lat_mat.inverse()), m_tol(xtal_tol) { - if (!force && m_lat_mat.determinant() < 0) { - // this->make_right_handed(); - // throw std::runtime_error("Attempted to construct a left-handed lattice. - // Try again or override if you know what you're doing"); + double xtal_tol) + : m_lat_mat(lat_mat), m_inv_lat_mat(lat_mat.inverse()), m_tol(xtal_tol) {} + +/// \brief Construct a Lattice from lengths and angles +/// +/// \param lengths_and_angles The lattice vector lengths and angles, in order +/// [a, b, c, \alpha, \beta, \gamma] +/// \param xtal_tol The tolerance used for comparisons +/// +Lattice Lattice::from_lengths_and_angles(std::vector lengths_and_angles, + double xtal_tol) { + if (lengths_and_angles.size() != 6) { + throw std::runtime_error( + "Error in Lattice::from_lengths_and_angles: lengths_and_angles.size() " + "!= 6"); } + + double a = lengths_and_angles[0]; + double b = lengths_and_angles[1]; + double c = lengths_and_angles[2]; + double alpha = lengths_and_angles[3]; + double beta = lengths_and_angles[4]; + double gamma = lengths_and_angles[5]; + + double _alpha = alpha * (M_PI / 180); + double _beta = beta * (M_PI / 180); + double _gamma = gamma * (M_PI / 180); + + double b_x = b * std::cos(_gamma); + double b_y = b * std::sin(_gamma); + double c_x = c * std::cos(_beta); + + // b_x *c_x + b_y *c_y = b * c * cos(_alpha) + double c_y = (b * c * std::cos(_alpha) - b_x * c_x) / b_y; + + // c_y ** 2 + c_z ** 2 = pow(c * sin(_beta), 2.) + double c_z = std::sqrt(std::pow(c * std::sin(_beta), 2.0) - c_y * c_y); + + Eigen::MatrixXd column_vector_matrix(3, 3); + column_vector_matrix.col(0) << a, 0.0, 0.0; + column_vector_matrix.col(1) << b_x, b_y, 0.0; + column_vector_matrix.col(2) << c_x, c_y, c_z; + return Lattice(column_vector_matrix, xtal_tol); } Lattice Lattice::fcc(double tol) { diff --git a/src/casm/crystallography/Site.cc b/src/casm/crystallography/Site.cc index 6724800..a9796b1 100644 --- a/src/casm/crystallography/Site.cc +++ b/src/casm/crystallography/Site.cc @@ -483,7 +483,10 @@ xtal::Site copy_apply(const xtal::SymOp &op, xtal::Site site) { sym::copy_apply(op, name_dof_pr.second)); } - return xtal::Site(transformed_coord, transformed_occupants, transformed_dof); + xtal::Site new_site(transformed_coord, transformed_occupants, + transformed_dof); + new_site.set_label(site.label()); + return new_site; } } // namespace sym } // namespace CASM diff --git a/src/casm/crystallography/io/BasicStructureIO.cc b/src/casm/crystallography/io/BasicStructureIO.cc index 2c37eb9..d41c6e2 100644 --- a/src/casm/crystallography/io/BasicStructureIO.cc +++ b/src/casm/crystallography/io/BasicStructureIO.cc @@ -536,6 +536,10 @@ void write_prim(const xtal::BasicStructure &prim, jsonParser &json, json["species"][mol_names[i][j]], c2f_mat); } } + + if (valid_index(prim.basis()[i].label())) { + sjson["label"] = prim.basis()[i].label(); + } } } From 064685b4e3d88c8dfc54ec7f4485e425b0688161 Mon Sep 17 00:00:00 2001 From: bpuchala Date: Wed, 6 Dec 2023 13:51:14 -0500 Subject: [PATCH 02/11] add libcasm.xtal.convert with pymatgen conversions --- python/libcasm/xtal/convert/__init__.py | 45 ++ python/libcasm/xtal/convert/pymatgen.py | 896 ++++++++++++++++++++++++ python/setup.py | 2 +- python/tests/convert/test_pymatgen.py | 503 +++++++++++++ setup.py | 2 +- 5 files changed, 1446 insertions(+), 2 deletions(-) create mode 100644 python/libcasm/xtal/convert/__init__.py create mode 100644 python/libcasm/xtal/convert/pymatgen.py create mode 100644 python/tests/convert/test_pymatgen.py diff --git a/python/libcasm/xtal/convert/__init__.py b/python/libcasm/xtal/convert/__init__.py new file mode 100644 index 0000000..7b142da --- /dev/null +++ b/python/libcasm/xtal/convert/__init__.py @@ -0,0 +1,45 @@ +"""Data structure conversions + +Users of libcasm.xtal.convert should take care to double-check +that conversions are performed correctly! + +This package is a work in progress. Particularly for more +complicated structures or molecules, such as those with mixed +occupation, charge, spin, and magnetic moments, a limited number +or no test cases may exist. Users should double-check that +conversions are correct and make adjustments as needed! + +Notes +----- + +- This package is intended to work on plain old Python data structures, + meaning it should work without needing to import libcasm, pymatgen, + ase, etc. +- Use of standard Python modules and numpy is allowed + +""" + +import warnings + +warnings.warn( + """ + Users of libcasm.xtal.convert should take care to double-check + that conversions are performed correctly! + + This package is a work in progress. Particularly for more + complicated structures or molecules, such as those with mixed + occupation, charge, spin, and magnetic moments, a limited number + or no test cases may exist. Users should double-check that + conversions are correct and make adjustments as needed! + + Suppress this warning with: + + import warnings + warnings.simplefilter("ignore") + + Or by setting the PYTHONWARNINGS environment variable: + + export PYTHONWARNINGS="ignore" + + """ +) diff --git a/python/libcasm/xtal/convert/pymatgen.py b/python/libcasm/xtal/convert/pymatgen.py new file mode 100644 index 0000000..19a37a9 --- /dev/null +++ b/python/libcasm/xtal/convert/pymatgen.py @@ -0,0 +1,896 @@ +"""Convert between CASM and pymatgen dict formats (v2023.10.4)""" + +# Note: +# - This package is intended to work on plain old Python data structures, +# meaning it should work without needing to import libcasm, pymatgen, +# ase, etc. +# - Use of standard Python modules and numpy is allowed + +import copy +import re +from typing import Literal, Optional + +import numpy as np + + +def make_pymatgen_lattice_dict( + matrix: list[list[float]], + pbc: tuple[bool] = (True, True, True), +) -> dict: + """Create the pymatgen Lattice dict + + Parameters + ---------- + matrix: list[list[float]] + List of lattice vectors, (i.e. row-vector matrix of lattice vectors). + pbc: tuple[bool] = (True, True, True) + A tuple defining the periodic boundary conditions along the three + axes of the lattice. Default is periodic in all directions. This + is not stored in a CASM :class:`~_xtal.Lattice` and must be specified + if it should be anything other than periodic along all three axes. + + Returns + ------- + data: dict + The pymatgen dict representation of a lattice, with format: + + matrix: list[list[float]] + List of lattice vectors, (i.e. row-vector matrix of lattice vectors). + + pbc: tuple[bool] = (True, True, True) + A tuple defining the periodic boundary conditions along the three + axes of the lattice. Default is periodic in all directions. + + """ + return { + "matrix": matrix, + "pbc": pbc, + } + + +# TODO: +# - def make_pymatgen_molecule_dict(casm_occupant: dict) -> dict +# - def make_casm_occupant_dict(pymatgen_molecule: dict) -> dict + +# pymatgen.core.Site: dict +# species: list[dict] +# A list of species occupying the site, including occupation (float), and +# optional "idealized" (integer) oxidation state and spin. Calculated (float) +# charge and magnetic moments should be stored in Site.properties. +# +# Ex: +# +# [ +# { +# "element": "A", +# "occu": float, +# "oxidation_state": Optional[int], +# "spin": Optional[int], +# }, +# ... +# ] +# +# Note that pymatgen expects "element" is an actual element in the periodic +# table, or a dummy symbol which 'cannot have any part of first two letters that +# will constitute an Element symbol. Otherwise, a composition may be parsed +# wrongly. E.g., "X" is fine, but "Vac" is not because Vac contains V, a valid +# Element.'. +# +# xyz: list[float] +# Cartesian coordinates +# properties: Optional[dict] +# Properties associated with the site. Options include: "magmom", ? +# label: Optional[str] +# Label for site + +# pymatgen.core.IMolecule: +# sites: list[Site] +# charge: float = 0.0 +# Charge for the molecule. +# spin_multiplicity: Optional[int] = None +# properties: Optional[dict] = None +# Properties associated with the molecule as a whole. Options include: ? + +# def make_pymatgen_molecule_dict( +# casm_occupant: dict, +# ) -> dict: +# # charge = +# # spin_multiplicity = +# # sites = +# # properties = +# return { +# "charge": charge, +# "spin_multiplicity": spin_multiplicity, +# "sites": sites, +# "properties": properties, +# } + + +def copy_properties( + properties: dict, + rename_as: dict[str, str] = {}, + include_all: bool = True, +) -> dict: + """Copy a dictionary of properties, optionally renaming some + + Parameters + ---------- + properties: dict + The input properties + + rename_as: dict[str, str] = {} + A lookup table where the keys are keys in the input `in_properties` that should + be changed to the values in the output `out_properties`. + + include_all: bool = True + If True, all properties in the input `in_properties` are included in the output + `out_properties`. If False, only the properties found in `rename_as` are + included in the output. + + Returns + ------- + out_properties: dict + A copy of `in_properties`, with the specified renaming of keys. + + """ + out_properties = {} + for casm_key in properties: + if include_all is False: + if casm_key not in rename_as: + continue + new_key = rename_as.get(casm_key, casm_key) + out_properties[new_key] = copy.deepcopy(properties[casm_key]) + return out_properties + + +def make_pymatgen_structure_dict( + casm_structure: dict, + charge: Optional[float] = None, + pbc: tuple[bool] = (True, True, True), + atom_type_to_pymatgen_species_list: dict = {}, + atom_type_to_pymatgen_label: dict = {}, + casm_to_pymatgen_atom_properties: dict = {}, + include_all_atom_properties: bool = True, + casm_to_pymatgen_global_properties: dict = {}, + include_all_global_properties: bool = True, +) -> dict: + """Convert a CASM :class:`~_xtal.Structure` dict to a pymatgen IStructure dict + + Parameters + ---------- + structure: dict + The :class:`~_xtal.Structure`, represented as a dict, to be represented as a + pymatgen dict. Must be an atomic structure only. + + charge: Optional[float] = None + Overall charge of the structure. If None, when pymatgen constructs an + IStructure, default behavior is that the total charge is the sum of the + oxidation states (weighted by occupation) on each site. + + pbc: tuple[bool] = (True, True, True) + A tuple defining the periodic boundary conditions along the three + axes of the lattice. Default is periodic in all directions. This + is not stored in a CASM :class:`~_xtal.Lattice` and must be specified + if it should be anything other than periodic along all three axes. + + atom_type_to_pymatgen_species_list: dict = {} + A lookup table of CASM structure atom type (key) to pymatgen PeriodicSite + species list (value) representing the occupancy of the site. + + The pymatgen species list format is: + + .. code-block:: Python + + [ + { + "element": str, + "occu": float, + "oxidation_state": Optional[int], + "spin": Optional[int], + }, + ... + ] + + where: + + - "element": str, the element name, or a "dummy" species name + - "occu": float, the random site occupation + - "oxidation_state": Optional[int], the oxidation state, expected to + idealized to integer, e.g. -2, -1, 0, 1, 2, ... + - "spin: Optional[int], the spin associated with the species + + By default, atoms in the input structure will be represented using + ``[{ "element": atom_type, "occu": 1.0 }]``. + + atom_type_to_pymatgen_label: dict = {} + A lookup table of CASM structure atom type (key) to pymatgen PeriodicSite label + (value). If an atom_type in `casm_structure` is not found in the dict, then + the atom_type is used for the label in the output `pymatgen_structure`. + + casm_to_pymatgen_atom_properties: dict = {} + If a CASM structure atom property is found as a key in + `casm_to_pymatgen_atom_properties`, it is renamed in the pymatgen + PeriodicSite properties using the associated value. + + include_all_atom_properties: bool = True + If True (default), all atom properties in `structure` are included in the + result. If False , only `structure` atom properties found in + `atom_properties_to_pymatgen_properties` are included in the result. + + casm_to_pymatgen_global_properties: dict = {} + If a CASM structure global property is found as a key in + `casm_to_pymatgen_global_properties`, it is renamed in the pymatgen + structure properties using the associated value. + + include_all_global_properties: bool = True + If True (default), all global properties in `structure` are included in the + result. If False , only `structure` global properties found in + `global_properties_to_pymatgen_properties` are included in the result. + + Returns + ------- + data: dict + The pymatgen IStructure `dict` representation, with format: + + sites: list[PeriodicSite] + A list of PeriodicSite, with format: + + species: list[dict] + A list of dict as described for the input parameter + `atom_type_to_pymatgen_species_list`. + + abc: list[float] + Fractional coordinates of the site, relative to the lattice + vectors + + properties: Optional[dict] = None + Properties associated with the site as a dict, e.g. + ``{"magmom": 5}``. Obtained from + `casm_structure.atom_properties`. + + label: Optional[str] = None + Label for the site. Defaults to None. + + + charge: Optional[float] = None + Charge for the structure, expected to be equal to sum of oxidation + states. + + lattice: dict + Dict representation of a pymatgen Lattice, with format: + + matrix: list[list[float]] + List of lattice vectors, (i.e. row-vector matrix of lattice + vectors). + + pbc: tuple[bool] = (True, True, True) + A tuple defining the periodic boundary conditions along the + three axes of the lattice. Default is periodic in all + directions. + + properties: tuple[bool] = (True, True, True) + Properties associated with the structure as a whole. Options include: ? + + Raises + ------ + ValueError + For non-atomic structure, if ``"mol_type" in structure``. + """ + + if "mol_type" in casm_structure: + raise ValueError( + "Error: only atomic structures may be converted using to_structure_dict" + ) + + ### Reading casm_structure + + # required keys: "lattice_vectors", "atom_type", "coordinate_mode", "atom_coords" + lattice_column_vector_matrix = np.array( + casm_structure["lattice_vectors"] + ).transpose() + atom_type = casm_structure["atom_type"] + coordinate_mode = casm_structure["coordinate_mode"] + + if coordinate_mode in ["Fractional", "fractional", "FRAC", "Direct", "direct"]: + atom_coordinate_frac = np.array(casm_structure["atom_coords"]).transpose() + elif coordinate_mode in ["Cartesian", "cartesian", "CART"]: + atom_coordinate_cart = np.array(casm_structure["atom_coords"]).transpose() + atom_coordinate_frac = ( + np.linalg.pinv(lattice_column_vector_matrix) @ atom_coordinate_cart + ) + else: + raise Exception(f"Error: unrecognized coordinate_mode: {coordinate_mode}") + + atom_properties = {} + if "atom_properties" in casm_structure: + for key in casm_structure["atom_properties"]: + atom_properties[key] = np.array( + casm_structure["atom_properties"][key]["value"] + ).transpose() + + global_properties = {} + if "global_properties" in casm_structure: + for key in casm_structure["global_properties"]: + global_properties[key] = np.array( + casm_structure["global_properties"][key]["value"] + ).transpose() + + ### Convert to pymatgen dict + + # lattice + lattice = { + "matrix": lattice_column_vector_matrix.transpose().tolist(), + "pbc": pbc, + } + + # sites + sites = [] + for i, _atom_type in enumerate(atom_type): + # species list + default_species_list = [{"element": _atom_type, "occu": 1.0}] + species = atom_type_to_pymatgen_species_list.get( + _atom_type, default_species_list + ) + + # abc coordinate + abc = atom_coordinate_frac[:, i].tolist() + + # site properties + _atom_properties = {} + for key in atom_properties: + _atom_properties[key] = atom_properties[:, i] + properties = copy_properties( + properties=_atom_properties, + rename_as=casm_to_pymatgen_atom_properties, + include_all=include_all_atom_properties, + ) + + # label + label = atom_type_to_pymatgen_label.get(_atom_type, _atom_type) + + site = { + "species": species, + "abc": abc, + } + if label is not None: + site["label"] = label + if len(properties): + site["properties"] = properties + + sites.append(site) + + # properties + properties = copy_properties( + properties=global_properties, + rename_as=casm_to_pymatgen_global_properties, + include_all=include_all_global_properties, + ) + + pymatgen_structure = { + "lattice": lattice, + "sites": sites, + } + if charge is not None: + pymatgen_structure["charge"] = charge + if len(properties): + pymatgen_structure["properties"] = properties + + return pymatgen_structure + + +def make_casm_structure_dict( + pymatgen_structure: dict, + frac: bool = True, + atom_type_from: Literal["element", "label", "species_list"] = "element", + atom_type_to_pymatgen_species_list: dict = {}, + atom_type_to_pymatgen_label: dict = {}, + casm_to_pymatgen_atom_properties: dict = {}, + include_all_atom_properties: bool = True, + casm_to_pymatgen_global_properties: dict = {}, + include_all_global_properties: bool = True, +) -> dict: + """Convert a pymatgen IStructure dict to an atomic CASM :class:`~_xtal.Structure` + dict + + Notes + ----- + + - An atomic CASM :class:`~_xtal.Structure` only allows one species at each basis + site, whereas a pymatgen IStructure can allow a composition at each basis site. + - The species at each site in the resulting CASM structure can be determined using + one of several options chosen by a choice of the `atom_type_from` parameter. + + Parameters + ---------- + pymatgen_structure: dict + The pymatgen IStructure, represented as a dict, to be converted to a CASM + :class:`~_xtal.Structure` dict representation. Must be an atomic structure only. + + frac: bool = True + If True, coordinates in the result are expressed in fractional coordinates + relative to the lattice vectors. Otherwise, Cartesian coordinates are used. + + atom_type_from: Literal["element", "label", "species_list"] = "element" + Specifies which component of `pymatgen_structure` is used to determine the CASM + structure `atom_type`. Options are: + + - "element": Use PeriodicSite species element name to determine the CASM + `atom_type`. + - "label": Use PeriodicSite "label" and `atom_type_to_pymatgen_label` to + determine the CASM `atom_type`. With this option, a "label" must exist in + the pymatgen PeriodicSite dict. By default, the label found is used for the + atom type in the resulting CASM structure. If the label found is a value in + the `atom_type_to_pymatgen_label` dict, then the associated key is used for + the atom type in the resulting CASM structure. + - "species_list": Use PeriodicSite species list to determine the CASM + `atom_type` from the `atom_type_to_pymatgen_species_list` dict. The species + list must compare equal to a value in the `atom_type_to_pymatgen_species_list` + dict or the default value ``[{ "element": atom_type, "occu": 1.0 }]``, + otherwise an exception will be raised. + + atom_type_to_pymatgen_species_list: dict = {} + A lookup table of CASM structure atom type (key) to pymatgen PeriodicSite + species list (value) representing the occupancy of the site. + + The pymatgen species list format is: + + .. code-block:: Python + + [ + { + "element": str, + "occu": float, + "oxidation_state": Optional[int], + "spin": Optional[int], + }, + ... + ] + + where: + + - "element": str, the element name, or a "dummy" species name + - "occu": float, the random site occupation + - "oxidation_state": Optional[int], the oxidation state, expected to + idealized to integer, e.g. -2, -1, 0, 1, 2, ... + - "spin: Optional[int], the spin associated with the species + + By default, atoms in the input structure will be represented using + ``[{ "element": atom_type, "occu": 1.0 }]``. + + atom_type_to_pymatgen_label: dict = {} + A lookup table of CASM structure atom type (key) to pymatgen PeriodicSite label + (value). + + casm_to_pymatgen_atom_properties: dict = {} + If a CASM structure atom property is found as a key in + `casm_to_pymatgen_atom_properties`, it is renamed in the pymatgen + PeriodicSite properties using the associated value. + + include_all_atom_properties: bool = True + If True (default), all atom properties in `structure` are included in the + result. If False , only `structure` atom properties found in + `atom_properties_to_pymatgen_properties` are included in the result. + + casm_to_pymatgen_global_properties: dict = {} + If a CASM structure global property is found as a key in + `casm_to_pymatgen_global_properties`, it is renamed in the pymatgen + structure properties using the associated value. + + include_all_global_properties: bool = True + If True (default), all global properties in `structure` are included in the + result. If False , only `structure` global properties found in + `global_properties_to_pymatgen_properties` are included in the result. + + Returns + ------- + data: dict + The CASM Structure `dict` representation, with format described + `here `_. + """ + + ### Reading pymatgen_structure + + # required keys: "lattice_vectors", "atom_type", "coordinate_mode", "atom_coords" + lattice = pymatgen_structure["lattice"]["matrix"] + # ? charge = pymatgen_structure.get("charge", None) + sites = pymatgen_structure.get("sites", []) + + n_sites = len(sites) + atom_type = [] + atom_coordinate_frac = np.zeros((3, n_sites)) + atom_properties = {} + for i, site in enumerate(sites): + # "species" / "abc" / "label" / "properties" + + # Determine the CASM atom_type associated with this site. The method + # depends on the choice of the `atom_type_from`. + if atom_type_from == "element": + if len(site["species"]) != 1: + raise Exception( + f"Error: multiple species on site {i}, which is not " + f'allowed with atom_type_from=="element"' + ) + if "element" not in site["species"][0]: + raise Exception( + f"Error: element not found for site {i}, " + f'which is not allowed with atom_type_from=="element"' + ) + atom_type.append(site["species"][0]["element"]) + elif atom_type_from == "label": + if "label" not in site: + raise Exception( + f"Error: no label found on site {i}, which is not allowed " + f'with atom_type_from=="label"' + ) + label = site.get("label") + if label is None: + raise Exception( + f"Error: label is null for site {i}, " + f'which is not allowed with atom_type_from=="label"' + ) + _atom_type = None + for key, value in atom_type_to_pymatgen_label.items(): + if value == label: + _atom_type = key + break + if _atom_type is None: + _atom_type = label + atom_type.append(_atom_type) + elif atom_type_from == "species_list": + _atom_type = None + for key, value in atom_type_to_pymatgen_species_list.items(): + if value == site["species"]: + _atom_type = key + break + if _atom_type is None: + if len(site["species"]) != 1: + raise Exception( + f'Error: no match found for the "species" list on site {i}, "' + f'which is not allowed with atom_type_from=="species_list"' + ) + element = site["species"][0].get("element", None) + default_species_list = [{"element": element, "occu": 1.0}] + if element is None or site["species"] != default_species_list: + raise Exception( + f'Error: no match found for the "species" list on site {i}, "' + f'which is not allowed with atom_type_from=="species_list"' + ) + _atom_type = element + atom_type.append(_atom_type) + else: + raise Exception(f"Error: invalid atom_type_from=={atom_type_from}") + + atom_coordinate_frac[:, i] = np.array(site["abc"]) + + if "properties" in site: + for key in site["properties"]: + _value = site["properties"][key] + if isinstance(_value, [float, int]): + value = np.array([_value], dtype="float") + elif isinstance(_value, list): + value = np.array([_value], dtype="float") + else: + raise Exception( + f"Error: unsupported site properties: {str(_value)}" + ) + if len(value.shape) != 1: + raise Exception( + "Error: only scalar and vector site properties are supported" + ) + if key not in atom_properties: + atom_properties[key]["value"] = np.zeros((value.size, n_sites)) + atom_properties[key]["value"][:, i] = value + + global_properties = {} + if "properties" in pymatgen_structure: + for key in pymatgen_structure["properties"]: + _value = pymatgen_structure["properties"][key] + if isinstance(_value, [float, int]): + value = np.array([_value], dtype="float") + elif isinstance(_value, list): + value = np.array([_value], dtype="float") + else: + raise Exception( + f"Error: unsupported global properties, {str(key)}:{str(_value)}" + ) + if len(value.shape) != 1: + raise Exception( + "Error: only scalar and vector global properties are supported" + ) + global_properties[key]["value"] = value + + ### Convert to casm dict + + # ? "charge" + + if frac is True: + coordinate_mode = "Fractional" + atom_coords = atom_coordinate_frac + else: + coordinate_mode = "Cartesian" + column_vector_matrix = np.array(lattice).transpose() + atom_coords = column_vector_matrix @ atom_coordinate_frac + + atom_properties = copy_properties( + properties=atom_properties, + rename_as={ + value: key for key, value in casm_to_pymatgen_atom_properties.items() + }, + include_all=include_all_atom_properties, + ) + + global_properties = copy_properties( + properties=global_properties, + rename_as={ + value: key for key, value in casm_to_pymatgen_global_properties.items() + }, + include_all=include_all_global_properties, + ) + + casm_structure = { + "lattice_vectors": lattice, + "coordinate_mode": coordinate_mode, + "atom_coords": atom_coords.transpose().tolist(), + "atom_type": atom_type, + } + if len(atom_properties): + casm_structure["atom_properties"] = atom_properties + if len(global_properties): + casm_structure["global_properties"] = global_properties + + return casm_structure + + +def make_casm_prim_dict( + pymatgen_structure: dict, + title: str = "Prim", + description: Optional[str] = None, + frac: bool = True, + occupant_names_from: Literal["element", "species"] = "element", + occupant_name_to_pymatgen_species: dict = {}, + occupant_sets: Optional[list[set[str]]] = None, + use_site_labels: bool = False, + casm_species: dict = {}, + site_dof: dict = {}, + global_dof: dict = {}, +) -> dict: + """Use a pymatgen IStructure dict to construct a CASM :class:`~_xtal.Prim` dict + + Notes + ----- + + - The allowed occupants on each site in the resulting CASM prim is determined from + the elements on each site in the pymatgen structure. + - This method does not attempt to use site properties or structure properties + + Parameters + ---------- + pymatgen_structure: dict + The pymatgen IStructure, represented as a dict, to be converted to a CASM + :class:`~_xtal.Structure` dict representation. Must be an atomic structure only. + + title: str = "Prim" + A title for the project. For use by CASM, should consist of alphanumeric + characters and underscores only. The first character may not be a number. + + description: Optional[str] = None + An extended description for the project. Included by convention in most example + prim files, this attribute is not read by CASM. + + frac: bool = True + If True, coordinates in the result are expressed in fractional coordinates + relative to the lattice vectors. Otherwise, Cartesian coordinates are used. + + occupant_names_from: Literal["element", "species"] = "element" + Specifies which component of `pymatgen_structure` is used to determine the CASM + prim basis site occupant names. Options are: + + - "element": Use PeriodicSite species element name to determine the CASM + occupant names. + - "species": Use PeriodicSite species dicts to determine the CASM + occupant names from the `occupant_name_to_pymatgen_species` dict. The species + dict, excluding "occu", must compare equal to a value in the + `occupant_name_to_pymatgen_species` dict or the default value + ``[{ "element": occupant_name }]``, otherwise an exception will be raised. + + If a pymatgen site has mixed occupation, the corresponding CASM prim basis site + will have multiple allowed occupants. Additionally, the `occupant_sets` + parameter may be used to specify that the presence of one type of occupant + specifies that a particular set of occupants should be allowed on the + corresponding CASM prim basis sites. + + occupant_name_to_pymatgen_species: dict = {} + A lookup table of CASM occupant name (key) to pymatgen PeriodicSite + species (value) representing the occupancy of the site. + + The pymatgen species format is: + + .. code-block:: Python + + { + "element": str, + "oxidation_state": Optional[Union[int, float]], + "spin": Optional[Union[int, float]], + } + + where: + + - "element": str, the element name, or a "dummy" species name + - "oxidation_state": Optional[Union[int, float]], the oxidation state, expected + to be idealized to integer, e.g. -2, -1, 0, 1, 2, ... + - "spin: Optional[Union[int, float]], the spin associated with the species + + Note that the "occu" part of the species representation on a PeriodicSite is + ignored by this method. The pymatgen documentation / implementation seem + inconsistent whether these should be integer or float; for purposes of + this method they just must compare exactly. + + occupant_sets: Optional[list[set[str]]] = None, + Optional sets of occupants that should be allowed on the same sites. + + For example, if ``occupant_sets == [set(["A", "B"]), set(["C", "D"])]``, then + any site where the pymatgen structure has either "A" or "B" occupation, the CASM + prim basis site occupants will be expanded to include both "A" and "B", and any + site where the pymatgen structure has either "C" or "D" occupation, the CASM + prim basis site occupants will be expanded to include both "C" and "D". + + use_site_labels: bool = False + If True, set CASM prim basis site labels based on the pymatgen structure site + labels. If False, do not set prim basis site labels. CASM prim basis site labels + are an integer, greater than or equal to zero, that if provided distinguishes + otherwise identical sites. + + casm_species: dict = {}, + A dictionary used to define fixed properties of any species listed as an allowed + occupant that is not a single isotropic atom. This parameter is filtered to + only include occupants found in the input, then copied to the "species" + attribute of the output prim dict. + + site_dof: dict = {} + A dictionary specifying the types of continuous site degrees of freedom (DoF) + allowed on every basis site. Note that CASM supports having different DoF on + each basis site, but this method currently does not. + + global_dof: dict = {} + A dictionary specifying the types of continuous global degrees of freedom (DoF) + and their basis. + + Returns + ------- + data: dict + The CASM Prim `dict` representation, with format described + `here `_. + Site occupants are sorted in alphabetical order. + """ + + # validate title + if not re.match("^[a-zA-Z]\w*$", title): + raise Exception(f"Error: invalid title: {title}") + + ### Reading pymatgen_structure + + # required keys: "lattice_vectors", "atom_type", "coordinate_mode", "atom_coords" + lattice = pymatgen_structure["lattice"]["matrix"] + # ? charge = pymatgen_structure.get("charge", None) + sites = pymatgen_structure.get("sites", []) + + n_sites = len(sites) + occupants = [] + site_coordinate_frac = np.zeros((3, n_sites)) + label = [] + for i, site in enumerate(sites): + # "species" / "abc" / "label" / "properties" + + # Determine the CASM occupant names associated with this site. The method + # depends on the choice of the `occupant_names_from`. + if occupant_names_from == "element": + site_occupants = [] + for j, species in enumerate(site["species"]): + element = species.get("element", None) + if element is None: + raise Exception( + f'Error: no element for species {j} on site {i}, which is not "' + f'allowed with occupant_names_from=="element"' + ) + site_occupants.append(element) + occupants.append(site_occupants) + elif occupant_names_from == "species": + site_occupants = [] + for j, species in enumerate(site["species"]): + occupant_name = None + _species = copy.deepcopy(species) + if "occu" in _species: + del _species["occu"] + # check occupant_name_to_pymatgen_species + for key, value in occupant_name_to_pymatgen_species: + _value = copy.deepcopy(value) + if "occu" in _value: + del _value["occu"] + if value == _species: + occupant_name = key + break + # check default, occupant_name == element (no oxidation_state, no spin) + if occupant_name is None: + element = species.get("element", None) + default_species = [{"element": element}] + if _species == default_species: + occupant_name = element + # if still not found, raise + if occupant_name is None: + raise Exception( + f'Error: no match found for species {j} on site {i}, which is "' + f'not allowed with occupant_names_from=="species"' + ) + site_occupants.append(occupant_name) + occupants.append(site_occupants) + else: + raise Exception( + f"Error: invalid occupant_names_from=={occupant_names_from}" + ) + + site_coordinate_frac[:, i] = np.array(site["abc"]) + label.append(site.get("label", None)) + + ### Convert to casm dict + + # ? "charge" + + # Get coordinate mode and coordinates + if frac is True: + coordinate_mode = "Fractional" + site_coords = site_coordinate_frac + else: + coordinate_mode = "Cartesian" + column_vector_matrix = np.array(lattice).transpose() + site_coords = column_vector_matrix @ site_coordinate_frac + + # Expand site occupants based on occupant_sets + if occupant_sets is not None: + for i, site_occupants in enumerate(occupants): + _site_occupants = set(site_occupants) + + for occ_name in site_occupants: + for occ_set in occupant_sets: + if occ_name in occ_set: + _site_occupants.update(occ_set) + + occupants[i] = sorted(list(_site_occupants)) + + # Get entries in casm_species that are actual occupants in the resulting prim + filtered_casm_species = {} + for site_occupants in occupants: + for occupant_name in site_occupants: + if occupant_name in casm_species: + filtered_casm_species[occupant_name] = copy.deepcopy( + casm_species[occupant_name] + ) + + # Construct prim basis list + basis = [] + distinct_site_labels = list(set(label)) + for i in range(n_sites): + basis_site = { + "coordinate": site_coords[:, i].tolist(), + "occupants": occupants[i], + } + if use_site_labels: + basis_site["label"] = distinct_site_labels.index(label[i]) + if len(site_dof): + basis_site["dofs"] = (copy.deepcopy(site_dof),) + basis.append(basis_site) + + # Construct prim dict + prim = { + "title": title, + "lattice_vectors": lattice, + "coordinate_mode": coordinate_mode, + "basis": basis, + } + if description is not None: + prim["description"] = description + if len(global_dof): + prim["dofs"] = copy.deepcopy(global_dof) + if len(filtered_casm_species): + prim["species"] = filtered_casm_species + + return prim diff --git a/python/setup.py b/python/setup.py index 07d2aa0..54b61b9 100644 --- a/python/setup.py +++ b/python/setup.py @@ -70,7 +70,7 @@ setup( name="libcasm-xtal", version=__version__, - packages=["libcasm", "libcasm.xtal"], + packages=["libcasm", "libcasm.xtal", "libcasm.xtal.convert"], install_requires=["pybind11", "libcasm-global>=2.0.2"], ext_modules=ext_modules, cmdclass={"build_ext": build_ext}, diff --git a/python/tests/convert/test_pymatgen.py b/python/tests/convert/test_pymatgen.py new file mode 100644 index 0000000..a26f68e --- /dev/null +++ b/python/tests/convert/test_pymatgen.py @@ -0,0 +1,503 @@ +import math + +import numpy as np +import pytest + +import libcasm.xtal.convert.pymatgen as convert + +# Tests should work with or without libcasm.xtal +try: + import libcasm.xtal as xtal + + import_libcasm_xtal = True +except ImportError: + import_libcasm_xtal = False + +# Tests should work with or without pymatgen +try: + from pymatgen.core import IStructure + + import_pymatgen = True +except ImportError: + import_pymatgen = False + + +def pretty_print(data): + if import_libcasm_xtal: + print(xtal.pretty_json(data)) + else: + import json + + print(json.dumps(data, indent=2)) + + +def test_pymatgen(): + """This just prints a message if pymatgen is not installed that we will be + skipping some checks""" + if not import_pymatgen: + pytest.skip("Skipping checks that require pymatgen") + + +def test_pymatgen_compare(): + """Warning: order matters in comparison of IStructure + + For pymatgen v2023.11.12 the following asserts pass: + """ + if import_pymatgen: + lattice_vectors = [[5.692, 0.0, 0.0], [0.0, 5.692, 0.0], [0.0, 0.0, 5.692]] + coords_frac = [ + [0.0, 0.0, 0.0], + [0.0, 0.5, 0.5], + [0.5, 0.0, 0.5], + [0.5, 0.5, 0.0], + [0.5, 0.5, 0.5], + [0.5, 0.0, 0.0], + [0.0, 0.5, 0.0], + [0.0, 0.0, 0.5], + ] + element_structure = IStructure( + lattice=lattice_vectors, + species=["Na"] * 4 + ["Cl"] * 4, + coords=coords_frac, + coords_are_cartesian=False, + ) + species_structure = IStructure( + lattice=lattice_vectors, + species=["Na+"] * 4 + ["Cl-"] * 4, + coords=coords_frac, + coords_are_cartesian=False, + ) + assert element_structure != species_structure + assert species_structure == element_structure + + +def test_BCC_Fe_element(): + ### Make pymatgen structure from CASM structure + casm_structure = { + "atom_coords": [[0.0, 0.0, 0.0]], + "atom_type": ["Fe"], + "coordinate_mode": "Fractional", + "lattice_vectors": [ + [-1.1547005383792517, 1.1547005383792517, 1.1547005383792517], + [1.1547005383792517, -1.1547005383792517, 1.1547005383792517], + [1.1547005383792517, 1.1547005383792517, -1.1547005383792517], + ], + } + if import_libcasm_xtal: + bcc_casm_structure = xtal.Structure.from_dict(casm_structure) + assert isinstance(bcc_casm_structure, xtal.Structure) + + # Check pymatgen as_dict -> from_dict + if import_pymatgen: + bcc_pymatgen_structure = IStructure( + lattice=casm_structure["lattice_vectors"], + species=casm_structure["atom_type"], + coords=casm_structure["atom_coords"], + coords_are_cartesian=False, + ) + # pretty_print(bcc_pymatgen_structure.as_dict()) + + # Check libcasm.xtal.convert.to_pymatgen_structure_dict + pymatgen_structure = convert.make_pymatgen_structure_dict(casm_structure) + # pretty_print(pymatgen_structure) + + assert len(pymatgen_structure) == 2 + assert np.allclose( + np.array(pymatgen_structure["lattice"]["matrix"]), + np.array(casm_structure["lattice_vectors"]), + ) + assert pymatgen_structure["lattice"]["pbc"] == (True, True, True) + assert "charge" not in pymatgen_structure + assert "properties" not in pymatgen_structure + assert len(pymatgen_structure["sites"]) == 1 + + ## site 0 + site = pymatgen_structure["sites"][0] + assert len(site) == 3 + assert np.allclose(site["abc"], np.array([0.0, 0.0, 0.0])) + assert site["label"] == "Fe" + assert "properties" not in site + assert len(site["species"]) == 1 + + # species 0 + species = site["species"][0] + assert species["element"] == "Fe" + assert math.isclose(species["occu"], 1.0) + assert "oxidation_state" not in species + + if import_pymatgen: + bcc_pymatgen_structure_in = IStructure.from_dict(pymatgen_structure) + assert bcc_pymatgen_structure_in == bcc_pymatgen_structure + assert bcc_pymatgen_structure == bcc_pymatgen_structure_in + + ### Make CASM structure from pymatgen structure + casm_structure_2 = convert.make_casm_structure_dict( + pymatgen_structure=pymatgen_structure, + frac=True, + atom_type_from="element", + ) + # pretty_print(casm_structure_2) + assert len(casm_structure_2) == 4 + assert np.allclose( + np.array(casm_structure_2["lattice_vectors"]), + np.array(casm_structure["lattice_vectors"]), + ) + assert casm_structure_2["atom_type"] == casm_structure["atom_type"] + assert np.allclose( + np.array(casm_structure_2["atom_coords"]), + np.array(casm_structure["atom_coords"]), + ) + assert casm_structure_2["coordinate_mode"] == "Fractional" + assert "atom_properties" not in casm_structure_2 + assert "global_properties" not in casm_structure_2 + + if import_libcasm_xtal: + bcc_casm_structure_2 = xtal.Structure.from_dict(casm_structure) + assert bcc_casm_structure.is_equivalent_to(bcc_casm_structure_2) + + ### Make CASM prim from pymatgen structure + casm_prim = convert.make_casm_prim_dict( + pymatgen_structure=pymatgen_structure, + frac=True, + occupant_names_from="element", + occupant_sets=[set(["Fe", "Cr"])], + ) + # pretty_print(casm_prim) + assert len(casm_prim) == 4 + assert "title" in casm_prim + assert "description" not in casm_prim + assert np.allclose( + np.array(casm_prim["lattice_vectors"]), + np.array(casm_structure["lattice_vectors"]), + ) + assert casm_prim["coordinate_mode"] == "Fractional" + assert "dofs" not in casm_prim + + assert len(casm_prim["basis"]) == 1 + basis_site = casm_prim["basis"][0] + assert len(basis_site) == 2 + assert np.allclose( + np.array(basis_site["coordinate"]), np.array(casm_structure["atom_coords"][0]) + ) + assert basis_site["occupants"] == ["Cr", "Fe"] + + if import_libcasm_xtal: + bcc_casm_prim = xtal.Prim.from_dict(casm_prim) + assert isinstance(bcc_casm_prim, xtal.Prim) + + +def test_NaCl_species_1(): + # This example constructs a CASM structure without oxidation states + # and uses atom_type_to_pymatgen_species_list + casm_structure = { + "atom_coords": [ + [0.0, 0.0, 0.0], + [0.0, 0.5, 0.5], + [0.5, 0.0, 0.5], + [0.5, 0.5, 0.0], + [0.5, 0.5, 0.5], + [0.5, 0.0, 0.0], + [0.0, 0.5, 0.0], + [0.0, 0.0, 0.5], + ], + "atom_type": ["Na", "Na", "Na", "Na", "Cl", "Cl", "Cl", "Cl"], + "coordinate_mode": "Fractional", + "lattice_vectors": [[5.692, 0.0, 0.0], [0.0, 5.692, 0.0], [0.0, 0.0, 5.692]], + } + atom_type_to_pymatgen_species_list = { + "Na": [{"element": "Na", "oxidation_state": 1, "occu": 1}], + "Cl": [{"element": "Cl", "oxidation_state": -1, "occu": 1}], + } + pymatgen_species = ["Na+"] * 4 + ["Cl-"] * 4 + + if import_libcasm_xtal: + NaCl_casm_structure = xtal.Structure.from_dict(casm_structure) + assert isinstance(NaCl_casm_structure, xtal.Structure) + + if import_pymatgen: + NaCl_pymatgen_structure = IStructure( + lattice=casm_structure["lattice_vectors"], + species=pymatgen_species, + coords=casm_structure["atom_coords"], + coords_are_cartesian=False, + ) + # pretty_print(NaCl_pymatgen_structure.as_dict()) + + # Check libcasm.xtal.convert.to_pymatgen_structure_dict + pymatgen_structure = convert.make_pymatgen_structure_dict( + casm_structure, + atom_type_to_pymatgen_species_list=atom_type_to_pymatgen_species_list, + ) + # pretty_print(pymatgen_structure) + + assert len(pymatgen_structure) == 2 + assert np.allclose( + np.array(pymatgen_structure["lattice"]["matrix"]), + np.array(casm_structure["lattice_vectors"]), + ) + assert pymatgen_structure["lattice"]["pbc"] == (True, True, True) + assert "charge" not in pymatgen_structure + assert "properties" not in pymatgen_structure + assert len(pymatgen_structure["sites"]) == 8 + + for i, site in enumerate(pymatgen_structure["sites"]): + # Na+ sites: + if i < 4: + assert len(site) == 3 + assert np.allclose(site["abc"], np.array(casm_structure["atom_coords"][i])) + assert len(site["species"]) == 1 + assert site["label"] == "Na" + + # species 0 + species = site["species"][0] + assert species["element"] == "Na" + assert math.isclose(species["occu"], 1) + assert math.isclose(species["oxidation_state"], 1) + + # Cl- sites: + else: + assert len(site) == 3 + assert np.allclose(site["abc"], np.array(casm_structure["atom_coords"][i])) + assert len(site["species"]) == 1 + assert site["label"] == "Cl" + + # species 0 + species = site["species"][0] + assert species["element"] == "Cl" + assert math.isclose(species["occu"], 1) + assert math.isclose(species["oxidation_state"], -1) + + if import_pymatgen: + NaCl_pymatgen_structure_in = IStructure.from_dict(pymatgen_structure) + # pretty_print(NaCl_pymatgen_structure_in.as_dict()) + assert NaCl_pymatgen_structure_in == NaCl_pymatgen_structure + assert NaCl_pymatgen_structure == NaCl_pymatgen_structure_in + + ### Make CASM structure from pymatgen structure + casm_structure_2 = convert.make_casm_structure_dict( + pymatgen_structure=pymatgen_structure, + frac=True, + atom_type_from="species_list", + atom_type_to_pymatgen_species_list=atom_type_to_pymatgen_species_list, + ) + # pretty_print(casm_structure_2) + assert len(casm_structure_2) == 4 + assert np.allclose( + np.array(casm_structure_2["lattice_vectors"]), + np.array(casm_structure["lattice_vectors"]), + ) + assert casm_structure_2["atom_type"] == casm_structure["atom_type"] + assert np.allclose( + np.array(casm_structure_2["atom_coords"]), + np.array(casm_structure["atom_coords"]), + ) + assert casm_structure_2["coordinate_mode"] == "Fractional" + assert "atom_properties" not in casm_structure_2 + assert "global_properties" not in casm_structure_2 + + if import_libcasm_xtal: + NaCl_casm_structure_2 = xtal.Structure.from_dict(casm_structure) + assert NaCl_casm_structure.is_equivalent_to(NaCl_casm_structure_2) + + ### Make CASM prim from pymatgen structure + casm_prim = convert.make_casm_prim_dict( + pymatgen_structure=pymatgen_structure, + frac=True, + occupant_names_from="element", + occupant_sets=[set(["Na", "Va"]), set(["Cl", "Va"])], + ) + # pretty_print(casm_prim) + assert len(casm_prim) == 4 + assert "title" in casm_prim + assert "description" not in casm_prim + assert np.allclose( + np.array(casm_prim["lattice_vectors"]), + np.array(casm_structure["lattice_vectors"]), + ) + assert casm_prim["coordinate_mode"] == "Fractional" + assert "dofs" not in casm_prim + + assert len(casm_prim["basis"]) == 8 + + for i, basis_site in enumerate(casm_prim["basis"]): + # Na+ sites: + if i < 4: + assert len(basis_site) == 2 + assert np.allclose( + np.array(basis_site["coordinate"]), + np.array(casm_structure["atom_coords"][i]), + ) + assert basis_site["occupants"] == ["Na", "Va"] + + # Cl- sites: + else: + assert len(basis_site) == 2 + assert np.allclose( + np.array(basis_site["coordinate"]), + np.array(casm_structure["atom_coords"][i]), + ) + assert basis_site["occupants"] == ["Cl", "Va"] + + if import_libcasm_xtal: + NaCl_casm_prim = xtal.Prim.from_dict(casm_prim) + assert isinstance(NaCl_casm_prim, xtal.Prim) + + +def test_NaCl_species_2(): + # This example uses atom_type_to_pymatgen_label to set labels and then also + # tests using atom_types_from=="label" converting the pymatgen structure to a + # casm structure + casm_structure = { + "atom_coords": [ + [0.0, 0.0, 0.0], + [0.0, 0.5, 0.5], + [0.5, 0.0, 0.5], + [0.5, 0.5, 0.0], + [0.5, 0.5, 0.5], + [0.5, 0.0, 0.0], + [0.0, 0.5, 0.0], + [0.0, 0.0, 0.5], + ], + "atom_type": ["Na", "Na", "Na", "Na", "Cl", "Cl", "Cl", "Cl"], + "coordinate_mode": "Fractional", + "lattice_vectors": [[5.692, 0.0, 0.0], [0.0, 5.692, 0.0], [0.0, 0.0, 5.692]], + } + atom_type_to_pymatgen_species_list = { + "Na": [{"element": "Na", "oxidation_state": 1, "occu": 1}], + "Cl": [{"element": "Cl", "oxidation_state": -1, "occu": 1}], + } + atom_type_to_pymatgen_label = {"Na": "Na+", "Cl": "Cl-"} + pymatgen_species = ["Na+"] * 4 + ["Cl-"] * 4 + + if import_libcasm_xtal: + NaCl_casm_structure = xtal.Structure.from_dict(casm_structure) + assert isinstance(NaCl_casm_structure, xtal.Structure) + + if import_pymatgen: + NaCl_pymatgen_structure = IStructure( + lattice=casm_structure["lattice_vectors"], + species=pymatgen_species, + coords=casm_structure["atom_coords"], + coords_are_cartesian=False, + ) + # pretty_print(NaCl_pymatgen_structure.as_dict()) + + # Check libcasm.xtal.convert.to_pymatgen_structure_dict + pymatgen_structure = convert.make_pymatgen_structure_dict( + casm_structure, + atom_type_to_pymatgen_species_list=atom_type_to_pymatgen_species_list, + atom_type_to_pymatgen_label=atom_type_to_pymatgen_label, + ) + # pretty_print(pymatgen_structure) + + assert len(pymatgen_structure) == 2 + assert np.allclose( + np.array(pymatgen_structure["lattice"]["matrix"]), + np.array(casm_structure["lattice_vectors"]), + ) + assert pymatgen_structure["lattice"]["pbc"] == (True, True, True) + assert "charge" not in pymatgen_structure + assert "properties" not in pymatgen_structure + assert len(pymatgen_structure["sites"]) == 8 + + for i, site in enumerate(pymatgen_structure["sites"]): + # Na+ sites: + if i < 4: + assert len(site) == 3 + assert np.allclose(site["abc"], np.array(casm_structure["atom_coords"][i])) + assert len(site["species"]) == 1 + assert site["label"] == "Na+" + + # species 0 + species = site["species"][0] + assert species["element"] == "Na" + assert math.isclose(species["occu"], 1) + assert math.isclose(species["oxidation_state"], 1) + + # Cl- sites: + else: + assert len(site) == 3 + assert np.allclose(site["abc"], np.array(casm_structure["atom_coords"][i])) + assert len(site["species"]) == 1 + assert site["label"] == "Cl-" + + # species 0 + species = site["species"][0] + assert species["element"] == "Cl" + assert math.isclose(species["occu"], 1) + assert math.isclose(species["oxidation_state"], -1) + + if import_pymatgen: + NaCl_pymatgen_structure_in = IStructure.from_dict(pymatgen_structure) + # pretty_print(NaCl_pymatgen_structure_in.as_dict()) + assert NaCl_pymatgen_structure_in == NaCl_pymatgen_structure + assert NaCl_pymatgen_structure == NaCl_pymatgen_structure_in + + ### Make CASM structure from pymatgen structure + casm_structure_2 = convert.make_casm_structure_dict( + pymatgen_structure=pymatgen_structure, + frac=True, + atom_type_from="label", + atom_type_to_pymatgen_label=atom_type_to_pymatgen_label, + ) + # pretty_print(casm_structure_2) + assert len(casm_structure_2) == 4 + assert np.allclose( + np.array(casm_structure_2["lattice_vectors"]), + np.array(casm_structure["lattice_vectors"]), + ) + assert casm_structure_2["atom_type"] == casm_structure["atom_type"] + assert np.allclose( + np.array(casm_structure_2["atom_coords"]), + np.array(casm_structure["atom_coords"]), + ) + assert casm_structure_2["coordinate_mode"] == "Fractional" + assert "atom_properties" not in casm_structure_2 + assert "global_properties" not in casm_structure_2 + + if import_libcasm_xtal: + NaCl_casm_structure_2 = xtal.Structure.from_dict(casm_structure) + assert NaCl_casm_structure.is_equivalent_to(NaCl_casm_structure_2) + + ### Make CASM prim from pymatgen structure + casm_prim = convert.make_casm_prim_dict( + pymatgen_structure=pymatgen_structure, + frac=True, + occupant_names_from="element", + occupant_sets=[set(["Na", "Va"]), set(["Cl", "Va"])], + ) + # pretty_print(casm_prim) + assert len(casm_prim) == 4 + assert "title" in casm_prim + assert "description" not in casm_prim + assert np.allclose( + np.array(casm_prim["lattice_vectors"]), + np.array(casm_structure["lattice_vectors"]), + ) + assert casm_prim["coordinate_mode"] == "Fractional" + assert "dofs" not in casm_prim + + assert len(casm_prim["basis"]) == 8 + + for i, basis_site in enumerate(casm_prim["basis"]): + # Na+ sites: + if i < 4: + assert len(basis_site) == 2 + assert np.allclose( + np.array(basis_site["coordinate"]), + np.array(casm_structure["atom_coords"][i]), + ) + assert basis_site["occupants"] == ["Na", "Va"] + + # Cl- sites: + else: + assert len(basis_site) == 2 + assert np.allclose( + np.array(basis_site["coordinate"]), + np.array(casm_structure["atom_coords"][i]), + ) + assert basis_site["occupants"] == ["Cl", "Va"] + + if import_libcasm_xtal: + NaCl_casm_prim = xtal.Prim.from_dict(casm_prim) + assert isinstance(NaCl_casm_prim, xtal.Prim) diff --git a/setup.py b/setup.py index aebf442..3b989bf 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name="libcasm-xtal", version="2.0a8", - packages=["libcasm", "libcasm.xtal"], + packages=["libcasm", "libcasm.xtal", "libcasm.xtal.convert"], package_dir={"": "python"}, cmake_install_dir="python/libcasm", include_package_data=False, From f9069371e9bf0b740cd5676e499748007b28ffbc Mon Sep 17 00:00:00 2001 From: bpuchala Date: Wed, 6 Dec 2023 13:53:06 -0500 Subject: [PATCH 03/11] update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d6f8e..34567a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add to libcasm.xtal: substitute_structure_species - Add to libcasm.xtal.Prim: method labels, constructor parameter `labels` - Add to libcasm.xtal.Lattice: methods reciprocal, volume, lengths_and_angles, from_lengths_and_angles +- Add libcasm.xtal.convert package +- Add libcasm.xtal.convert.pymatgen ## [2.0a8] - 2023-11-15 From f3c81ecf8c5db4e245eff8c00b84a198a810bb84 Mon Sep 17 00:00:00 2001 From: bpuchala Date: Mon, 25 Dec 2023 23:04:00 -0500 Subject: [PATCH 04/11] factor conf.py --- python/doc/conf.py | 58 +++++++++++++++++++------- python/doc/reference/libcasm/index.rst | 4 +- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/python/doc/conf.py b/python/doc/conf.py index 3f300fe..fe33f3d 100644 --- a/python/doc/conf.py +++ b/python/doc/conf.py @@ -1,3 +1,18 @@ +import os + +# -- package specific configuration -- +project = "libcasm-xtal" +version = "2.0" # The short X.Y version. +release = "2.0a8" # The full version, including alpha/beta/rc tags. +project_desc = "CASM Crystallography" +logo_text = "libcasm-xtal" +github_url = "https://github.com/prisms-center/CASMcode_crystallography/" +pypi_url = "https://pypi.org/project/libcasm-xtal/" +intersphinx_libcasm_packages = [("global", "2.0"), ("configuration", "2.0")] + + +# -- CASM common configuration --- + # -*- coding: utf-8 -*- # # CASM documentation build configuration file, created by @@ -12,7 +27,6 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import os # -- General configuration ------------------------------------------------ @@ -35,8 +49,7 @@ # if LIBCASM_LOCAL_PYDOCS env variable is set, create local docs pydocs_path = os.environ.get("LIBCASM_LOCAL_PYDOCS", None) -packages = [("global", "2.0"), ("configuration", "2.0")] -for package, vers in packages: +for package, vers in intersphinx_libcasm_packages: if pydocs_path is None: url = ( f"https://prisms-center.github.io/CASMcode_pydocs/libcasm/{package}/{vers}/" @@ -87,7 +100,6 @@ master_doc = "index" # General information about the project. -project = "libcasm-xtal" copyright = "2023, CASM Developers" author = "CASM Developers" @@ -95,10 +107,7 @@ # |version| and |release|, also used in various other places throughout the # built documents. # -# The short X.Y version. -version = "2.0" -# The full version, including alpha/beta/rc tags. -release = "2.0a8" + # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -132,12 +141,29 @@ html_logo = "_static/small_logo.svg" html_theme_options = { "logo": { - "text": "libcasm-xtal", + "text": logo_text, "image_light": "_static/small_logo.svg", "image_dark": "_static/small_logo_dark.svg", }, "pygment_light_style": "xcode", "pygment_dark_style": "lightbulb", + "icon_links": [ + { + # Label for this link + "name": "GitHub", + "url": github_url, # required + "icon": "fa-brands fa-github", + "type": "fontawesome", + }, + { + # Label for this link + "name": "PyPI", + "url": pypi_url, # required + "icon": "fa-brands fa-python", + "type": "fontawesome", + }, + ], + # "primary_sidebar_end": ["primary_sidebar_end"] } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -192,8 +218,8 @@ latex_documents = [ ( master_doc, - "libcasm-xtal.tex", - "libcasm-xtal Documentation", + f"{project}.tex", + f"{project} Documentation", "CASM Developers", "manual", ), @@ -203,7 +229,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [(master_doc, "libcasm-xtal", "libcasm-xtal Documentation", [author], 1)] +man_pages = [(master_doc, f"{project}", f"{project} Documentation", [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -213,11 +239,11 @@ texinfo_documents = [ ( master_doc, - "libcasm-xtal", - "libcasm-xtal Documentation", + f"{project}", + f"{project} Documentation", author, - "libcasm-xtal", - "CASM Crystallography", + f"{project}", + project_desc, "Miscellaneous", ), ] diff --git a/python/doc/reference/libcasm/index.rst b/python/doc/reference/libcasm/index.rst index c5f5ef9..a5b494d 100644 --- a/python/doc/reference/libcasm/index.rst +++ b/python/doc/reference/libcasm/index.rst @@ -1,8 +1,8 @@ .. DO NOT DELETE! This causes _autosummary to generate stub files -libcasm.xtal module -=================== +Reference (libcasm-xtal) +======================== .. autosummary:: :toctree: _autosummary From b8c6ffd17e6b7aab190a2b2f097a26febf20efa0 Mon Sep 17 00:00:00 2001 From: bpuchala Date: Mon, 22 Jan 2024 08:39:08 -0500 Subject: [PATCH 05/11] add unit_cell, diagonal_only, and fixed_shape parameters to libcasm.xtal.enumerate_superlattices --- CHANGELOG.md | 7 +++ python/src/xtal.cpp | 62 +++++++++++--------- python/tests/test_enumerate_superlattices.py | 48 ++++++++++++++- 3 files changed, 88 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34567a2..0aad1ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add libcasm.xtal.convert.pymatgen +## Unreleased + +## Added + +- Added `unit_cell`, `diagonal_only`, and `fixed_shape` parameters to libcasm.xtal.enumerate_superlattices. + + ## [2.0a8] - 2023-11-15 ### Changed diff --git a/python/src/xtal.cpp b/python/src/xtal.cpp index f083427..4553a03 100644 --- a/python/src/xtal.cpp +++ b/python/src/xtal.cpp @@ -108,8 +108,15 @@ std::vector make_lattice_point_group( std::vector enumerate_superlattices( xtal::Lattice const &unit_lattice, std::vector const &point_group, Index max_volume, - Index min_volume = 1, std::string dirs = std::string("abc")) { - xtal::ScelEnumProps enum_props{min_volume, max_volume + 1, dirs}; + Index min_volume = 1, std::string dirs = std::string("abc"), + std::optional unit_cell = std::nullopt, + bool diagonal_only = false, bool fixed_shape = false) { + if (!unit_cell.has_value()) { + unit_cell = Eigen::Matrix3i::Identity(); + } + xtal::ScelEnumProps enum_props{min_volume, max_volume + 1, + dirs, unit_cell.value(), + diagonal_only, fixed_shape}; xtal::SuperlatticeEnumerator enumerator{unit_lattice, point_group, enum_props}; std::vector superlattices; @@ -1474,24 +1481,12 @@ PYBIND11_MODULE(_xtal, m) { If `superlattice` is not a superlattice of `unit_lattice`. )pbdoc"); - m.def( - "enumerate_superlattices", - [](xtal::Lattice unit_lattice, std::vector point_group, - Index max_volume, Index min_volume, - std::string dirs) -> std::vector { - xtal::ScelEnumProps enum_props{min_volume, max_volume + 1, dirs}; - xtal::SuperlatticeEnumerator enumerator{unit_lattice, point_group, - enum_props}; - std::vector superlattices; - for (auto const &superlat : enumerator) { - superlattices.push_back(xtal::canonical::equivalent( - superlat, point_group, unit_lattice.tol())); - } - return superlattices; - }, - py::arg("unit_lattice"), py::arg("point_group"), py::arg("max_volume"), - py::arg("min_volume") = Index(1), py::arg("dirs") = std::string("abc"), - R"pbdoc( + m.def("enumerate_superlattices", &enumerate_superlattices, + py::arg("unit_lattice"), py::arg("point_group"), py::arg("max_volume"), + py::arg("min_volume") = Index(1), py::arg("dirs") = std::string("abc"), + py::arg("unit_cell") = std::nullopt, py::arg("diagonal_only") = false, + py::arg("fixed_shape") = false, + R"pbdoc( Enumerate symmetrically distinct superlattices Superlattices satify: @@ -1505,14 +1500,14 @@ PYBIND11_MODULE(_xtal, m) { transformation matrix. Superlattices `S1` and `S2` are symmetrically equivalent if there exists `p` and - `U` such that: + `A` such that: .. code-block:: Python - S2 = p.matrix() @ S1 @ U, + S2 = p.matrix() @ S1 @ A, - where `p` is an element in the point group, and `U` is a unimodular matrix - (integer matrix, with abs(det(U))==1). + where `p` is an element in the point group, and `A` is a unimodular matrix + (integer matrix, with abs(det(A))==1). Parameters ---------- @@ -1524,15 +1519,26 @@ PYBIND11_MODULE(_xtal, m) { :func:`make_crystal_point_group()`, or the lattice point group, :func:`make_point_group()`. max_volume : int - The maximum volume superlattice to enumerate, as a multiple of the volume of - `unit_lattice`. + The maximum volume superlattice to enumerate. The volume is measured + relative the unit cell being used to generate supercells. min_volume : int, default=1 - The minimum volume superlattice to enumerate, as a multiple of the volume of - `unit_lattice`. + The minimum volume superlattice to enumerate. The volume is measured + relative the unit cell being used to generate supercells. dirs : str, default="abc" A string indicating which lattice vectors to enumerate over. Some combination of 'a', 'b', and 'c', where 'a' indicates the first lattice vector of the unit cell, 'b' the second, and 'c' the third. + unit_cell: Optional[np.ndarray] = None, + An integer shape=(3,3) transformation matrix `U` allows + specifying an alternative unit cell that can be used to generate + superlattices of the form `S = (L @ U) @ T`. If None, `U` is set to the + identity matrix. + diagonal_only: bool = False + If true, restrict :math:`T` to diagonal matrices. + fixed_shape: bool = False + If true, restrict `T` to diagonal matrices with diagonal coefficients + `[m, 1, 1]` (1d), `[m, m, 1]` (2d), or `[m, m, m]` (3d), + where the dimension is determined from ``len(dirs)``. Returns ------- diff --git a/python/tests/test_enumerate_superlattices.py b/python/tests/test_enumerate_superlattices.py index a4bbc15..183c81f 100644 --- a/python/tests/test_enumerate_superlattices.py +++ b/python/tests/test_enumerate_superlattices.py @@ -3,7 +3,7 @@ import libcasm.xtal as xtal -def test_enumerate_superlattices_simple_cubic_point_group(): +def test_enumerate_superlattices_simple_cubic_point_group_1(): unit_lattice = xtal.Lattice(np.eye(3).transpose()) point_group = xtal.make_point_group(unit_lattice) superlattices = xtal.enumerate_superlattices( @@ -12,6 +12,52 @@ def test_enumerate_superlattices_simple_cubic_point_group(): assert len(superlattices) == 16 +def test_enumerate_superlattices_simple_cubic_point_group_2(): + """Test diagonal_only parameter""" + unit_lattice = xtal.Lattice(np.eye(3).transpose()) + point_group = xtal.make_point_group(unit_lattice) + superlattices = xtal.enumerate_superlattices( + unit_lattice, + point_group, + max_volume=4, + min_volume=1, + dirs="abc", + diagonal_only=True, + ) + assert len(superlattices) == 5 + + +def test_enumerate_superlattices_simple_cubic_point_group_3(): + """Test diagonal_only & fixed_shape parameters""" + unit_lattice = xtal.Lattice(np.eye(3).transpose()) + point_group = xtal.make_point_group(unit_lattice) + superlattices = xtal.enumerate_superlattices( + unit_lattice, + point_group, + max_volume=10, + min_volume=1, + dirs="abc", + diagonal_only=True, + fixed_shape=True, + ) + assert len(superlattices) == 2 + + +def test_enumerate_superlattices_simple_cubic_point_group_4(): + """Test unit_cell parameter""" + unit_lattice = xtal.Lattice(np.eye(3).transpose()) + point_group = xtal.make_point_group(unit_lattice) + superlattices = xtal.enumerate_superlattices( + unit_lattice, + point_group, + max_volume=4, + min_volume=1, + dirs="abc", + unit_cell=np.array([[2, 0, 0], [0, 1, 0], [0, 0, 1]]), + ) + assert len(superlattices) == 26 + + def test_enumerate_superlattices_disp_1d_crystal_point_group(simple_cubic_1d_disp_prim): unit_lattice = xtal.Lattice(np.eye(3).transpose()) point_group = xtal.make_crystal_point_group(simple_cubic_1d_disp_prim) From 81b0359f3f08b144b90bd7f296364bb165d3a5cb Mon Sep 17 00:00:00 2001 From: bpuchala Date: Mon, 22 Jan 2024 10:22:56 -0500 Subject: [PATCH 06/11] doc fix --- python/doc/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/doc/index.rst b/python/doc/index.rst index 5fb2698..576e4a3 100644 --- a/python/doc/index.rst +++ b/python/doc/index.rst @@ -24,7 +24,7 @@ The libcasm-xtal package is the CASM crystallography module. This includes: About CASM ========== -The libcasm-global package is part of the CASM_ open source software package, which is designed to perform first-principles statistical mechanical studies of multi-component crystalline solids. +The libcasm-xtal package is part of the CASM_ open source software package, which is designed to perform first-principles statistical mechanical studies of multi-component crystalline solids. CASM is developed by the Van der Ven group, originally at the University of Michigan and currently at the University of California Santa Barbara. From 9d1b89fd43f4577126313417e61ded508e5005bb Mon Sep 17 00:00:00 2001 From: bpuchala Date: Fri, 26 Jan 2024 08:21:57 -0500 Subject: [PATCH 07/11] doc fix --- python/doc/usage/lattice.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/doc/usage/lattice.rst b/python/doc/usage/lattice.rst index 4858c74..5278e62 100644 --- a/python/doc/usage/lattice.rst +++ b/python/doc/usage/lattice.rst @@ -190,7 +190,7 @@ The ``==`` and ``!=`` operators can be used to check if two lattices have identi Lattice equivalence ------------------- -A lattice can be represented using any choice of lattice vectors that results in the same lattice points. The :func:`~libcasm.xtal.is_equivalent_to` method checks for the equivalence of lattices that do not have identical lattice vectors by determining if one choice of lattice vectors can be formed by linear combination of another choice of lattice vectors according to :math:`L_1 = L_2 U`, where :math:`L_1` and :math:`L_2` are the lattice vectors as columns of matrices, and :math:`U` is an integer matrix with :math:`\det(U) = \pm 1`: +A lattice can be represented using any choice of lattice vectors that results in the same lattice points. The :func:`~libcasm.xtal.Lattice.is_equivalent_to` method checks for the equivalence of lattices that do not have identical lattice vectors by determining if one choice of lattice vectors can be formed by linear combination of another choice of lattice vectors according to :math:`L_1 = L_2 U`, where :math:`L_1` and :math:`L_2` are the lattice vectors as columns of matrices, and :math:`U` is an integer matrix with :math:`\det(U) = \pm 1`: .. code-block:: Python From 56af057595bf6b21be17fa238ba13da389a5b39e Mon Sep 17 00:00:00 2001 From: bpuchala Date: Fri, 26 Jan 2024 08:26:19 -0500 Subject: [PATCH 08/11] moving structure conversions to a different package --- CHANGELOG.md | 2 - python/libcasm/xtal/convert/__init__.py | 45 -- python/libcasm/xtal/convert/pymatgen.py | 896 ------------------------ python/setup.py | 2 +- python/tests/convert/test_pymatgen.py | 503 ------------- setup.py | 2 +- 6 files changed, 2 insertions(+), 1448 deletions(-) delete mode 100644 python/libcasm/xtal/convert/__init__.py delete mode 100644 python/libcasm/xtal/convert/pymatgen.py delete mode 100644 python/tests/convert/test_pymatgen.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aad1ae..0f7d454 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,8 +27,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add to libcasm.xtal: substitute_structure_species - Add to libcasm.xtal.Prim: method labels, constructor parameter `labels` - Add to libcasm.xtal.Lattice: methods reciprocal, volume, lengths_and_angles, from_lengths_and_angles -- Add libcasm.xtal.convert package -- Add libcasm.xtal.convert.pymatgen ## Unreleased diff --git a/python/libcasm/xtal/convert/__init__.py b/python/libcasm/xtal/convert/__init__.py deleted file mode 100644 index 7b142da..0000000 --- a/python/libcasm/xtal/convert/__init__.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Data structure conversions - -Users of libcasm.xtal.convert should take care to double-check -that conversions are performed correctly! - -This package is a work in progress. Particularly for more -complicated structures or molecules, such as those with mixed -occupation, charge, spin, and magnetic moments, a limited number -or no test cases may exist. Users should double-check that -conversions are correct and make adjustments as needed! - -Notes ------ - -- This package is intended to work on plain old Python data structures, - meaning it should work without needing to import libcasm, pymatgen, - ase, etc. -- Use of standard Python modules and numpy is allowed - -""" - -import warnings - -warnings.warn( - """ - Users of libcasm.xtal.convert should take care to double-check - that conversions are performed correctly! - - This package is a work in progress. Particularly for more - complicated structures or molecules, such as those with mixed - occupation, charge, spin, and magnetic moments, a limited number - or no test cases may exist. Users should double-check that - conversions are correct and make adjustments as needed! - - Suppress this warning with: - - import warnings - warnings.simplefilter("ignore") - - Or by setting the PYTHONWARNINGS environment variable: - - export PYTHONWARNINGS="ignore" - - """ -) diff --git a/python/libcasm/xtal/convert/pymatgen.py b/python/libcasm/xtal/convert/pymatgen.py deleted file mode 100644 index 19a37a9..0000000 --- a/python/libcasm/xtal/convert/pymatgen.py +++ /dev/null @@ -1,896 +0,0 @@ -"""Convert between CASM and pymatgen dict formats (v2023.10.4)""" - -# Note: -# - This package is intended to work on plain old Python data structures, -# meaning it should work without needing to import libcasm, pymatgen, -# ase, etc. -# - Use of standard Python modules and numpy is allowed - -import copy -import re -from typing import Literal, Optional - -import numpy as np - - -def make_pymatgen_lattice_dict( - matrix: list[list[float]], - pbc: tuple[bool] = (True, True, True), -) -> dict: - """Create the pymatgen Lattice dict - - Parameters - ---------- - matrix: list[list[float]] - List of lattice vectors, (i.e. row-vector matrix of lattice vectors). - pbc: tuple[bool] = (True, True, True) - A tuple defining the periodic boundary conditions along the three - axes of the lattice. Default is periodic in all directions. This - is not stored in a CASM :class:`~_xtal.Lattice` and must be specified - if it should be anything other than periodic along all three axes. - - Returns - ------- - data: dict - The pymatgen dict representation of a lattice, with format: - - matrix: list[list[float]] - List of lattice vectors, (i.e. row-vector matrix of lattice vectors). - - pbc: tuple[bool] = (True, True, True) - A tuple defining the periodic boundary conditions along the three - axes of the lattice. Default is periodic in all directions. - - """ - return { - "matrix": matrix, - "pbc": pbc, - } - - -# TODO: -# - def make_pymatgen_molecule_dict(casm_occupant: dict) -> dict -# - def make_casm_occupant_dict(pymatgen_molecule: dict) -> dict - -# pymatgen.core.Site: dict -# species: list[dict] -# A list of species occupying the site, including occupation (float), and -# optional "idealized" (integer) oxidation state and spin. Calculated (float) -# charge and magnetic moments should be stored in Site.properties. -# -# Ex: -# -# [ -# { -# "element": "A", -# "occu": float, -# "oxidation_state": Optional[int], -# "spin": Optional[int], -# }, -# ... -# ] -# -# Note that pymatgen expects "element" is an actual element in the periodic -# table, or a dummy symbol which 'cannot have any part of first two letters that -# will constitute an Element symbol. Otherwise, a composition may be parsed -# wrongly. E.g., "X" is fine, but "Vac" is not because Vac contains V, a valid -# Element.'. -# -# xyz: list[float] -# Cartesian coordinates -# properties: Optional[dict] -# Properties associated with the site. Options include: "magmom", ? -# label: Optional[str] -# Label for site - -# pymatgen.core.IMolecule: -# sites: list[Site] -# charge: float = 0.0 -# Charge for the molecule. -# spin_multiplicity: Optional[int] = None -# properties: Optional[dict] = None -# Properties associated with the molecule as a whole. Options include: ? - -# def make_pymatgen_molecule_dict( -# casm_occupant: dict, -# ) -> dict: -# # charge = -# # spin_multiplicity = -# # sites = -# # properties = -# return { -# "charge": charge, -# "spin_multiplicity": spin_multiplicity, -# "sites": sites, -# "properties": properties, -# } - - -def copy_properties( - properties: dict, - rename_as: dict[str, str] = {}, - include_all: bool = True, -) -> dict: - """Copy a dictionary of properties, optionally renaming some - - Parameters - ---------- - properties: dict - The input properties - - rename_as: dict[str, str] = {} - A lookup table where the keys are keys in the input `in_properties` that should - be changed to the values in the output `out_properties`. - - include_all: bool = True - If True, all properties in the input `in_properties` are included in the output - `out_properties`. If False, only the properties found in `rename_as` are - included in the output. - - Returns - ------- - out_properties: dict - A copy of `in_properties`, with the specified renaming of keys. - - """ - out_properties = {} - for casm_key in properties: - if include_all is False: - if casm_key not in rename_as: - continue - new_key = rename_as.get(casm_key, casm_key) - out_properties[new_key] = copy.deepcopy(properties[casm_key]) - return out_properties - - -def make_pymatgen_structure_dict( - casm_structure: dict, - charge: Optional[float] = None, - pbc: tuple[bool] = (True, True, True), - atom_type_to_pymatgen_species_list: dict = {}, - atom_type_to_pymatgen_label: dict = {}, - casm_to_pymatgen_atom_properties: dict = {}, - include_all_atom_properties: bool = True, - casm_to_pymatgen_global_properties: dict = {}, - include_all_global_properties: bool = True, -) -> dict: - """Convert a CASM :class:`~_xtal.Structure` dict to a pymatgen IStructure dict - - Parameters - ---------- - structure: dict - The :class:`~_xtal.Structure`, represented as a dict, to be represented as a - pymatgen dict. Must be an atomic structure only. - - charge: Optional[float] = None - Overall charge of the structure. If None, when pymatgen constructs an - IStructure, default behavior is that the total charge is the sum of the - oxidation states (weighted by occupation) on each site. - - pbc: tuple[bool] = (True, True, True) - A tuple defining the periodic boundary conditions along the three - axes of the lattice. Default is periodic in all directions. This - is not stored in a CASM :class:`~_xtal.Lattice` and must be specified - if it should be anything other than periodic along all three axes. - - atom_type_to_pymatgen_species_list: dict = {} - A lookup table of CASM structure atom type (key) to pymatgen PeriodicSite - species list (value) representing the occupancy of the site. - - The pymatgen species list format is: - - .. code-block:: Python - - [ - { - "element": str, - "occu": float, - "oxidation_state": Optional[int], - "spin": Optional[int], - }, - ... - ] - - where: - - - "element": str, the element name, or a "dummy" species name - - "occu": float, the random site occupation - - "oxidation_state": Optional[int], the oxidation state, expected to - idealized to integer, e.g. -2, -1, 0, 1, 2, ... - - "spin: Optional[int], the spin associated with the species - - By default, atoms in the input structure will be represented using - ``[{ "element": atom_type, "occu": 1.0 }]``. - - atom_type_to_pymatgen_label: dict = {} - A lookup table of CASM structure atom type (key) to pymatgen PeriodicSite label - (value). If an atom_type in `casm_structure` is not found in the dict, then - the atom_type is used for the label in the output `pymatgen_structure`. - - casm_to_pymatgen_atom_properties: dict = {} - If a CASM structure atom property is found as a key in - `casm_to_pymatgen_atom_properties`, it is renamed in the pymatgen - PeriodicSite properties using the associated value. - - include_all_atom_properties: bool = True - If True (default), all atom properties in `structure` are included in the - result. If False , only `structure` atom properties found in - `atom_properties_to_pymatgen_properties` are included in the result. - - casm_to_pymatgen_global_properties: dict = {} - If a CASM structure global property is found as a key in - `casm_to_pymatgen_global_properties`, it is renamed in the pymatgen - structure properties using the associated value. - - include_all_global_properties: bool = True - If True (default), all global properties in `structure` are included in the - result. If False , only `structure` global properties found in - `global_properties_to_pymatgen_properties` are included in the result. - - Returns - ------- - data: dict - The pymatgen IStructure `dict` representation, with format: - - sites: list[PeriodicSite] - A list of PeriodicSite, with format: - - species: list[dict] - A list of dict as described for the input parameter - `atom_type_to_pymatgen_species_list`. - - abc: list[float] - Fractional coordinates of the site, relative to the lattice - vectors - - properties: Optional[dict] = None - Properties associated with the site as a dict, e.g. - ``{"magmom": 5}``. Obtained from - `casm_structure.atom_properties`. - - label: Optional[str] = None - Label for the site. Defaults to None. - - - charge: Optional[float] = None - Charge for the structure, expected to be equal to sum of oxidation - states. - - lattice: dict - Dict representation of a pymatgen Lattice, with format: - - matrix: list[list[float]] - List of lattice vectors, (i.e. row-vector matrix of lattice - vectors). - - pbc: tuple[bool] = (True, True, True) - A tuple defining the periodic boundary conditions along the - three axes of the lattice. Default is periodic in all - directions. - - properties: tuple[bool] = (True, True, True) - Properties associated with the structure as a whole. Options include: ? - - Raises - ------ - ValueError - For non-atomic structure, if ``"mol_type" in structure``. - """ - - if "mol_type" in casm_structure: - raise ValueError( - "Error: only atomic structures may be converted using to_structure_dict" - ) - - ### Reading casm_structure - - # required keys: "lattice_vectors", "atom_type", "coordinate_mode", "atom_coords" - lattice_column_vector_matrix = np.array( - casm_structure["lattice_vectors"] - ).transpose() - atom_type = casm_structure["atom_type"] - coordinate_mode = casm_structure["coordinate_mode"] - - if coordinate_mode in ["Fractional", "fractional", "FRAC", "Direct", "direct"]: - atom_coordinate_frac = np.array(casm_structure["atom_coords"]).transpose() - elif coordinate_mode in ["Cartesian", "cartesian", "CART"]: - atom_coordinate_cart = np.array(casm_structure["atom_coords"]).transpose() - atom_coordinate_frac = ( - np.linalg.pinv(lattice_column_vector_matrix) @ atom_coordinate_cart - ) - else: - raise Exception(f"Error: unrecognized coordinate_mode: {coordinate_mode}") - - atom_properties = {} - if "atom_properties" in casm_structure: - for key in casm_structure["atom_properties"]: - atom_properties[key] = np.array( - casm_structure["atom_properties"][key]["value"] - ).transpose() - - global_properties = {} - if "global_properties" in casm_structure: - for key in casm_structure["global_properties"]: - global_properties[key] = np.array( - casm_structure["global_properties"][key]["value"] - ).transpose() - - ### Convert to pymatgen dict - - # lattice - lattice = { - "matrix": lattice_column_vector_matrix.transpose().tolist(), - "pbc": pbc, - } - - # sites - sites = [] - for i, _atom_type in enumerate(atom_type): - # species list - default_species_list = [{"element": _atom_type, "occu": 1.0}] - species = atom_type_to_pymatgen_species_list.get( - _atom_type, default_species_list - ) - - # abc coordinate - abc = atom_coordinate_frac[:, i].tolist() - - # site properties - _atom_properties = {} - for key in atom_properties: - _atom_properties[key] = atom_properties[:, i] - properties = copy_properties( - properties=_atom_properties, - rename_as=casm_to_pymatgen_atom_properties, - include_all=include_all_atom_properties, - ) - - # label - label = atom_type_to_pymatgen_label.get(_atom_type, _atom_type) - - site = { - "species": species, - "abc": abc, - } - if label is not None: - site["label"] = label - if len(properties): - site["properties"] = properties - - sites.append(site) - - # properties - properties = copy_properties( - properties=global_properties, - rename_as=casm_to_pymatgen_global_properties, - include_all=include_all_global_properties, - ) - - pymatgen_structure = { - "lattice": lattice, - "sites": sites, - } - if charge is not None: - pymatgen_structure["charge"] = charge - if len(properties): - pymatgen_structure["properties"] = properties - - return pymatgen_structure - - -def make_casm_structure_dict( - pymatgen_structure: dict, - frac: bool = True, - atom_type_from: Literal["element", "label", "species_list"] = "element", - atom_type_to_pymatgen_species_list: dict = {}, - atom_type_to_pymatgen_label: dict = {}, - casm_to_pymatgen_atom_properties: dict = {}, - include_all_atom_properties: bool = True, - casm_to_pymatgen_global_properties: dict = {}, - include_all_global_properties: bool = True, -) -> dict: - """Convert a pymatgen IStructure dict to an atomic CASM :class:`~_xtal.Structure` - dict - - Notes - ----- - - - An atomic CASM :class:`~_xtal.Structure` only allows one species at each basis - site, whereas a pymatgen IStructure can allow a composition at each basis site. - - The species at each site in the resulting CASM structure can be determined using - one of several options chosen by a choice of the `atom_type_from` parameter. - - Parameters - ---------- - pymatgen_structure: dict - The pymatgen IStructure, represented as a dict, to be converted to a CASM - :class:`~_xtal.Structure` dict representation. Must be an atomic structure only. - - frac: bool = True - If True, coordinates in the result are expressed in fractional coordinates - relative to the lattice vectors. Otherwise, Cartesian coordinates are used. - - atom_type_from: Literal["element", "label", "species_list"] = "element" - Specifies which component of `pymatgen_structure` is used to determine the CASM - structure `atom_type`. Options are: - - - "element": Use PeriodicSite species element name to determine the CASM - `atom_type`. - - "label": Use PeriodicSite "label" and `atom_type_to_pymatgen_label` to - determine the CASM `atom_type`. With this option, a "label" must exist in - the pymatgen PeriodicSite dict. By default, the label found is used for the - atom type in the resulting CASM structure. If the label found is a value in - the `atom_type_to_pymatgen_label` dict, then the associated key is used for - the atom type in the resulting CASM structure. - - "species_list": Use PeriodicSite species list to determine the CASM - `atom_type` from the `atom_type_to_pymatgen_species_list` dict. The species - list must compare equal to a value in the `atom_type_to_pymatgen_species_list` - dict or the default value ``[{ "element": atom_type, "occu": 1.0 }]``, - otherwise an exception will be raised. - - atom_type_to_pymatgen_species_list: dict = {} - A lookup table of CASM structure atom type (key) to pymatgen PeriodicSite - species list (value) representing the occupancy of the site. - - The pymatgen species list format is: - - .. code-block:: Python - - [ - { - "element": str, - "occu": float, - "oxidation_state": Optional[int], - "spin": Optional[int], - }, - ... - ] - - where: - - - "element": str, the element name, or a "dummy" species name - - "occu": float, the random site occupation - - "oxidation_state": Optional[int], the oxidation state, expected to - idealized to integer, e.g. -2, -1, 0, 1, 2, ... - - "spin: Optional[int], the spin associated with the species - - By default, atoms in the input structure will be represented using - ``[{ "element": atom_type, "occu": 1.0 }]``. - - atom_type_to_pymatgen_label: dict = {} - A lookup table of CASM structure atom type (key) to pymatgen PeriodicSite label - (value). - - casm_to_pymatgen_atom_properties: dict = {} - If a CASM structure atom property is found as a key in - `casm_to_pymatgen_atom_properties`, it is renamed in the pymatgen - PeriodicSite properties using the associated value. - - include_all_atom_properties: bool = True - If True (default), all atom properties in `structure` are included in the - result. If False , only `structure` atom properties found in - `atom_properties_to_pymatgen_properties` are included in the result. - - casm_to_pymatgen_global_properties: dict = {} - If a CASM structure global property is found as a key in - `casm_to_pymatgen_global_properties`, it is renamed in the pymatgen - structure properties using the associated value. - - include_all_global_properties: bool = True - If True (default), all global properties in `structure` are included in the - result. If False , only `structure` global properties found in - `global_properties_to_pymatgen_properties` are included in the result. - - Returns - ------- - data: dict - The CASM Structure `dict` representation, with format described - `here `_. - """ - - ### Reading pymatgen_structure - - # required keys: "lattice_vectors", "atom_type", "coordinate_mode", "atom_coords" - lattice = pymatgen_structure["lattice"]["matrix"] - # ? charge = pymatgen_structure.get("charge", None) - sites = pymatgen_structure.get("sites", []) - - n_sites = len(sites) - atom_type = [] - atom_coordinate_frac = np.zeros((3, n_sites)) - atom_properties = {} - for i, site in enumerate(sites): - # "species" / "abc" / "label" / "properties" - - # Determine the CASM atom_type associated with this site. The method - # depends on the choice of the `atom_type_from`. - if atom_type_from == "element": - if len(site["species"]) != 1: - raise Exception( - f"Error: multiple species on site {i}, which is not " - f'allowed with atom_type_from=="element"' - ) - if "element" not in site["species"][0]: - raise Exception( - f"Error: element not found for site {i}, " - f'which is not allowed with atom_type_from=="element"' - ) - atom_type.append(site["species"][0]["element"]) - elif atom_type_from == "label": - if "label" not in site: - raise Exception( - f"Error: no label found on site {i}, which is not allowed " - f'with atom_type_from=="label"' - ) - label = site.get("label") - if label is None: - raise Exception( - f"Error: label is null for site {i}, " - f'which is not allowed with atom_type_from=="label"' - ) - _atom_type = None - for key, value in atom_type_to_pymatgen_label.items(): - if value == label: - _atom_type = key - break - if _atom_type is None: - _atom_type = label - atom_type.append(_atom_type) - elif atom_type_from == "species_list": - _atom_type = None - for key, value in atom_type_to_pymatgen_species_list.items(): - if value == site["species"]: - _atom_type = key - break - if _atom_type is None: - if len(site["species"]) != 1: - raise Exception( - f'Error: no match found for the "species" list on site {i}, "' - f'which is not allowed with atom_type_from=="species_list"' - ) - element = site["species"][0].get("element", None) - default_species_list = [{"element": element, "occu": 1.0}] - if element is None or site["species"] != default_species_list: - raise Exception( - f'Error: no match found for the "species" list on site {i}, "' - f'which is not allowed with atom_type_from=="species_list"' - ) - _atom_type = element - atom_type.append(_atom_type) - else: - raise Exception(f"Error: invalid atom_type_from=={atom_type_from}") - - atom_coordinate_frac[:, i] = np.array(site["abc"]) - - if "properties" in site: - for key in site["properties"]: - _value = site["properties"][key] - if isinstance(_value, [float, int]): - value = np.array([_value], dtype="float") - elif isinstance(_value, list): - value = np.array([_value], dtype="float") - else: - raise Exception( - f"Error: unsupported site properties: {str(_value)}" - ) - if len(value.shape) != 1: - raise Exception( - "Error: only scalar and vector site properties are supported" - ) - if key not in atom_properties: - atom_properties[key]["value"] = np.zeros((value.size, n_sites)) - atom_properties[key]["value"][:, i] = value - - global_properties = {} - if "properties" in pymatgen_structure: - for key in pymatgen_structure["properties"]: - _value = pymatgen_structure["properties"][key] - if isinstance(_value, [float, int]): - value = np.array([_value], dtype="float") - elif isinstance(_value, list): - value = np.array([_value], dtype="float") - else: - raise Exception( - f"Error: unsupported global properties, {str(key)}:{str(_value)}" - ) - if len(value.shape) != 1: - raise Exception( - "Error: only scalar and vector global properties are supported" - ) - global_properties[key]["value"] = value - - ### Convert to casm dict - - # ? "charge" - - if frac is True: - coordinate_mode = "Fractional" - atom_coords = atom_coordinate_frac - else: - coordinate_mode = "Cartesian" - column_vector_matrix = np.array(lattice).transpose() - atom_coords = column_vector_matrix @ atom_coordinate_frac - - atom_properties = copy_properties( - properties=atom_properties, - rename_as={ - value: key for key, value in casm_to_pymatgen_atom_properties.items() - }, - include_all=include_all_atom_properties, - ) - - global_properties = copy_properties( - properties=global_properties, - rename_as={ - value: key for key, value in casm_to_pymatgen_global_properties.items() - }, - include_all=include_all_global_properties, - ) - - casm_structure = { - "lattice_vectors": lattice, - "coordinate_mode": coordinate_mode, - "atom_coords": atom_coords.transpose().tolist(), - "atom_type": atom_type, - } - if len(atom_properties): - casm_structure["atom_properties"] = atom_properties - if len(global_properties): - casm_structure["global_properties"] = global_properties - - return casm_structure - - -def make_casm_prim_dict( - pymatgen_structure: dict, - title: str = "Prim", - description: Optional[str] = None, - frac: bool = True, - occupant_names_from: Literal["element", "species"] = "element", - occupant_name_to_pymatgen_species: dict = {}, - occupant_sets: Optional[list[set[str]]] = None, - use_site_labels: bool = False, - casm_species: dict = {}, - site_dof: dict = {}, - global_dof: dict = {}, -) -> dict: - """Use a pymatgen IStructure dict to construct a CASM :class:`~_xtal.Prim` dict - - Notes - ----- - - - The allowed occupants on each site in the resulting CASM prim is determined from - the elements on each site in the pymatgen structure. - - This method does not attempt to use site properties or structure properties - - Parameters - ---------- - pymatgen_structure: dict - The pymatgen IStructure, represented as a dict, to be converted to a CASM - :class:`~_xtal.Structure` dict representation. Must be an atomic structure only. - - title: str = "Prim" - A title for the project. For use by CASM, should consist of alphanumeric - characters and underscores only. The first character may not be a number. - - description: Optional[str] = None - An extended description for the project. Included by convention in most example - prim files, this attribute is not read by CASM. - - frac: bool = True - If True, coordinates in the result are expressed in fractional coordinates - relative to the lattice vectors. Otherwise, Cartesian coordinates are used. - - occupant_names_from: Literal["element", "species"] = "element" - Specifies which component of `pymatgen_structure` is used to determine the CASM - prim basis site occupant names. Options are: - - - "element": Use PeriodicSite species element name to determine the CASM - occupant names. - - "species": Use PeriodicSite species dicts to determine the CASM - occupant names from the `occupant_name_to_pymatgen_species` dict. The species - dict, excluding "occu", must compare equal to a value in the - `occupant_name_to_pymatgen_species` dict or the default value - ``[{ "element": occupant_name }]``, otherwise an exception will be raised. - - If a pymatgen site has mixed occupation, the corresponding CASM prim basis site - will have multiple allowed occupants. Additionally, the `occupant_sets` - parameter may be used to specify that the presence of one type of occupant - specifies that a particular set of occupants should be allowed on the - corresponding CASM prim basis sites. - - occupant_name_to_pymatgen_species: dict = {} - A lookup table of CASM occupant name (key) to pymatgen PeriodicSite - species (value) representing the occupancy of the site. - - The pymatgen species format is: - - .. code-block:: Python - - { - "element": str, - "oxidation_state": Optional[Union[int, float]], - "spin": Optional[Union[int, float]], - } - - where: - - - "element": str, the element name, or a "dummy" species name - - "oxidation_state": Optional[Union[int, float]], the oxidation state, expected - to be idealized to integer, e.g. -2, -1, 0, 1, 2, ... - - "spin: Optional[Union[int, float]], the spin associated with the species - - Note that the "occu" part of the species representation on a PeriodicSite is - ignored by this method. The pymatgen documentation / implementation seem - inconsistent whether these should be integer or float; for purposes of - this method they just must compare exactly. - - occupant_sets: Optional[list[set[str]]] = None, - Optional sets of occupants that should be allowed on the same sites. - - For example, if ``occupant_sets == [set(["A", "B"]), set(["C", "D"])]``, then - any site where the pymatgen structure has either "A" or "B" occupation, the CASM - prim basis site occupants will be expanded to include both "A" and "B", and any - site where the pymatgen structure has either "C" or "D" occupation, the CASM - prim basis site occupants will be expanded to include both "C" and "D". - - use_site_labels: bool = False - If True, set CASM prim basis site labels based on the pymatgen structure site - labels. If False, do not set prim basis site labels. CASM prim basis site labels - are an integer, greater than or equal to zero, that if provided distinguishes - otherwise identical sites. - - casm_species: dict = {}, - A dictionary used to define fixed properties of any species listed as an allowed - occupant that is not a single isotropic atom. This parameter is filtered to - only include occupants found in the input, then copied to the "species" - attribute of the output prim dict. - - site_dof: dict = {} - A dictionary specifying the types of continuous site degrees of freedom (DoF) - allowed on every basis site. Note that CASM supports having different DoF on - each basis site, but this method currently does not. - - global_dof: dict = {} - A dictionary specifying the types of continuous global degrees of freedom (DoF) - and their basis. - - Returns - ------- - data: dict - The CASM Prim `dict` representation, with format described - `here `_. - Site occupants are sorted in alphabetical order. - """ - - # validate title - if not re.match("^[a-zA-Z]\w*$", title): - raise Exception(f"Error: invalid title: {title}") - - ### Reading pymatgen_structure - - # required keys: "lattice_vectors", "atom_type", "coordinate_mode", "atom_coords" - lattice = pymatgen_structure["lattice"]["matrix"] - # ? charge = pymatgen_structure.get("charge", None) - sites = pymatgen_structure.get("sites", []) - - n_sites = len(sites) - occupants = [] - site_coordinate_frac = np.zeros((3, n_sites)) - label = [] - for i, site in enumerate(sites): - # "species" / "abc" / "label" / "properties" - - # Determine the CASM occupant names associated with this site. The method - # depends on the choice of the `occupant_names_from`. - if occupant_names_from == "element": - site_occupants = [] - for j, species in enumerate(site["species"]): - element = species.get("element", None) - if element is None: - raise Exception( - f'Error: no element for species {j} on site {i}, which is not "' - f'allowed with occupant_names_from=="element"' - ) - site_occupants.append(element) - occupants.append(site_occupants) - elif occupant_names_from == "species": - site_occupants = [] - for j, species in enumerate(site["species"]): - occupant_name = None - _species = copy.deepcopy(species) - if "occu" in _species: - del _species["occu"] - # check occupant_name_to_pymatgen_species - for key, value in occupant_name_to_pymatgen_species: - _value = copy.deepcopy(value) - if "occu" in _value: - del _value["occu"] - if value == _species: - occupant_name = key - break - # check default, occupant_name == element (no oxidation_state, no spin) - if occupant_name is None: - element = species.get("element", None) - default_species = [{"element": element}] - if _species == default_species: - occupant_name = element - # if still not found, raise - if occupant_name is None: - raise Exception( - f'Error: no match found for species {j} on site {i}, which is "' - f'not allowed with occupant_names_from=="species"' - ) - site_occupants.append(occupant_name) - occupants.append(site_occupants) - else: - raise Exception( - f"Error: invalid occupant_names_from=={occupant_names_from}" - ) - - site_coordinate_frac[:, i] = np.array(site["abc"]) - label.append(site.get("label", None)) - - ### Convert to casm dict - - # ? "charge" - - # Get coordinate mode and coordinates - if frac is True: - coordinate_mode = "Fractional" - site_coords = site_coordinate_frac - else: - coordinate_mode = "Cartesian" - column_vector_matrix = np.array(lattice).transpose() - site_coords = column_vector_matrix @ site_coordinate_frac - - # Expand site occupants based on occupant_sets - if occupant_sets is not None: - for i, site_occupants in enumerate(occupants): - _site_occupants = set(site_occupants) - - for occ_name in site_occupants: - for occ_set in occupant_sets: - if occ_name in occ_set: - _site_occupants.update(occ_set) - - occupants[i] = sorted(list(_site_occupants)) - - # Get entries in casm_species that are actual occupants in the resulting prim - filtered_casm_species = {} - for site_occupants in occupants: - for occupant_name in site_occupants: - if occupant_name in casm_species: - filtered_casm_species[occupant_name] = copy.deepcopy( - casm_species[occupant_name] - ) - - # Construct prim basis list - basis = [] - distinct_site_labels = list(set(label)) - for i in range(n_sites): - basis_site = { - "coordinate": site_coords[:, i].tolist(), - "occupants": occupants[i], - } - if use_site_labels: - basis_site["label"] = distinct_site_labels.index(label[i]) - if len(site_dof): - basis_site["dofs"] = (copy.deepcopy(site_dof),) - basis.append(basis_site) - - # Construct prim dict - prim = { - "title": title, - "lattice_vectors": lattice, - "coordinate_mode": coordinate_mode, - "basis": basis, - } - if description is not None: - prim["description"] = description - if len(global_dof): - prim["dofs"] = copy.deepcopy(global_dof) - if len(filtered_casm_species): - prim["species"] = filtered_casm_species - - return prim diff --git a/python/setup.py b/python/setup.py index 54b61b9..07d2aa0 100644 --- a/python/setup.py +++ b/python/setup.py @@ -70,7 +70,7 @@ setup( name="libcasm-xtal", version=__version__, - packages=["libcasm", "libcasm.xtal", "libcasm.xtal.convert"], + packages=["libcasm", "libcasm.xtal"], install_requires=["pybind11", "libcasm-global>=2.0.2"], ext_modules=ext_modules, cmdclass={"build_ext": build_ext}, diff --git a/python/tests/convert/test_pymatgen.py b/python/tests/convert/test_pymatgen.py deleted file mode 100644 index a26f68e..0000000 --- a/python/tests/convert/test_pymatgen.py +++ /dev/null @@ -1,503 +0,0 @@ -import math - -import numpy as np -import pytest - -import libcasm.xtal.convert.pymatgen as convert - -# Tests should work with or without libcasm.xtal -try: - import libcasm.xtal as xtal - - import_libcasm_xtal = True -except ImportError: - import_libcasm_xtal = False - -# Tests should work with or without pymatgen -try: - from pymatgen.core import IStructure - - import_pymatgen = True -except ImportError: - import_pymatgen = False - - -def pretty_print(data): - if import_libcasm_xtal: - print(xtal.pretty_json(data)) - else: - import json - - print(json.dumps(data, indent=2)) - - -def test_pymatgen(): - """This just prints a message if pymatgen is not installed that we will be - skipping some checks""" - if not import_pymatgen: - pytest.skip("Skipping checks that require pymatgen") - - -def test_pymatgen_compare(): - """Warning: order matters in comparison of IStructure - - For pymatgen v2023.11.12 the following asserts pass: - """ - if import_pymatgen: - lattice_vectors = [[5.692, 0.0, 0.0], [0.0, 5.692, 0.0], [0.0, 0.0, 5.692]] - coords_frac = [ - [0.0, 0.0, 0.0], - [0.0, 0.5, 0.5], - [0.5, 0.0, 0.5], - [0.5, 0.5, 0.0], - [0.5, 0.5, 0.5], - [0.5, 0.0, 0.0], - [0.0, 0.5, 0.0], - [0.0, 0.0, 0.5], - ] - element_structure = IStructure( - lattice=lattice_vectors, - species=["Na"] * 4 + ["Cl"] * 4, - coords=coords_frac, - coords_are_cartesian=False, - ) - species_structure = IStructure( - lattice=lattice_vectors, - species=["Na+"] * 4 + ["Cl-"] * 4, - coords=coords_frac, - coords_are_cartesian=False, - ) - assert element_structure != species_structure - assert species_structure == element_structure - - -def test_BCC_Fe_element(): - ### Make pymatgen structure from CASM structure - casm_structure = { - "atom_coords": [[0.0, 0.0, 0.0]], - "atom_type": ["Fe"], - "coordinate_mode": "Fractional", - "lattice_vectors": [ - [-1.1547005383792517, 1.1547005383792517, 1.1547005383792517], - [1.1547005383792517, -1.1547005383792517, 1.1547005383792517], - [1.1547005383792517, 1.1547005383792517, -1.1547005383792517], - ], - } - if import_libcasm_xtal: - bcc_casm_structure = xtal.Structure.from_dict(casm_structure) - assert isinstance(bcc_casm_structure, xtal.Structure) - - # Check pymatgen as_dict -> from_dict - if import_pymatgen: - bcc_pymatgen_structure = IStructure( - lattice=casm_structure["lattice_vectors"], - species=casm_structure["atom_type"], - coords=casm_structure["atom_coords"], - coords_are_cartesian=False, - ) - # pretty_print(bcc_pymatgen_structure.as_dict()) - - # Check libcasm.xtal.convert.to_pymatgen_structure_dict - pymatgen_structure = convert.make_pymatgen_structure_dict(casm_structure) - # pretty_print(pymatgen_structure) - - assert len(pymatgen_structure) == 2 - assert np.allclose( - np.array(pymatgen_structure["lattice"]["matrix"]), - np.array(casm_structure["lattice_vectors"]), - ) - assert pymatgen_structure["lattice"]["pbc"] == (True, True, True) - assert "charge" not in pymatgen_structure - assert "properties" not in pymatgen_structure - assert len(pymatgen_structure["sites"]) == 1 - - ## site 0 - site = pymatgen_structure["sites"][0] - assert len(site) == 3 - assert np.allclose(site["abc"], np.array([0.0, 0.0, 0.0])) - assert site["label"] == "Fe" - assert "properties" not in site - assert len(site["species"]) == 1 - - # species 0 - species = site["species"][0] - assert species["element"] == "Fe" - assert math.isclose(species["occu"], 1.0) - assert "oxidation_state" not in species - - if import_pymatgen: - bcc_pymatgen_structure_in = IStructure.from_dict(pymatgen_structure) - assert bcc_pymatgen_structure_in == bcc_pymatgen_structure - assert bcc_pymatgen_structure == bcc_pymatgen_structure_in - - ### Make CASM structure from pymatgen structure - casm_structure_2 = convert.make_casm_structure_dict( - pymatgen_structure=pymatgen_structure, - frac=True, - atom_type_from="element", - ) - # pretty_print(casm_structure_2) - assert len(casm_structure_2) == 4 - assert np.allclose( - np.array(casm_structure_2["lattice_vectors"]), - np.array(casm_structure["lattice_vectors"]), - ) - assert casm_structure_2["atom_type"] == casm_structure["atom_type"] - assert np.allclose( - np.array(casm_structure_2["atom_coords"]), - np.array(casm_structure["atom_coords"]), - ) - assert casm_structure_2["coordinate_mode"] == "Fractional" - assert "atom_properties" not in casm_structure_2 - assert "global_properties" not in casm_structure_2 - - if import_libcasm_xtal: - bcc_casm_structure_2 = xtal.Structure.from_dict(casm_structure) - assert bcc_casm_structure.is_equivalent_to(bcc_casm_structure_2) - - ### Make CASM prim from pymatgen structure - casm_prim = convert.make_casm_prim_dict( - pymatgen_structure=pymatgen_structure, - frac=True, - occupant_names_from="element", - occupant_sets=[set(["Fe", "Cr"])], - ) - # pretty_print(casm_prim) - assert len(casm_prim) == 4 - assert "title" in casm_prim - assert "description" not in casm_prim - assert np.allclose( - np.array(casm_prim["lattice_vectors"]), - np.array(casm_structure["lattice_vectors"]), - ) - assert casm_prim["coordinate_mode"] == "Fractional" - assert "dofs" not in casm_prim - - assert len(casm_prim["basis"]) == 1 - basis_site = casm_prim["basis"][0] - assert len(basis_site) == 2 - assert np.allclose( - np.array(basis_site["coordinate"]), np.array(casm_structure["atom_coords"][0]) - ) - assert basis_site["occupants"] == ["Cr", "Fe"] - - if import_libcasm_xtal: - bcc_casm_prim = xtal.Prim.from_dict(casm_prim) - assert isinstance(bcc_casm_prim, xtal.Prim) - - -def test_NaCl_species_1(): - # This example constructs a CASM structure without oxidation states - # and uses atom_type_to_pymatgen_species_list - casm_structure = { - "atom_coords": [ - [0.0, 0.0, 0.0], - [0.0, 0.5, 0.5], - [0.5, 0.0, 0.5], - [0.5, 0.5, 0.0], - [0.5, 0.5, 0.5], - [0.5, 0.0, 0.0], - [0.0, 0.5, 0.0], - [0.0, 0.0, 0.5], - ], - "atom_type": ["Na", "Na", "Na", "Na", "Cl", "Cl", "Cl", "Cl"], - "coordinate_mode": "Fractional", - "lattice_vectors": [[5.692, 0.0, 0.0], [0.0, 5.692, 0.0], [0.0, 0.0, 5.692]], - } - atom_type_to_pymatgen_species_list = { - "Na": [{"element": "Na", "oxidation_state": 1, "occu": 1}], - "Cl": [{"element": "Cl", "oxidation_state": -1, "occu": 1}], - } - pymatgen_species = ["Na+"] * 4 + ["Cl-"] * 4 - - if import_libcasm_xtal: - NaCl_casm_structure = xtal.Structure.from_dict(casm_structure) - assert isinstance(NaCl_casm_structure, xtal.Structure) - - if import_pymatgen: - NaCl_pymatgen_structure = IStructure( - lattice=casm_structure["lattice_vectors"], - species=pymatgen_species, - coords=casm_structure["atom_coords"], - coords_are_cartesian=False, - ) - # pretty_print(NaCl_pymatgen_structure.as_dict()) - - # Check libcasm.xtal.convert.to_pymatgen_structure_dict - pymatgen_structure = convert.make_pymatgen_structure_dict( - casm_structure, - atom_type_to_pymatgen_species_list=atom_type_to_pymatgen_species_list, - ) - # pretty_print(pymatgen_structure) - - assert len(pymatgen_structure) == 2 - assert np.allclose( - np.array(pymatgen_structure["lattice"]["matrix"]), - np.array(casm_structure["lattice_vectors"]), - ) - assert pymatgen_structure["lattice"]["pbc"] == (True, True, True) - assert "charge" not in pymatgen_structure - assert "properties" not in pymatgen_structure - assert len(pymatgen_structure["sites"]) == 8 - - for i, site in enumerate(pymatgen_structure["sites"]): - # Na+ sites: - if i < 4: - assert len(site) == 3 - assert np.allclose(site["abc"], np.array(casm_structure["atom_coords"][i])) - assert len(site["species"]) == 1 - assert site["label"] == "Na" - - # species 0 - species = site["species"][0] - assert species["element"] == "Na" - assert math.isclose(species["occu"], 1) - assert math.isclose(species["oxidation_state"], 1) - - # Cl- sites: - else: - assert len(site) == 3 - assert np.allclose(site["abc"], np.array(casm_structure["atom_coords"][i])) - assert len(site["species"]) == 1 - assert site["label"] == "Cl" - - # species 0 - species = site["species"][0] - assert species["element"] == "Cl" - assert math.isclose(species["occu"], 1) - assert math.isclose(species["oxidation_state"], -1) - - if import_pymatgen: - NaCl_pymatgen_structure_in = IStructure.from_dict(pymatgen_structure) - # pretty_print(NaCl_pymatgen_structure_in.as_dict()) - assert NaCl_pymatgen_structure_in == NaCl_pymatgen_structure - assert NaCl_pymatgen_structure == NaCl_pymatgen_structure_in - - ### Make CASM structure from pymatgen structure - casm_structure_2 = convert.make_casm_structure_dict( - pymatgen_structure=pymatgen_structure, - frac=True, - atom_type_from="species_list", - atom_type_to_pymatgen_species_list=atom_type_to_pymatgen_species_list, - ) - # pretty_print(casm_structure_2) - assert len(casm_structure_2) == 4 - assert np.allclose( - np.array(casm_structure_2["lattice_vectors"]), - np.array(casm_structure["lattice_vectors"]), - ) - assert casm_structure_2["atom_type"] == casm_structure["atom_type"] - assert np.allclose( - np.array(casm_structure_2["atom_coords"]), - np.array(casm_structure["atom_coords"]), - ) - assert casm_structure_2["coordinate_mode"] == "Fractional" - assert "atom_properties" not in casm_structure_2 - assert "global_properties" not in casm_structure_2 - - if import_libcasm_xtal: - NaCl_casm_structure_2 = xtal.Structure.from_dict(casm_structure) - assert NaCl_casm_structure.is_equivalent_to(NaCl_casm_structure_2) - - ### Make CASM prim from pymatgen structure - casm_prim = convert.make_casm_prim_dict( - pymatgen_structure=pymatgen_structure, - frac=True, - occupant_names_from="element", - occupant_sets=[set(["Na", "Va"]), set(["Cl", "Va"])], - ) - # pretty_print(casm_prim) - assert len(casm_prim) == 4 - assert "title" in casm_prim - assert "description" not in casm_prim - assert np.allclose( - np.array(casm_prim["lattice_vectors"]), - np.array(casm_structure["lattice_vectors"]), - ) - assert casm_prim["coordinate_mode"] == "Fractional" - assert "dofs" not in casm_prim - - assert len(casm_prim["basis"]) == 8 - - for i, basis_site in enumerate(casm_prim["basis"]): - # Na+ sites: - if i < 4: - assert len(basis_site) == 2 - assert np.allclose( - np.array(basis_site["coordinate"]), - np.array(casm_structure["atom_coords"][i]), - ) - assert basis_site["occupants"] == ["Na", "Va"] - - # Cl- sites: - else: - assert len(basis_site) == 2 - assert np.allclose( - np.array(basis_site["coordinate"]), - np.array(casm_structure["atom_coords"][i]), - ) - assert basis_site["occupants"] == ["Cl", "Va"] - - if import_libcasm_xtal: - NaCl_casm_prim = xtal.Prim.from_dict(casm_prim) - assert isinstance(NaCl_casm_prim, xtal.Prim) - - -def test_NaCl_species_2(): - # This example uses atom_type_to_pymatgen_label to set labels and then also - # tests using atom_types_from=="label" converting the pymatgen structure to a - # casm structure - casm_structure = { - "atom_coords": [ - [0.0, 0.0, 0.0], - [0.0, 0.5, 0.5], - [0.5, 0.0, 0.5], - [0.5, 0.5, 0.0], - [0.5, 0.5, 0.5], - [0.5, 0.0, 0.0], - [0.0, 0.5, 0.0], - [0.0, 0.0, 0.5], - ], - "atom_type": ["Na", "Na", "Na", "Na", "Cl", "Cl", "Cl", "Cl"], - "coordinate_mode": "Fractional", - "lattice_vectors": [[5.692, 0.0, 0.0], [0.0, 5.692, 0.0], [0.0, 0.0, 5.692]], - } - atom_type_to_pymatgen_species_list = { - "Na": [{"element": "Na", "oxidation_state": 1, "occu": 1}], - "Cl": [{"element": "Cl", "oxidation_state": -1, "occu": 1}], - } - atom_type_to_pymatgen_label = {"Na": "Na+", "Cl": "Cl-"} - pymatgen_species = ["Na+"] * 4 + ["Cl-"] * 4 - - if import_libcasm_xtal: - NaCl_casm_structure = xtal.Structure.from_dict(casm_structure) - assert isinstance(NaCl_casm_structure, xtal.Structure) - - if import_pymatgen: - NaCl_pymatgen_structure = IStructure( - lattice=casm_structure["lattice_vectors"], - species=pymatgen_species, - coords=casm_structure["atom_coords"], - coords_are_cartesian=False, - ) - # pretty_print(NaCl_pymatgen_structure.as_dict()) - - # Check libcasm.xtal.convert.to_pymatgen_structure_dict - pymatgen_structure = convert.make_pymatgen_structure_dict( - casm_structure, - atom_type_to_pymatgen_species_list=atom_type_to_pymatgen_species_list, - atom_type_to_pymatgen_label=atom_type_to_pymatgen_label, - ) - # pretty_print(pymatgen_structure) - - assert len(pymatgen_structure) == 2 - assert np.allclose( - np.array(pymatgen_structure["lattice"]["matrix"]), - np.array(casm_structure["lattice_vectors"]), - ) - assert pymatgen_structure["lattice"]["pbc"] == (True, True, True) - assert "charge" not in pymatgen_structure - assert "properties" not in pymatgen_structure - assert len(pymatgen_structure["sites"]) == 8 - - for i, site in enumerate(pymatgen_structure["sites"]): - # Na+ sites: - if i < 4: - assert len(site) == 3 - assert np.allclose(site["abc"], np.array(casm_structure["atom_coords"][i])) - assert len(site["species"]) == 1 - assert site["label"] == "Na+" - - # species 0 - species = site["species"][0] - assert species["element"] == "Na" - assert math.isclose(species["occu"], 1) - assert math.isclose(species["oxidation_state"], 1) - - # Cl- sites: - else: - assert len(site) == 3 - assert np.allclose(site["abc"], np.array(casm_structure["atom_coords"][i])) - assert len(site["species"]) == 1 - assert site["label"] == "Cl-" - - # species 0 - species = site["species"][0] - assert species["element"] == "Cl" - assert math.isclose(species["occu"], 1) - assert math.isclose(species["oxidation_state"], -1) - - if import_pymatgen: - NaCl_pymatgen_structure_in = IStructure.from_dict(pymatgen_structure) - # pretty_print(NaCl_pymatgen_structure_in.as_dict()) - assert NaCl_pymatgen_structure_in == NaCl_pymatgen_structure - assert NaCl_pymatgen_structure == NaCl_pymatgen_structure_in - - ### Make CASM structure from pymatgen structure - casm_structure_2 = convert.make_casm_structure_dict( - pymatgen_structure=pymatgen_structure, - frac=True, - atom_type_from="label", - atom_type_to_pymatgen_label=atom_type_to_pymatgen_label, - ) - # pretty_print(casm_structure_2) - assert len(casm_structure_2) == 4 - assert np.allclose( - np.array(casm_structure_2["lattice_vectors"]), - np.array(casm_structure["lattice_vectors"]), - ) - assert casm_structure_2["atom_type"] == casm_structure["atom_type"] - assert np.allclose( - np.array(casm_structure_2["atom_coords"]), - np.array(casm_structure["atom_coords"]), - ) - assert casm_structure_2["coordinate_mode"] == "Fractional" - assert "atom_properties" not in casm_structure_2 - assert "global_properties" not in casm_structure_2 - - if import_libcasm_xtal: - NaCl_casm_structure_2 = xtal.Structure.from_dict(casm_structure) - assert NaCl_casm_structure.is_equivalent_to(NaCl_casm_structure_2) - - ### Make CASM prim from pymatgen structure - casm_prim = convert.make_casm_prim_dict( - pymatgen_structure=pymatgen_structure, - frac=True, - occupant_names_from="element", - occupant_sets=[set(["Na", "Va"]), set(["Cl", "Va"])], - ) - # pretty_print(casm_prim) - assert len(casm_prim) == 4 - assert "title" in casm_prim - assert "description" not in casm_prim - assert np.allclose( - np.array(casm_prim["lattice_vectors"]), - np.array(casm_structure["lattice_vectors"]), - ) - assert casm_prim["coordinate_mode"] == "Fractional" - assert "dofs" not in casm_prim - - assert len(casm_prim["basis"]) == 8 - - for i, basis_site in enumerate(casm_prim["basis"]): - # Na+ sites: - if i < 4: - assert len(basis_site) == 2 - assert np.allclose( - np.array(basis_site["coordinate"]), - np.array(casm_structure["atom_coords"][i]), - ) - assert basis_site["occupants"] == ["Na", "Va"] - - # Cl- sites: - else: - assert len(basis_site) == 2 - assert np.allclose( - np.array(basis_site["coordinate"]), - np.array(casm_structure["atom_coords"][i]), - ) - assert basis_site["occupants"] == ["Cl", "Va"] - - if import_libcasm_xtal: - NaCl_casm_prim = xtal.Prim.from_dict(casm_prim) - assert isinstance(NaCl_casm_prim, xtal.Prim) diff --git a/setup.py b/setup.py index 3b989bf..aebf442 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name="libcasm-xtal", version="2.0a8", - packages=["libcasm", "libcasm.xtal", "libcasm.xtal.convert"], + packages=["libcasm", "libcasm.xtal"], package_dir={"": "python"}, cmake_install_dir="python/libcasm", include_package_data=False, From 3184d67f2350ad2d82c7ce7be2c5081933e71258 Mon Sep 17 00:00:00 2001 From: bpuchala Date: Fri, 26 Jan 2024 08:51:44 -0500 Subject: [PATCH 09/11] Update CHANGELOG.md --- CHANGELOG.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f7d454..0a0b2b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [2.0a9] - X +## [2.0a9] - Unreleased ### Fixed @@ -27,12 +27,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add to libcasm.xtal: substitute_structure_species - Add to libcasm.xtal.Prim: method labels, constructor parameter `labels` - Add to libcasm.xtal.Lattice: methods reciprocal, volume, lengths_and_angles, from_lengths_and_angles - - -## Unreleased - -## Added - - Added `unit_cell`, `diagonal_only`, and `fixed_shape` parameters to libcasm.xtal.enumerate_superlattices. From e1ad567cb0b4d71b145a6181dc83ccb9ca5f9b98 Mon Sep 17 00:00:00 2001 From: bpuchala Date: Fri, 26 Jan 2024 08:53:13 -0500 Subject: [PATCH 10/11] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a0b2b3..e31bc17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add to libcasm.xtal: make_primitive_prim (equivalent to current make_primitive), make_primtive_structure, and make_canonical_structure. +- Add to libcasm.xtal: make_primitive_prim (equivalent to current make_primitive), make_primitive_structure, and make_canonical_structure. - Add options to the BCC and FCC structure factory functions in libcasm.xtal.structures to make the conventional cubic unit cells. - Add to libcasm.xtal: StructureAtomInfo namedtuple, and methods sort_structure_by_atom_info, sort_structure_by_atom_type, sort_structure_by_atom_coordinate_frac, and sort_structure_by_atom_coordinate_cart - Add to libcasm.xtal: substitute_structure_species From 74dac0dd84622692423d512dd29f7a2437fb94c2 Mon Sep 17 00:00:00 2001 From: bpuchala Date: Fri, 26 Jan 2024 09:40:53 -0500 Subject: [PATCH 11/11] update github actions --- .github/workflows/build_wheels.yml | 20 +++++++++---------- .github/workflows/test-linux-build.yml | 6 +++--- .github/workflows/test-linux-cxx-only.yml | 8 ++++---- .github/workflows/test-linux-dependencies.yml | 8 ++++---- .github/workflows/test-linux.yml | 8 ++++---- .github/workflows/test-macos-build.yml | 6 +++--- .github/workflows/test-macos.yml | 4 ++-- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 551dee3..fae12f6 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -8,16 +8,16 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Build wheels uses: pypa/cibuildwheel@v2.14.1 env: CIBW_ARCHS_LINUX: x86_64 - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: - name: dist + name: dist_linux path: ./wheelhouse/*.whl build_wheels_macos_x86_64: @@ -25,16 +25,16 @@ jobs: runs-on: macos-12 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Build wheels uses: pypa/cibuildwheel@v2.14.1 env: CIBW_ARCHS_MACOS: x86_64 - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: - name: dist + name: dist_macos path: ./wheelhouse/*.whl build_sdist: @@ -42,8 +42,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.11' @@ -60,7 +60,7 @@ jobs: - name: upload sdist if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: dist + name: dist_sdist path: dist/*.tar.gz diff --git a/.github/workflows/test-linux-build.yml b/.github/workflows/test-linux-build.yml index 818920b..2862dc8 100644 --- a/.github/workflows/test-linux-build.yml +++ b/.github/workflows/test-linux-build.yml @@ -14,8 +14,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.11' @@ -29,7 +29,7 @@ jobs: ### libcasm-global ### - name: restore libcasm-global cache id: cache-libcasm-global-restore - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: CASMcode_global/dist key: ${{ runner.os }}-libcasm-global-v2-0-3 diff --git a/.github/workflows/test-linux-cxx-only.yml b/.github/workflows/test-linux-cxx-only.yml index 84cf1b5..b0ad0e7 100644 --- a/.github/workflows/test-linux-cxx-only.yml +++ b/.github/workflows/test-linux-cxx-only.yml @@ -14,8 +14,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.11' @@ -29,7 +29,7 @@ jobs: ### libcasm-global ### - name: restore libcasm-global cache id: cache-libcasm-global-restore - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: CASMcode_global/dist key: ${{ runner.os }}-libcasm-global-v2-0-3 @@ -79,7 +79,7 @@ jobs: - name: upload test log if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: libcasm-xtal-cxx-test-log path: build_cxx_test/Testing/Temporary/LastTest.log diff --git a/.github/workflows/test-linux-dependencies.yml b/.github/workflows/test-linux-dependencies.yml index 9293b74..fa33b9e 100644 --- a/.github/workflows/test-linux-dependencies.yml +++ b/.github/workflows/test-linux-dependencies.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.11' @@ -21,14 +21,14 @@ jobs: ### libcasm-global ### - name: restore libcasm-global cache id: cache-libcasm-global-restore - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: CASMcode_global/dist key: ${{ runner.os }}-libcasm-global-v2-0-3 - name: checkout libcasm-global if: steps.cache-libcasm-global-restore.outputs.cache-hit != 'true' - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: prisms-center/CASMcode_global path: CASMcode_global @@ -47,7 +47,7 @@ jobs: - name: save libcasm-global cache id: cache-libcasm-global-save - uses: actions/cache/save@v3 + uses: actions/cache/save@v4 with: path: CASMcode_global/dist key: ${{ steps.cache-libcasm-global-restore.outputs.cache-primary-key }} diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml index 28ff744..0c08077 100644 --- a/.github/workflows/test-linux.yml +++ b/.github/workflows/test-linux.yml @@ -14,8 +14,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.11' @@ -29,7 +29,7 @@ jobs: ### libcasm-global ### - name: restore libcasm-global cache id: cache-libcasm-global-restore - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: CASMcode_global/dist key: ${{ runner.os }}-libcasm-global-v2-0-3 @@ -62,7 +62,7 @@ jobs: - name: upload docs if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: libcasm-xtal-docs path: python/doc/_build/html diff --git a/.github/workflows/test-macos-build.yml b/.github/workflows/test-macos-build.yml index ce7e116..8a8fac7 100644 --- a/.github/workflows/test-macos-build.yml +++ b/.github/workflows/test-macos-build.yml @@ -7,8 +7,8 @@ jobs: runs-on: macOS-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.11' @@ -36,7 +36,7 @@ jobs: - name: upload libcasm-xtal-macos-latest-x86_64-dist if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: libcasm-xtal-macos-latest-x86_64-dist path: dist diff --git a/.github/workflows/test-macos.yml b/.github/workflows/test-macos.yml index c7fcdcf..804ac9e 100644 --- a/.github/workflows/test-macos.yml +++ b/.github/workflows/test-macos.yml @@ -7,8 +7,8 @@ jobs: runs-on: macOS-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.11'