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' diff --git a/CHANGELOG.md b/CHANGELOG.md index 399cc4f..e31bc17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,12 @@ 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 - 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 @@ -20,10 +21,14 @@ 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 +- 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 +- Added `unit_cell`, `diagonal_only`, and `fixed_shape` parameters to libcasm.xtal.enumerate_superlattices. + ## [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/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/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. 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 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 diff --git a/python/src/xtal.cpp b/python/src/xtal.cpp index b902099..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; @@ -296,7 +303,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 +319,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 +366,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 +593,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 +1137,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 +1155,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, @@ -1418,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: @@ -1449,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 ---------- @@ -1468,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 ------- @@ -1745,6 +1807,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 +1835,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 +1858,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 +1866,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_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) 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(); + } } }