diff --git a/CMakeLists.txt b/CMakeLists.txt index 06aca53ad..d1bca9668 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,4 @@ -# CMake 3.12.0 is the oldest version supported by the way freud links TBB to -# object libraries like _cluster. This is also the oldest version tested in CI. -cmake_minimum_required(VERSION 3.12.0) +cmake_minimum_required(VERSION 3.15...3.30) project(freud) @@ -27,6 +25,19 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # https://stackoverflow.com/questions/50600708/combining-cmake-object-libraries-with-shared-libraries set(CMAKE_POSITION_INDEPENDENT_CODE ON) add_subdirectory(CMake) + +# find python +if(CMAKE_VERSION VERSION_LESS 3.18) + set(DEV_MODULE Development) +else() + set(DEV_MODULE Development.Module) +endif() + +find_package( + Python 3.8 + COMPONENTS Interpreter ${DEV_MODULE} + REQUIRED) + find_package_config_first(TBB) if(TBB_FOUND) @@ -36,6 +47,18 @@ if(TBB_FOUND) "[${TBB_LIBRARY}][${TBB_INCLUDE_DIR}]") endif() +# go find nanobind +execute_process( + COMMAND ${Python_EXECUTABLE} "-m" "nanobind" "--cmake_dir" + OUTPUT_STRIP_TRAILING_WHITESPACE + OUTPUT_VARIABLE nanobind_ROOT) +find_package(nanobind CONFIG REQUIRED) +if(nanobind_FOUND) + find_package_message( + nanobind "Found nanobind: ${nanobind_DIR} ${nanobind_VERSION}" + "[${nanobind_DIR},${nanobind_VERSION}]") +endif() + # Fail fast if users have not cloned submodules. if(NOT WIN32) string(ASCII 27 Esc) @@ -76,7 +99,3 @@ set(ignoreMe "${SKBUILD}") add_subdirectory(cpp) add_subdirectory(freud) - -if(_using_conda OR DEFINED ENV{CIBUILDWHEEL}) - set_target_properties(libfreud PROPERTIES INSTALL_RPATH_USE_LINK_PATH True) -endif() diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 0d8750ce5..8a625289e 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -6,16 +6,31 @@ if(WIN32) add_compile_options(/DNOMINMAX) endif() -add_subdirectory(cluster) -add_subdirectory(density) -add_subdirectory(diffraction) -add_subdirectory(environment) -add_subdirectory(locality) -add_subdirectory(order) -add_subdirectory(parallel) -add_subdirectory(pmft) -add_subdirectory(util) +# Detect when building against a conda environment set the _using_conda variable +# for use both in this file and in the parent +get_filename_component(_python_bin_dir ${PYTHON_EXECUTABLE} DIRECTORY) +if(EXISTS "${_python_bin_dir}/../conda-meta") + message("-- Detected conda environment, setting INSTALL_RPATH_USE_LINK_PATH") + set(_using_conda On) + set(_using_conda + On + PARENT_SCOPE) +else() + set(_using_conda Off) + set(_using_conda + Off + PARENT_SCOPE) +endif() + +add_subdirectory(box) + +# commented out for now, uncomment them as the conversion to pybind11 progresses +# add_subdirectory(cluster) add_subdirectory(density) +# add_subdirectory(diffraction) add_subdirectory(environment) +# add_subdirectory(locality) add_subdirectory(order) add_subdirectory(parallel) +# add_subdirectory(pmft) add_subdirectory(util) +#[[ add_library( libfreud SHARED $ @@ -53,3 +68,4 @@ if(CMAKE_EXPORT_COMPILE_COMMANDS) COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_BINARY_DIR}/compile_commands.json ${PROJECT_SOURCE_DIR}) endif() +]] diff --git a/cpp/box/Box.h b/cpp/box/Box.h index 0391669bf..df0d61e51 100644 --- a/cpp/box/Box.h +++ b/cpp/box/Box.h @@ -4,13 +4,13 @@ #ifndef BOX_H #define BOX_H -#include "utils.h" #include #include #include #include #include "VectorMath.h" +#include "utils.h" /*! \file Box.h \brief Represents simulation boxes and contains helpful wrapping functions. @@ -93,12 +93,6 @@ class Box return !(*this == b); } - //! Set L, box lengths, inverses. Box is also centered at zero. - void setL(const vec3& L) - { - setL(L.x, L.y, L.z); - } - //! Set L, box lengths, inverses. Box is also centered at zero. void setL(const float Lx, const float Ly, const float Lz) { @@ -156,9 +150,9 @@ class Box } //! Get current stored inverse of L - vec3 getLinv() const + std::vector getLinv() const { - return m_Linv; + return {m_Linv.x, m_Linv.y, m_Linv.z}; } //! Get tilt factor xy @@ -227,10 +221,9 @@ class Box //! Convert fractional coordinates into absolute coordinates in place /*! \param vecs Vectors of fractional coordinates between 0 and 1 within * parallelepipedal box - * \param Nvecs Number of vectors - * \param out The array in which to place the wrapped vectors. + * \param out_data The array in which to place the wrapped vectors. */ - void makeAbsolute(const vec3* vecs, unsigned int Nvecs, vec3* out) const + void makeAbsolute(const vec3* vecs, const unsigned int Nvecs, vec3* out) const { util::forLoopWrapper(0, Nvecs, [&](size_t begin, size_t end) { for (size_t i = begin; i < end; ++i) @@ -294,12 +287,12 @@ class Box * \param Nvecs Number of vectors \param res Array to save the images */ - void getImages(vec3* vecs, unsigned int Nvecs, vec3* res) const + void getImages(const vec3* vecs, unsigned int Nvecs, vec3* images) const { util::forLoopWrapper(0, Nvecs, [&](size_t begin, size_t end) { for (size_t i = begin; i < end; ++i) { - getImage(vecs[i], res[i]); + getImage(vecs[i], images[i]); } }); } @@ -374,7 +367,7 @@ class Box * \param masses Optional array of masses, of length Nvecs * \return Center of mass as a vec3 */ - vec3 centerOfMass(vec3* vecs, size_t Nvecs, const float* masses = nullptr) const + vec3 centerOfMass(vec3* vecs, size_t Nvecs, const float* masses) const { // This roughly follows the implementation in // https://en.wikipedia.org/wiki/Center_of_mass#Systems_with_periodic_boundary_conditions @@ -386,7 +379,7 @@ class Box vec3 phase(constants::TWO_PI * makeFractional(vecs[i])); vec3> xi(std::polar(float(1.0), phase.x), std::polar(float(1.0), phase.y), std::polar(float(1.0), phase.z)); - float mass = (masses != nullptr) ? masses[i] : float(1.0); + float mass = masses[i]; total_mass += mass; xi_mean += std::complex(mass, 0) * xi; } @@ -401,7 +394,7 @@ class Box * \param Nvecs Number of vectors * \param masses Optional array of masses, of length Nvecs */ - void center(vec3* vecs, unsigned int Nvecs, const float* masses = nullptr) const + void center(vec3* vecs, unsigned int Nvecs, const float* masses) const { vec3 com(centerOfMass(vecs, Nvecs, masses)); util::forLoopWrapper(0, Nvecs, [&](size_t begin, size_t end) { @@ -433,10 +426,6 @@ class Box void computeDistances(const vec3* query_points, const unsigned int n_query_points, const vec3* points, const unsigned int n_points, float* distances) const { - if (n_query_points != n_points) - { - throw std::invalid_argument("The number of query points and points must match."); - } util::forLoopWrapper(0, n_query_points, [&](size_t begin, size_t end) { for (size_t i = begin; i < end; ++i) { @@ -473,7 +462,7 @@ class Box \param n_points The number of points. \param contains_mask Mask of points inside the box. */ - void contains(const vec3* points, const unsigned int n_points, bool* contains_mask) const + void contains(vec3* points, const unsigned int n_points, bool* contains_mask) const { util::forLoopWrapper(0, n_points, [&](size_t begin, size_t end) { std::transform(&points[begin], &points[end], &contains_mask[begin], @@ -528,11 +517,6 @@ class Box /*! \param periodic Flags to set * \post Period flags are set to \a periodic */ - void setPeriodic(vec3 periodic) - { - m_periodic = periodic; - } - void setPeriodic(bool x, bool y, bool z) { m_periodic = vec3(x, y, z); diff --git a/cpp/box/CMakeLists.txt b/cpp/box/CMakeLists.txt new file mode 100644 index 000000000..85078e5b2 --- /dev/null +++ b/cpp/box/CMakeLists.txt @@ -0,0 +1,22 @@ +# create the target and add the needed properties +nanobind_add_module(_box SHARED module-box.cc export_Box.cc) +target_link_libraries(_box PUBLIC TBB::tbb) +# this probably isn't needed for box, but may be needed by other modules +# target_compile_definitions( box # Avoid deprecation warnings for unsupported +# NumPy API versions. See # +# https://numpy.org/doc/1.19/reference/c-api/deprecations.html PRIVATE +# "NPY_NO_DEPRECATED_API=NPY_1_10_API_VERSION" # Default voro++ verbosity is +# high. PRIVATE "VOROPP_VERBOSE=1") + +if(APPLE) + set_target_properties(_box PROPERTIES INSTALL_RPATH "@loader_path") +else() + set_target_properties(_box PROPERTIES INSTALL_RPATH "\$ORIGIN") +endif() + +if(_using_conda OR DEFINED ENV{CIBUILDWHEEL}) + set_target_properties(_box PROPERTIES INSTALL_RPATH_USE_LINK_PATH True) +endif() + +# install +install(TARGETS _box DESTINATION freud) diff --git a/cpp/box/export_Box.cc b/cpp/box/export_Box.cc new file mode 100644 index 000000000..562796f38 --- /dev/null +++ b/cpp/box/export_Box.cc @@ -0,0 +1,107 @@ +// Copyright (c) 2010-2024 The Regents of the University of Michigan +// This file is from the freud project, released under the BSD 3-Clause License. + +#include "export_Box.h" + +namespace nb = nanobind; + +namespace freud { namespace box { namespace wrap { + +void makeAbsolute(std::shared_ptr box, nb_array> vecs, + nb_array> out) +{ + unsigned int Nvecs = vecs.shape(0); + vec3* vecs_data = (vec3*) (vecs.data()); + vec3* out_data = (vec3*) (out.data()); + box->makeAbsolute(vecs_data, Nvecs, out_data); +} + +void makeFractional(std::shared_ptr box, nb_array> vecs, + nb_array> out) +{ + unsigned int Nvecs = vecs.shape(0); + vec3* vecs_data = (vec3*) (vecs.data()); + vec3* out_data = (vec3*) (out.data()); + box->makeFractional(vecs_data, Nvecs, out_data); +} + +void getImages(std::shared_ptr box, nb_array> vecs, + nb_array> images) +{ + const unsigned int Nvecs = vecs.shape(0); + vec3* vecs_data = (vec3*) (vecs.data()); + vec3* images_data = (vec3*) (images.data()); + box->getImages(vecs_data, Nvecs, images_data); +} + +void wrap(std::shared_ptr box, nb_array> vecs, + nb_array> out) +{ + const unsigned int Nvecs = vecs.shape(0); + vec3* vecs_data = (vec3*) (vecs.data()); + vec3* out_data = (vec3*) (out.data()); + box->wrap(vecs_data, Nvecs, out_data); +} + +void unwrap(std::shared_ptr box, nb_array vecs, nb_array images, nb_array out) +{ + const unsigned int Nvecs = vecs.shape(0); + vec3* vecs_data = (vec3*) (vecs.data()); + vec3* images_data = (vec3*) (images.data()); + vec3* out_data = (vec3*) (out.data()); + box->unwrap(vecs_data, images_data, Nvecs, out_data); +} + +std::vector centerOfMass(std::shared_ptr box, nb_array vecs, + nb_array> masses) +{ + const unsigned int Nvecs = vecs.shape(0); + vec3* vecs_data = (vec3*) (vecs.data()); + float* masses_data = (float*) (masses.data()); + auto com = box->centerOfMass(vecs_data, Nvecs, masses_data); + return {com.x, com.y, com.z}; +} + +void center(std::shared_ptr box, nb_array vecs, nb_array> masses) +{ + const unsigned int Nvecs = vecs.shape(0); + vec3* vecs_data = (vec3*) (vecs.data()); + float* masses_data = (float*) (masses.data()); + box->center(vecs_data, Nvecs, masses_data); +} + +void computeDistances(std::shared_ptr box, nb_array query_points, nb_array points, + nb_array> distances) +{ + const unsigned int n_query_points = query_points.shape(0); + vec3* query_points_data = (vec3*) (query_points.data()); + const unsigned int n_points = points.shape(0); + vec3* points_data = (vec3*) (points.data()); + float* distances_data = (float*) (distances.data()); + if (n_query_points != n_points) + { + throw std::invalid_argument("The number of query points and points must match."); + } + box->computeDistances(query_points_data, n_query_points, points_data, n_points, distances_data); +} + +void computeAllDistances(std::shared_ptr box, nb_array query_points, nb_array points, + nb_array> distances) +{ + const unsigned int n_query_points = query_points.shape(0); + vec3* query_points_data = (vec3*) (query_points.data()); + const unsigned int n_points = points.shape(0); + vec3* points_data = (vec3*) (points.data()); + float* distances_data = (float*) (distances.data()); + box->computeAllDistances(query_points_data, n_query_points, points_data, n_points, distances_data); +} + +void contains(std::shared_ptr box, nb_array points, nb_array> contains_mask) +{ + const unsigned int n_points = points.shape(0); + vec3* points_data = (vec3*) (points.data()); + bool* contains_mask_data = (bool*) (contains_mask.data()); + box->contains(points_data, n_points, contains_mask_data); +} + +}; }; }; // namespace freud::box::wrap diff --git a/cpp/box/export_Box.h b/cpp/box/export_Box.h new file mode 100644 index 000000000..fcf319691 --- /dev/null +++ b/cpp/box/export_Box.h @@ -0,0 +1,46 @@ +// Copyright (c) 2010-2024 The Regents of the University of Michigan +// This file is from the freud project, released under the BSD 3-Clause License. + +#ifndef EXPORT_BOX_H +#define EXPORT_BOX_H + +#include "Box.h" + +#include + +namespace freud { namespace box { namespace wrap { + +template> +using nb_array = nanobind::ndarray; + +void makeAbsolute(std::shared_ptr box, nb_array> vecs, + nb_array> out); + +void makeFractional(std::shared_ptr box, nb_array> vecs, + nb_array> out); + +void getImages(std::shared_ptr box, nb_array> vecs, + nb_array> images); + +void wrap(std::shared_ptr box, nb_array> vecs, + nb_array> out); + +void unwrap(std::shared_ptr box, nb_array vecs, nb_array images, nb_array out); + +std::vector centerOfMass(std::shared_ptr box, nb_array vecs, + nb_array> masses); + +void center(std::shared_ptr box, nb_array vecs, nb_array> masses); + +void computeDistances(std::shared_ptr box, nb_array query_points, nb_array points, + nb_array> distances); + +void computeAllDistances(std::shared_ptr box, nb_array query_points, nb_array points, + nb_array> distances); + +void contains(std::shared_ptr box, nb_array points, + nb_array> contains_mask); + +}; }; }; // namespace freud::box::wrap + +#endif diff --git a/cpp/box/module-box.cc b/cpp/box/module-box.cc new file mode 100644 index 000000000..9fc54f1e5 --- /dev/null +++ b/cpp/box/module-box.cc @@ -0,0 +1,51 @@ +// Copyright (c) 2010-2024 The Regents of the University of Michigan +// This file is from the freud project, released under the BSD 3-Clause License. + +#include +#include +#include + +#include "Box.h" +#include "export_Box.h" + +using namespace freud::box; + +NB_MODULE(_box, m) +{ + nanobind::class_(m, "Box") + // constructors + .def(nanobind::init()) + // getters and setters + .def("getLx", &Box::getLx) + .def("getLy", &Box::getLy) + .def("getLz", &Box::getLz) + .def("setL", &Box::setL) + .def("getLinv", &Box::getLinv) + .def("getTiltFactorXY", &Box::getTiltFactorXY) + .def("getTiltFactorXZ", &Box::getTiltFactorXZ) + .def("getTiltFactorYZ", &Box::getTiltFactorYZ) + .def("setTiltFactorXY", &Box::setTiltFactorXY) + .def("setTiltFactorXZ", &Box::setTiltFactorXZ) + .def("setTiltFactorYZ", &Box::setTiltFactorYZ) + .def("getPeriodicX", &Box::getPeriodicX) + .def("getPeriodicY", &Box::getPeriodicY) + .def("getPeriodicZ", &Box::getPeriodicZ) + .def("setPeriodic", &Box::setPeriodic) + .def("setPeriodicX", &Box::setPeriodicX) + .def("setPeriodicY", &Box::setPeriodicY) + .def("setPeriodicZ", &Box::setPeriodicZ) + .def("is2D", &Box::is2D) + .def("set2D", &Box::set2D) + .def("getVolume", &Box::getVolume) + .def("center", &wrap::center) + .def("centerOfMass", &wrap::centerOfMass) + // other stuff + .def("makeAbsolute", &wrap::makeAbsolute) + .def("makeFractional", &wrap::makeFractional) + .def("wrap", &wrap::wrap) + .def("unwrap", &wrap::unwrap) + .def("getImages", &wrap::getImages) + .def("computeDistances", &wrap::computeDistances) + .def("computeAllDistances", &wrap::computeAllDistances) + .def("contains", &wrap::contains); +} diff --git a/freud/CMakeLists.txt b/freud/CMakeLists.txt index d1e800d84..29ea88c59 100644 --- a/freud/CMakeLists.txt +++ b/freud/CMakeLists.txt @@ -1,116 +1,5 @@ -# Need to figure out coverage before CYTHON_FLAGS are set. -option(COVERAGE "Enable coverage" OFF) +set(python_files box.py util.py) +# cluster.py density.py diffraction.py environment.py locality.py order.py +# parallel.py pmft.py) -# Cython flags must be set before we run find_package for Cython since the -# compiler command is created immediately. -set(CYTHON_FLAGS - "--directive binding=True,boundscheck=False,wraparound=False,embedsignature=True,always_allow_keywords=True" - CACHE STRING "The directives for Cython compilation.") - -if(COVERAGE) - set(CYTHON_FLAGS - "${CYTHON_FLAGS},linetrace=True" - CACHE STRING "The directives for Cython compilation." FORCE) -endif() - -find_package(PythonLibs) -find_package(PythonExtensions REQUIRED) -find_package(Cython REQUIRED) -find_package(NumPy REQUIRED) - -include_directories(${NumPy_INCLUDE_DIRS}) - -# Avoid Cython/Python3.8 minor incompatibility warnings, see -# https://github.com/cython/cython/issues/3474. Note that this option is a bit -# expansive, but it's a temporary fix and we'll be testing on other Python -# versions concurrently so it shouldn't hide any real issues. For backwards -# compatibility with older CMake, I'm using PythonInterp; when we drop support -# for CMake < 3.12, we should switch to find_package(Python). -find_package(PythonInterp REQUIRED) -if(${PYTHON_VERSION_MAJOR} EQUAL 3 - AND ${PYTHON_VERSION_MINOR} EQUAL 8 - AND NOT WIN32) - add_compile_options("-Wno-deprecated-declarations") -endif() - -# Detect when building against a conda environment set the _using_conda variable -# for use both in this file and in the parent -get_filename_component(_python_bin_dir ${PYTHON_EXECUTABLE} DIRECTORY) -if(EXISTS "${_python_bin_dir}/../conda-meta") - message("-- Detected conda environment, setting INSTALL_RPATH_USE_LINK_PATH") - set(_using_conda On) - set(_using_conda - On - PARENT_SCOPE) -else() - set(_using_conda Off) - set(_using_conda - Off - PARENT_SCOPE) -endif() - -set(cython_modules_with_cpp - box - cluster - density - diffraction - environment - locality - order - parallel - pmft) - -set(cython_modules_without_cpp interface msd util) - -foreach(cython_module ${cython_modules_with_cpp} ${cython_modules_without_cpp}) - add_cython_target(${cython_module} PY3 CXX) - add_library(${cython_module} SHARED ${${cython_module}}) - make_python_extension_module(${cython_module}) - - target_compile_definitions( - ${cython_module} - # Avoid deprecation warnings for unsupported NumPy API versions. See - # https://numpy.org/doc/1.19/reference/c-api/deprecations.html - PRIVATE "NPY_NO_DEPRECATED_API=NPY_1_10_API_VERSION" - # Default voro++ verbosity is high. - PRIVATE "VOROPP_VERBOSE=1") - if(COVERAGE) - target_compile_definitions( - ${cython_module} # Enable line tracing for coverage purposes if requested. - PRIVATE "CYTHON_TRACE_NOGIL=1") - endif() - - target_link_libraries(${cython_module} libfreud) - - # Separate logic required for targets with C++ code. - if("${cython_module}" IN_LIST cython_modules_with_cpp) - - target_include_directories( - ${cython_module} PRIVATE ${PROJECT_SOURCE_DIR}/cpp/${cython_module}) - endif() - - install(TARGETS ${cython_module} DESTINATION freud) - - # Coverage requires the Cython-compiled C++ files for line coverage. - if(COVERAGE) - install(FILES ${${cython_module}} DESTINATION freud) - endif() - - if(APPLE) - set_target_properties(${cython_module} PROPERTIES INSTALL_RPATH - "@loader_path") - else() - set_target_properties(${cython_module} PROPERTIES INSTALL_RPATH "\$ORIGIN") - endif() - - if(_using_conda OR DEFINED ENV{CIBUILDWHEEL}) - set_target_properties(${cython_module} - PROPERTIES INSTALL_RPATH_USE_LINK_PATH True) - endif() -endforeach() - -# The SolidLiquid class has an instance of cluster::Cluster as a member, so -# including the header requires the Cluster.h header. Would prefer to inherit -# this information from the _order library, but that's not possible since we're -# linking to libfreud. -target_include_directories(order PUBLIC ${PROJECT_SOURCE_DIR}/cpp/cluster) +install(FILES ${python_files} DESTINATION freud) diff --git a/freud/__init__.py b/freud/__init__.py index aacc5685a..8370d7e74 100644 --- a/freud/__init__.py +++ b/freud/__init__.py @@ -1,51 +1,41 @@ # Copyright (c) 2010-2024 The Regents of the University of Michigan # This file is from the freud project, released under the BSD 3-Clause License. -from . import ( +from . import ( # cluster,; data,; density,; diffraction,; environment,; interface,; locality,; msd,; order,; parallel,; pmft, box, - cluster, - data, - density, - diffraction, - environment, - interface, - locality, - msd, - order, - parallel, - pmft, ) from .box import Box -from .locality import AABBQuery, LinkCell, NeighborList -from .parallel import NumThreads, get_num_threads, set_num_threads + +# from .locality import AABBQuery, LinkCell, NeighborList +# from .parallel import NumThreads, get_num_threads, set_num_threads # Override TBB's default autoselection. This is necessary because once the # automatic selection runs, the user cannot change it. -set_num_threads(0) +# set_num_threads(0) __version__ = "3.1.0" __all__ = [ "__version__", "box", - "cluster", - "data", - "density", - "diffraction", - "environment", - "interface", - "locality", - "msd", - "order", - "parallel", - "pmft", + # "cluster", + # "data", + # "density", + # "diffraction", + # "environment", + # "interface", + # "locality", + # "msd", + # "order", + # "parallel", + # "pmft", "Box", - "AABBQuery", - "LinkCell", - "NeighborList", - "get_num_threads", - "set_num_threads", - "NumThreads", + # "AABBQuery", + # "LinkCell", + # "NeighborList", + # "get_num_threads", + # "set_num_threads", + # "NumThreads", ] __citation__ = """@article{freud2020, diff --git a/freud/_box.pxd b/freud/_box.pxd deleted file mode 100644 index 71f88839d..000000000 --- a/freud/_box.pxd +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright (c) 2010-2024 The Regents of the University of Michigan -# This file is from the freud project, released under the BSD 3-Clause License. - -from libcpp cimport bool - -from freud.util cimport vec3 - -ctypedef unsigned int uint - -cdef extern from "Box.h" namespace "freud::box": - cdef cppclass Box: - Box() - Box(float, bool) - Box(float, float, float, bool) - Box(float, float, float, float, float, float, bool) - - bool operator==(const Box &) const - bool operator!=(const Box &) const - - void setL(vec3[float]) - void setL(float, float, float) - - void set2D(bool) - bool is2D() const - - float getLx() const - float getLy() const - float getLz() const - - vec3[float] getL() const - vec3[float] getLinv() const - - float getTiltFactorXY() const - float getTiltFactorXZ() const - float getTiltFactorYZ() const - - void setTiltFactorXY(float) - void setTiltFactorXZ(float) - void setTiltFactorYZ(float) - - float getVolume() const - void makeAbsolute(const vec3[float]*, unsigned int, vec3[float]*) const - void makeFractional(const vec3[float]*, unsigned int, vec3[float]*) const - void getImages(vec3[float]*, unsigned int, vec3[int]*) const - void wrap(const vec3[float]*, unsigned int, vec3[float]*) const - void unwrap(const vec3[float]*, const vec3[int]*, - unsigned int, vec3[float]*) const - vec3[float] centerOfMass(vec3[float]*, size_t, float*) const - void center(vec3[float]*, size_t, float*) const - void computeDistances(vec3[float]*, unsigned int, - vec3[float]*, unsigned int, float* - ) except + - void computeAllDistances(vec3[float]*, unsigned int, - vec3[float]*, unsigned int, float*) - void contains(vec3[float]*, unsigned int, bool*) const - vec3[bool] getPeriodic() const - bool getPeriodicX() const - bool getPeriodicY() const - bool getPeriodicZ() const - void setPeriodic(bool, bool, bool) - void setPeriodicX(bool) - void setPeriodicY(bool) - void setPeriodicZ(bool) diff --git a/freud/box.pxd b/freud/box.pxd deleted file mode 100644 index 6ff0239ca..000000000 --- a/freud/box.pxd +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2010-2024 The Regents of the University of Michigan -# This file is from the freud project, released under the BSD 3-Clause License. - -cimport freud._box - - -cdef class Box: - cdef freud._box.Box * thisptr - -cdef BoxFromCPP(const freud._box.Box & cppbox) diff --git a/freud/box.pyx b/freud/box.py similarity index 72% rename from freud/box.pyx rename to freud/box.py index f50ec08ec..8b583f791 100644 --- a/freud/box.pyx +++ b/freud/box.py @@ -7,30 +7,18 @@ wrapping vectors outside the box back into it. """ -from cpython.object cimport Py_EQ, Py_NE -from cython.operator cimport dereference -from libcpp cimport bool as cpp_bool - -from freud.util cimport vec3 - import logging import warnings import numpy as np +import freud._box import freud.util -cimport numpy as np - -cimport freud._box - logger = logging.getLogger(__name__) -# numpy must be initialized. When using numpy from C or Cython you must -# _always_ do that, or you will have segfaults -np.import_array() -cdef class Box: +class Box: r"""The freud Box class for simulation boxes. This class defines an arbitrary triclinic geometry within which all points are confined. @@ -60,52 +48,50 @@ if :code:`None`. (Default value = :code:`None`) """ # noqa: E501 - def __cinit__(self, Lx, Ly, Lz=0, xy=0, xz=0, yz=0, is2D=None): + def __init__(self, Lx, Ly, Lz=0, xy=0, xz=0, yz=0, is2D=None): if is2D is None: - is2D = (Lz == 0) + is2D = Lz == 0 if is2D: if not (Lx and Ly): raise ValueError("Lx and Ly must be nonzero for 2D boxes.") elif Lz != 0 or xz != 0 or yz != 0: warnings.warn( - "Specifying z-dimensions in a 2-dimensional box " - "has no effect!") + "Specifying z-dimensions in a 2-dimensional box " "has no effect!" + ) else: if not (Lx and Ly and Lz): - raise ValueError( - "Lx, Ly, and Lz must be nonzero for 3D boxes.") - self.thisptr = new freud._box.Box(Lx, Ly, Lz, xy, xz, yz, is2D) - - def __dealloc__(self): - del self.thisptr + raise ValueError("Lx, Ly, and Lz must be nonzero for 3D boxes.") + self._cpp_obj = freud._box.Box(Lx, Ly, Lz, xy, xz, yz, is2D) @property def L(self): r""":math:`\left(3, \right)` :class:`numpy.ndarray`: Get or set the box lengths along x, y, and z.""" - cdef vec3[float] result = self.thisptr.getL() - return np.asarray([result.x, result.y, result.z]) + return np.asarray( + [self._cpp_obj.getLx(), self._cpp_obj.getLy(), self._cpp_obj.getLz()] + ) @L.setter def L(self, value): try: if len(value) != 3: - raise ValueError('setL must be called with a scalar or a list ' - 'of length 3.') + raise ValueError( + "setL must be called with a scalar or a list " "of length 3." + ) except TypeError: # Will fail if object has no length value = (value, value, value) if self.is2D and value[2] != 0: warnings.warn( - "Specifying z-dimensions in a 2-dimensional box " - "has no effect!") - self.thisptr.setL(value[0], value[1], value[2]) + "Specifying z-dimensions in a 2-dimensional box " "has no effect!" + ) + self._cpp_obj.setL(value[0], value[1], value[2]) @property def Lx(self): """float: Get or set the x-dimension length.""" - return self.thisptr.getLx() + return self._cpp_obj.getLx() @Lx.setter def Lx(self, value): @@ -114,7 +100,7 @@ def Lx(self, value): @property def Ly(self): """float: Get or set the y-dimension length.""" - return self.thisptr.getLy() + return self._cpp_obj.getLy() @Ly.setter def Ly(self, value): @@ -123,7 +109,7 @@ def Ly(self, value): @property def Lz(self): """float: Get or set the z-dimension length.""" - return self.thisptr.getLz() + return self._cpp_obj.getLz() @Lz.setter def Lz(self, value): @@ -132,29 +118,45 @@ def Lz(self, value): @property def xy(self): """float: Get or set the xy tilt factor.""" - return self.thisptr.getTiltFactorXY() + return self._cpp_obj.getTiltFactorXY() @xy.setter def xy(self, value): - self.thisptr.setTiltFactorXY(value) + self._cpp_obj.setTiltFactorXY(value) @property def xz(self): """float: Get or set the xz tilt factor.""" - return self.thisptr.getTiltFactorXZ() + return self._cpp_obj.getTiltFactorXZ() @xz.setter def xz(self, value): - self.thisptr.setTiltFactorXZ(value) + self._cpp_obj.setTiltFactorXZ(value) @property def yz(self): """float: Get or set the yz tilt factor.""" - return self.thisptr.getTiltFactorYZ() + return self._cpp_obj.getTiltFactorYZ() @yz.setter def yz(self, value): - self.thisptr.setTiltFactorYZ(value) + self._cpp_obj.setTiltFactorYZ(value) + + def __eq__(self, other): + if type(other) != freud.box.Box: + return False + return ( + self.Lx == other.Lx + and self.Ly == other.Ly + and self.Lz == other.Lz + and self.xy == other.xy + and self.xz == other.xz + and self.yz == other.yz + and self.is2D == other.is2D + and self.periodic_x == other.periodic_x + and self.periodic_y == other.periodic_y + and self.periodic_z == other.periodic_z + ) @property def dimensions(self): @@ -164,24 +166,24 @@ def dimensions(self): @dimensions.setter def dimensions(self, value): assert value == 2 or value == 3 - self.thisptr.set2D(bool(value == 2)) + self._cpp_obj.set2D(bool(value == 2)) @property def is2D(self): """bool: Whether the box is 2D.""" - return self.thisptr.is2D() + return self._cpp_obj.is2D() @property def L_inv(self): r""":math:`\left(3, \right)` :class:`numpy.ndarray`: The inverse box lengths.""" - cdef vec3[float] result = self.thisptr.getLinv() - return np.asarray([result.x, result.y, result.z]) + result = self._cpp_obj.getLinv() + return np.asarray(result) @property def volume(self): """float: The box volume (area in 2D).""" - return self.thisptr.getVolume() + return self._cpp_obj.getVolume() def make_absolute(self, fractional_coordinates, out=None): r"""Convert fractional coordinates into absolute coordinates. @@ -200,20 +202,13 @@ def make_absolute(self, fractional_coordinates, out=None): Absolute coordinate vector(s). If ``out`` is provided, a reference to it is returned. """ # noqa: E501 - fractions = np.asarray(fractional_coordinates).copy() + fractions = np.asarray(fractional_coordinates) flatten = fractions.ndim == 1 fractions = np.atleast_2d(fractions) fractions = freud.util._convert_array(fractions, shape=(None, 3)) - out = freud.util._convert_array( - out, shape=fractions.shape, allow_copy=False) - - cdef const float[:, ::1] l_points = fractions - cdef unsigned int Np = l_points.shape[0] - cdef float[:, ::1] l_out = out + out = freud.util._convert_array(out, shape=fractions.shape, allow_copy=False) - self.thisptr.makeAbsolute( - &l_points[0, 0], Np, - &l_out[0, 0]) + self._cpp_obj.makeAbsolute(fractions, out) return np.squeeze(out) if flatten else out @@ -233,20 +228,13 @@ def make_fractional(self, absolute_coordinates, out=None): Fractional coordinate vector(s). If ``out`` is provided, a reference to it is returned. """ # noqa: E501 - vecs = np.asarray(absolute_coordinates).copy() + vecs = np.asarray(absolute_coordinates) flatten = vecs.ndim == 1 vecs = np.atleast_2d(vecs) vecs = freud.util._convert_array(vecs, shape=(None, 3)) - out = freud.util._convert_array( - out, shape=vecs.shape, allow_copy=False) + out = freud.util._convert_array(out, shape=vecs.shape, allow_copy=False) - cdef const float[:, ::1] l_points = vecs - cdef unsigned int Np = l_points.shape[0] - cdef float[:, ::1] l_out = out - - self.thisptr.makeFractional( - &l_points[0, 0], Np, - &l_out[0, 0]) + self._cpp_obj.makeFractional(vecs, out) return np.squeeze(out) if flatten else out @@ -265,13 +253,9 @@ def get_images(self, vecs): flatten = vecs.ndim == 1 vecs = np.atleast_2d(vecs) vecs = freud.util._convert_array(vecs, shape=(None, 3)) - images = np.zeros(vecs.shape, dtype=np.int32) - cdef const float[:, ::1] l_points = vecs - cdef const int[:, ::1] l_result = images - cdef unsigned int Np = l_points.shape[0] - self.thisptr.getImages( &l_points[0, 0], Np, - &l_result[0, 0]) + + self._cpp_obj.getImages(vecs, images) return np.squeeze(images) if flatten else images @@ -331,15 +315,9 @@ def wrap(self, vecs, out=None): flatten = vecs.ndim == 1 vecs = np.atleast_2d(vecs) vecs = freud.util._convert_array(vecs, shape=(None, 3)) - out = freud.util._convert_array( - out, shape=vecs.shape, allow_copy=False) - - cdef const float[:, ::1] l_points = vecs - cdef unsigned int Np = l_points.shape[0] - cdef float[:, ::1] l_out = out + out = freud.util._convert_array(out, shape=vecs.shape, allow_copy=False) - self.thisptr.wrap( &l_points[0, 0], - Np, &l_out[0, 0]) + self._cpp_obj.wrap(vecs, out) return np.squeeze(out) if flatten else out @@ -371,19 +349,10 @@ def unwrap(self, vecs, imgs, out=None): # Broadcasts (1, 3) to (N, 3) for both arrays vecs, imgs = np.broadcast_arrays(vecs, imgs) vecs = freud.util._convert_array(vecs, shape=(None, 3)).copy() - imgs = freud.util._convert_array( - imgs, shape=vecs.shape, dtype=np.int32) - out = freud.util._convert_array( - out, shape=vecs.shape, allow_copy=False) + imgs = freud.util._convert_array(imgs, shape=vecs.shape, dtype=np.int32) + out = freud.util._convert_array(out, shape=vecs.shape, allow_copy=False) - cdef const float[:, ::1] l_points = vecs - cdef const int[:, ::1] l_imgs = imgs - cdef unsigned int Np = l_points.shape[0] - cdef float[:, ::1] l_out = out - - self.thisptr.unwrap( &l_points[0, 0], - &l_imgs[0, 0], Np, - &l_out[0, 0]) + self._cpp_obj.unwrap(vecs, imgs, out) return np.squeeze(out) if flatten else out @@ -417,18 +386,14 @@ def center_of_mass(self, vecs, masses=None): Center of mass. """ # noqa: E501 vecs = freud.util._convert_array(vecs, shape=(None, 3)) - cdef const float[:, ::1] l_points = vecs - cdef float* l_masses_ptr = NULL - cdef float[::1] l_masses if masses is not None: - l_masses = freud.util._convert_array(masses, shape=(len(vecs), )) - l_masses_ptr = &l_masses[0] + masses = freud.util._convert_array(masses, shape=(len(vecs),)) + else: + masses = np.ones(vecs.shape[0], dtype=np.float32) - cdef size_t Np = l_points.shape[0] - cdef vec3[float] result = self.thisptr.centerOfMass( - &l_points[0, 0], Np, l_masses_ptr) - return np.asarray([result.x, result.y, result.z]) + result = self._cpp_obj.centerOfMass(vecs, masses) + return np.asarray(result) def center(self, vecs, masses=None): r"""Subtract center of mass from an array of vectors, using periodic boundaries. @@ -459,16 +424,13 @@ def center(self, vecs, masses=None): Vectors with center of mass subtracted. """ # noqa: E501 vecs = freud.util._convert_array(vecs, shape=(None, 3)).copy() - cdef const float[:, ::1] l_points = vecs - cdef float* l_masses_ptr = NULL - cdef float[::1] l_masses if masses is not None: - l_masses = freud.util._convert_array(masses, shape=(len(vecs), )) - l_masses_ptr = &l_masses[0] + masses = freud.util._convert_array(masses, shape=(len(vecs),)) + else: + masses = np.ones(vecs.shape[0], dtype=np.float32) - cdef size_t Np = l_points.shape[0] - self.thisptr.center( &l_points[0, 0], Np, l_masses_ptr) + self._cpp_obj.center(vecs, masses) return vecs def compute_distances(self, query_points, points): @@ -486,26 +448,17 @@ def compute_distances(self, query_points, points): Returns: :math:`\left(N, \right)` :class:`numpy.ndarray`: Array of distances between query points and points. - """ # noqa: E501 + """ # noqa: E501 query_points = freud.util._convert_array( - np.atleast_2d(query_points), shape=(None, 3)) - points = freud.util._convert_array( - np.atleast_2d(points), shape=(None, 3)) - - cdef: - const float[:, ::1] l_query_points = query_points - const float[:, ::1] l_points = points - size_t n_query_points = query_points.shape[0] - size_t n_points = points.shape[0] - float[::1] distances = np.empty( - n_query_points, dtype=np.float32) - - self.thisptr.computeDistances( - &l_query_points[0, 0], n_query_points, - &l_points[0, 0], n_points, - &distances[0]) - return np.asarray(distances) + np.atleast_2d(query_points), shape=(None, 3) + ) + points = freud.util._convert_array(np.atleast_2d(points), shape=(None, 3)) + + distances = np.empty(query_points.shape[0], dtype=np.float32) + + self._cpp_obj.computeDistances(query_points, points, distances) + return distances def compute_all_distances(self, query_points, points): r"""Calculate distances between all pairs of query points and points, using periodic boundaries. @@ -524,24 +477,17 @@ def compute_all_distances(self, query_points, points): Array of distances between query points and points. """ # noqa: E501 query_points = freud.util._convert_array( - np.atleast_2d(query_points), shape=(None, 3)) - points = freud.util._convert_array( - np.atleast_2d(points), shape=(None, 3)) + np.atleast_2d(query_points), shape=(None, 3) + ) + points = freud.util._convert_array(np.atleast_2d(points), shape=(None, 3)) - cdef: - const float[:, ::1] l_query_points = query_points - const float[:, ::1] l_points = points - size_t n_query_points = query_points.shape[0] - size_t n_points = points.shape[0] - float[:, ::1] distances = np.empty( - [n_query_points, n_points], dtype=np.float32) + n_query_points = query_points.shape[0] + n_points = points.shape[0] + distances = np.empty([n_query_points, n_points], dtype=np.float32) - self.thisptr.computeAllDistances( - &l_query_points[0, 0], n_query_points, - &l_points[0, 0], n_points, - &distances[0, 0]) + self._cpp_obj.computeAllDistances(query_points, points, distances) - return np.asarray(distances) + return distances def contains(self, points): r"""Compute a boolean array (mask) corresponding to point membership in a box. @@ -575,22 +521,12 @@ def contains(self, points): the box, and ``False`` corresponds to points outside the box. """ # noqa: E501 - points = freud.util._convert_array( - np.atleast_2d(points), shape=(None, 3)) - - cdef: - const float[:, ::1] l_points = points - size_t n_points = points.shape[0] - - contains_mask = freud.util._convert_array( - np.ones(n_points), dtype=bool) - cdef cpp_bool[::1] l_contains_mask = contains_mask + points = freud.util._convert_array(np.atleast_2d(points), shape=(None, 3)) + contains_mask = freud.util._convert_array(np.ones(points.shape[0]), dtype=bool) - self.thisptr.contains( - &l_points[0, 0], n_points, - &l_contains_mask[0]) + self._cpp_obj.contains(points, contains_mask) - return np.array(l_contains_mask).astype(bool) + return contains_mask @property def cubic(self): @@ -610,44 +546,49 @@ def cubic(self): def periodic(self): r""":math:`\left(3, \right)` :class:`numpy.ndarray`: Get or set the periodicity of the box in each dimension.""" - periodic = self.thisptr.getPeriodic() - return np.asarray([periodic.x, periodic.y, periodic.z]) + return np.asarray( + [ + self._cpp_obj.getPeriodicX(), + self._cpp_obj.getPeriodicY(), + self._cpp_obj.getPeriodicZ(), + ] + ) @periodic.setter def periodic(self, periodic): # Allow passing a single value try: - self.thisptr.setPeriodic(periodic[0], periodic[1], periodic[2]) + self._cpp_obj.setPeriodic(periodic[0], periodic[1], periodic[2]) except TypeError: # Allow single value to be passed for all directions - self.thisptr.setPeriodic(periodic, periodic, periodic) + self._cpp_obj.setPeriodic(periodic, periodic, periodic) @property def periodic_x(self): """bool: Get or set the periodicity of the box in x.""" - return self.thisptr.getPeriodicX() + return self._cpp_obj.getPeriodicX() @periodic_x.setter def periodic_x(self, periodic): - self.thisptr.setPeriodicX(periodic) + self._cpp_obj.setPeriodicX(periodic) @property def periodic_y(self): """bool: Get or set the periodicity of the box in y.""" - return self.thisptr.getPeriodicY() + return self._cpp_obj.getPeriodicY() @periodic_y.setter def periodic_y(self, periodic): - self.thisptr.setPeriodicY(periodic) + self._cpp_obj.setPeriodicY(periodic) @property def periodic_z(self): """bool: Get or set the periodicity of the box in z.""" - return self.thisptr.getPeriodicZ() + return self._cpp_obj.getPeriodicZ() @periodic_z.setter def periodic_z(self, periodic): - self.thisptr.setPeriodicZ(periodic) + self._cpp_obj.setPeriodicZ(periodic) def to_dict(self): r"""Return box as dictionary. @@ -663,13 +604,14 @@ def to_dict(self): dict: Box parameters """ return { - 'Lx': self.Lx, - 'Ly': self.Ly, - 'Lz': self.Lz, - 'xy': self.xy, - 'xz': self.xz, - 'yz': self.yz, - 'dimensions': self.dimensions} + "Lx": self.Lx, + "Ly": self.Ly, + "Lz": self.Lz, + "xy": self.xy, + "xz": self.xz, + "yz": self.yz, + "dimensions": self.dimensions, + } def to_matrix(self): r"""Returns the box matrix (3x3). @@ -685,9 +627,13 @@ def to_matrix(self): Returns: :math:`\left(3, 3\right)` :class:`numpy.ndarray`: Box matrix """ - return np.asarray([[self.Lx, self.xy * self.Ly, self.xz * self.Lz], - [0, self.Ly, self.yz * self.Lz], - [0, 0, self.Lz]]) + return np.asarray( + [ + [self.Lx, self.xy * self.Ly, self.xz * self.Lz], + [0, self.Ly, self.yz * self.Lz], + [0, 0, self.Lz], + ] + ) def to_box_lengths_and_angles(self): r"""Return the box lengths and angles. @@ -701,51 +647,45 @@ def to_box_lengths_and_angles(self): (self.xy * self.xz + self.yz) / (np.sqrt(1 + self.xy**2) * np.sqrt(1 + self.xz**2 + self.yz**2)) ) - beta = np.arccos(self.xz/np.sqrt(1+self.xz**2+self.yz**2)) - gamma = np.arccos(self.xy/np.sqrt(1+self.xy**2)) + beta = np.arccos(self.xz / np.sqrt(1 + self.xz**2 + self.yz**2)) + gamma = np.arccos(self.xy / np.sqrt(1 + self.xy**2)) L1 = self.Lx - a2 = [self.Ly*self.xy, self.Ly, 0] - a3 = [self.Lz*self.xz, self.Lz*self.yz, self.Lz] + a2 = [self.Ly * self.xy, self.Ly, 0] + a3 = [self.Lz * self.xz, self.Lz * self.yz, self.Lz] L2 = np.linalg.norm(a2) L3 = np.linalg.norm(a3) return (L1, L2, L3, alpha, beta, gamma) def __repr__(self): - return ("freud.box.{cls}(Lx={Lx}, Ly={Ly}, Lz={Lz}, " - "xy={xy}, xz={xz}, yz={yz}, " - "is2D={is2D})").format(cls=type(self).__name__, - Lx=self.Lx, - Ly=self.Ly, - Lz=self.Lz, - xy=self.xy, - xz=self.xz, - yz=self.yz, - is2D=self.is2D) + return ( + "freud.box.{cls}(Lx={Lx}, Ly={Ly}, Lz={Lz}, " + "xy={xy}, xz={xz}, yz={yz}, " + "is2D={is2D})" + ).format( + cls=type(self).__name__, + Lx=self.Lx, + Ly=self.Ly, + Lz=self.Lz, + xy=self.xy, + xz=self.xz, + yz=self.yz, + is2D=self.is2D, + ) def __str__(self): return repr(self) - def __richcmp__(self, other, int op): - r"""Implement all comparisons for Cython extension classes""" - cdef Box c_other - try: - c_other = other - if op == Py_EQ: - return dereference(self.thisptr) == dereference(c_other.thisptr) - if op == Py_NE: - return dereference(self.thisptr) != dereference(c_other.thisptr) - except TypeError: - # Cython cast to Box failed - pass - return NotImplemented - def __mul__(self, scale): if scale > 0: - return self.__class__(Lx=self.Lx*scale, - Ly=self.Ly*scale, - Lz=self.Lz*scale, - xy=self.xy, xz=self.xz, yz=self.yz, - is2D=self.is2D) + return self.__class__( + Lx=self.Lx * scale, + Ly=self.Ly * scale, + Lz=self.Lz * scale, + xy=self.xy, + xz=self.xz, + yz=self.yz, + is2D=self.is2D, + ) else: raise ValueError("Box can only be multiplied by positive values.") @@ -773,8 +713,10 @@ def plot(self, title=None, ax=None, image=[0, 0, 0], *args, **kwargs): :meth:`matplotlib.axes.Axes.plot`. """ import freud.plot - return freud.plot.box_plot(self, title=title, ax=ax, image=image, - *args, **kwargs) + + return freud.plot.box_plot( + self, title=title, ax=ax, image=image, *args, **kwargs + ) @classmethod def from_box(cls, box, dimensions=None): @@ -819,52 +761,54 @@ def from_box(cls, box, dimensions=None): # Handles freud.box.Box and objects with attributes Lx = box.Lx Ly = box.Ly - Lz = getattr(box, 'Lz', 0) - xy = getattr(box, 'xy', 0) - xz = getattr(box, 'xz', 0) - yz = getattr(box, 'yz', 0) + Lz = getattr(box, "Lz", 0) + xy = getattr(box, "xy", 0) + xz = getattr(box, "xz", 0) + yz = getattr(box, "yz", 0) if dimensions is None: - dimensions = getattr(box, 'dimensions', None) - elif dimensions != getattr(box, 'dimensions', dimensions): + dimensions = getattr(box, "dimensions", None) + elif dimensions != getattr(box, "dimensions", dimensions): raise ValueError( "The provided dimensions argument conflicts with the " - "dimensions attribute of the provided box object.") + "dimensions attribute of the provided box object." + ) except AttributeError: try: # Handle dictionary-like - Lx = box['Lx'] - Ly = box['Ly'] - Lz = box.get('Lz', 0) - xy = box.get('xy', 0) - xz = box.get('xz', 0) - yz = box.get('yz', 0) + Lx = box["Lx"] + Ly = box["Ly"] + Lz = box.get("Lz", 0) + xy = box.get("xy", 0) + xz = box.get("xz", 0) + yz = box.get("yz", 0) if dimensions is None: - dimensions = box.get('dimensions', None) + dimensions = box.get("dimensions", None) else: - if dimensions != box.get('dimensions', dimensions): + if dimensions != box.get("dimensions", dimensions): raise ValueError( "The provided dimensions argument conflicts with " "the dimensions attribute of the provided box " - "object.") + "object." + ) except (IndexError, KeyError, TypeError): if not len(box) in [2, 3, 6]: raise ValueError( "List-like objects must have length 2, 3, or 6 to be " - "converted to freud.box.Box.") + "converted to freud.box.Box." + ) # Handle list-like Lx = box[0] Ly = box[1] Lz = box[2] if len(box) > 2 else 0 xy, xz, yz = box[3:6] if len(box) == 6 else (0, 0, 0) except: # noqa - logger.debug('Supplied box cannot be converted to type ' - 'freud.box.Box.') + logger.debug("Supplied box cannot be converted to type " "freud.box.Box.") raise # Infer dimensions if not provided. if dimensions is None: dimensions = 2 if Lz == 0 else 3 - is2D = (dimensions == 2) + is2D = dimensions == 2 return cls(Lx=Lx, Ly=Ly, Lz=Lz, xy=xy, xz=xz, yz=yz, is2D=is2D) @classmethod @@ -903,9 +847,8 @@ def from_matrix(cls, box_matrix, dimensions=None): xz = yz = 0 if dimensions is None: dimensions = 2 if Lz == 0 else 3 - is2D = (dimensions == 2) - return cls(Lx=Lx, Ly=Ly, Lz=Lz, - xy=xy, xz=xz, yz=yz, is2D=is2D) + is2D = dimensions == 2 + return cls(Lx=Lx, Ly=Ly, Lz=Lz, xy=xy, xz=xz, yz=yz, is2D=is2D) @classmethod def cube(cls, L=None): @@ -938,13 +881,19 @@ def square(cls, L=None): # named access to positional arguments, so we keep this to # recover the behavior if L is None: - raise TypeError("square() missing 1 required " - "positional argument: L") + raise TypeError("square() missing 1 required " "positional argument: L") return cls(Lx=L, Ly=L, Lz=0, xy=0, xz=0, yz=0, is2D=True) @classmethod def from_box_lengths_and_angles( - cls, L1, L2, L3, alpha, beta, gamma, dimensions=None, + cls, + L1, + L2, + L3, + alpha, + beta, + gamma, + dimensions=None, ): r"""Construct a box from lengths and angles (in radians). @@ -992,11 +941,15 @@ def from_box_lengths_and_angles( return cls.from_matrix(np.array([a1, a2, a3]).T, dimensions=dimensions) -cdef BoxFromCPP(const freud._box.Box & cppbox): - b = Box(cppbox.getLx(), cppbox.getLy(), cppbox.getLz(), - cppbox.getTiltFactorXY(), cppbox.getTiltFactorXZ(), - cppbox.getTiltFactorYZ(), cppbox.is2D()) - b.periodic = [cppbox.getPeriodicX(), - cppbox.getPeriodicY(), - cppbox.getPeriodicZ()] +def BoxFromCPP(cppbox): + b = Box( + cppbox.getLx(), + cppbox.getLy(), + cppbox.getLz(), + cppbox.getTiltFactorXY(), + cppbox.getTiltFactorXZ(), + cppbox.getTiltFactorYZ(), + cppbox.is2D(), + ) + b.periodic = [cppbox.getPeriodicX(), cppbox.getPeriodicY(), cppbox.getPeriodicZ()] return b diff --git a/freud/util.py b/freud/util.py new file mode 100644 index 000000000..47b648c93 --- /dev/null +++ b/freud/util.py @@ -0,0 +1,172 @@ +# Copyright (c) 2010-2024 The Regents of the University of Michigan +# This file is from the freud project, released under the BSD 3-Clause License. + +from functools import wraps + +import numpy as np + +import freud.box + + +class _Compute: + r"""Parent class for all compute classes in freud. + + The primary purpose of this class is to prevent access of uncomputed + values. This is accomplished by maintaining a boolean flag to track whether + the compute method in a class has been called and decorating class + properties that rely on compute having been called. + + To use this class, one would write, for example, + + .. code-block:: python + class Cluster(_Compute): + + def compute(...) + ... + + @_Compute._computed_property + def cluster_idx(self): + return ... + + Attributes: + _called_compute (bool): + Flag representing whether the compute method has been called. + """ + + def __init__(self): + self._called_compute = False + + def __getattribute__(self, attr): + """Compute methods set a flag to indicate that quantities have been + computed. Compute must be called before plotting.""" + attribute = object.__getattribute__(self, attr) + if attr == "compute": + # Set the attribute *after* computing. This enables + # self._called_compute to be used in the compute method itself. + compute = attribute + + @wraps(compute) + def compute_wrapper(*args, **kwargs): + return_value = compute(*args, **kwargs) + self._called_compute = True + return return_value + + return compute_wrapper + elif attr == "plot": + if not self._called_compute: + raise AttributeError( + "The compute method must be called before calling plot." + ) + return attribute + + @staticmethod + def _computed_property(prop): + r"""Decorator that makes a class method to be a property with limited access. + + Args: + prop (callable): The property function. + + Returns: + Decorator decorating appropriate property method. + """ + + @property + @wraps(prop) + def wrapper(self, *args, **kwargs): + if not self._called_compute: + raise AttributeError("Property not computed. Call compute first.") + return prop(self, *args, **kwargs) + + return wrapper + + def __str__(self): + return repr(self) + + +def _convert_array( + array, shape=None, dtype=np.float32, requirements=("C",), allow_copy=True +): + """Function which takes a given array, checks the dimensions and shape, + and converts to a supplied dtype. + + Args: + array (:class:`numpy.ndarray` or :code:`None`): Array to check and convert. + If :code:`None`, an empty array of given shape and type will be initialized + (Default value: :code:`None`). + shape: (tuple of int and :code:`None`): Expected shape of the array. + Only the dimensions that are not :code:`None` are checked. + (Default value = :code:`None`). + dtype: :code:`dtype` to convert the array to if :code:`array.dtype` + is different. If :code:`None`, :code:`dtype` will not be changed + (Default value = :attr:`numpy.float32`). + requirements (Sequence[str]): A sequence of string flags to be passed to + :func:`numpy.require`. + allow_copy (bool): If :code:`False` and the input array does not already + conform to the required dtype and other requirements, this function + will raise an error rather than coercing the array into a copy that + does satisfy the requirements (Default value = :code:`True`). + + Returns: + :class:`numpy.ndarray`: Array. + """ + if array is None: + return np.empty(shape, dtype=dtype) + + array = np.asarray(array) + return_arr = np.require(array, dtype=dtype, requirements=requirements) + + if not allow_copy and return_arr is not array: + raise ValueError( + "The provided output array must have dtype " + f"{dtype}, and have the following array flags: " + f"{', '.join(requirements)}." + ) + + if shape is not None: + if return_arr.ndim != len(shape): + raise ValueError( + "array.ndim = {}; expected ndim = {}".format( + return_arr.ndim, len(shape) + ) + ) + + for i, s in enumerate(shape): + if s is not None and return_arr.shape[i] != s: + shape_str = ( + "(" + + ", ".join(str(i) if i is not None else "..." for i in shape) + + ")" + ) + raise ValueError( + "array.shape= {}; expected shape = {}".format( + return_arr.shape, shape_str + ) + ) + + return return_arr + + +def _convert_box(box, dimensions=None): + """Function which takes a box-like object and attempts to convert it to + :class:`freud.box.Box`. Existing :class:`freud.box.Box` objects are + used directly. + + Args: + box (box-like object (see :meth:`freud.box.Box.from_box`)): Box to + check and convert if needed. + dimensions (int): Number of dimensions the box should be. If not None, + used to verify the box dimensions (Default value = :code:`None`). + + Returns: + :class:`freud.box.Box`: freud box. + """ + if not isinstance(box, freud.box.Box): + try: + box = freud.box.Box.from_box(box) + except ValueError: + raise + + if dimensions is not None and box.dimensions != dimensions: + raise ValueError(f"The box must be {dimensions}-dimensional.") + + return box