From c1da8f7411467b94eae9ab595e693bbea20ea476 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Wed, 15 Apr 2026 00:59:56 +0000 Subject: [PATCH 01/33] add aux basis to BasisSet, clean up existing constructors --- cpp/include/qdk/chemistry/data/basis_set.hpp | 249 ++++++- cpp/src/qdk/chemistry/data/basis_set.cpp | 668 ++++++++++++++++++- cpp/tests/test_basis_set.cpp | 368 +++++++++- python/src/pybind11/data/basis_set.cpp | 422 ++++++++---- python/tests/test_basis_set.py | 302 ++++++++- 5 files changed, 1811 insertions(+), 198 deletions(-) diff --git a/cpp/include/qdk/chemistry/data/basis_set.hpp b/cpp/include/qdk/chemistry/data/basis_set.hpp index 613f034c9..9809f6ee0 100644 --- a/cpp/include/qdk/chemistry/data/basis_set.hpp +++ b/cpp/include/qdk/chemistry/data/basis_set.hpp @@ -187,15 +187,15 @@ struct Shell { class BasisSet : public DataClass, public std::enable_shared_from_this { public: - /** - * @brief Constructor with basis set name and structure - * @param name Name of the basis set (e.g., "6-31G", "cc-pVDZ") - * @param structure The molecular structure - * @param atomic_orbital_type Whether to use spherical or cartesian atomic - * orbitals - */ - BasisSet(const std::string& name, const Structure& structure, - AOType atomic_orbital_type = AOType::Spherical); +// /** +// * @brief Constructor with basis set name and structure +// * @param name Name of the basis set (e.g., "6-31G", "cc-pVDZ") +// * @param structure The molecular structure +// * @param atomic_orbital_type Whether to use spherical or cartesian atomic +// * orbitals +// */ +// BasisSet(const std::string& name, const Structure& structure, +// AOType atomic_orbital_type = AOType::Spherical); /** * @brief Constructor with shells @@ -219,15 +219,15 @@ class BasisSet : public DataClass, const Structure& structure, AOType atomic_orbital_type = AOType::Spherical); - /** - * @brief Constructor with basis set name and structure shared pointer - * @param name Name of the basis set (e.g., "6-31G", "cc-pVDZ") - * @param structure Shared pointer to the molecular structure - * @param atomic_orbital_type Whether to use spherical or cartesian atomic - * orbitals - */ - BasisSet(const std::string& name, std::shared_ptr structure, - AOType atomic_orbital_type = AOType::Spherical); +// /** +// * @brief Constructor with basis set name and structure shared pointer +// * @param name Name of the basis set (e.g., "6-31G", "cc-pVDZ") +// * @param structure Shared pointer to the molecular structure +// * @param atomic_orbital_type Whether to use spherical or cartesian atomic +// * orbitals +// */ +// BasisSet(const std::string& name, std::shared_ptr structure, +// AOType atomic_orbital_type = AOType::Spherical); /** * @brief Constructor with shells and structure shared pointer @@ -301,6 +301,78 @@ class BasisSet : public DataClass, std::shared_ptr structure, AOType basis_type = AOType::Spherical); + + /** + * @brief Constructor with shells, ECP shells, auxiliary shells, and structure + * @param name Name of the basis set + * @param shells Vector of shells to initialize the basis set with + * @param ecp_shells Vector of ECP shells to initialize the basis set with + * @param aux_shells Vector of auxiliary shells (e.g., for density fitting) + * @param structure The molecular structure + * @param basis_type Whether to use spherical or cartesian atomic orbitals + */ + BasisSet(const std::string& name, const std::vector& shells, + const std::vector& ecp_shells, + const std::vector& aux_shells, const Structure& structure, + AOType basis_type = AOType::Spherical); + + /** + * @brief Constructor with shells, ECP shells, auxiliary shells, and structure + * shared pointer + * @param name Name of the basis set + * @param shells Vector of shells to initialize the basis set with + * @param ecp_shells Vector of ECP shells to initialize the basis set with + * @param aux_shells Vector of auxiliary shells (e.g., for density fitting) + * @param structure Shared pointer to the molecular structure + * @param basis_type Whether to use spherical or cartesian atomic orbitals + */ + BasisSet(const std::string& name, const std::vector& shells, + const std::vector& ecp_shells, + const std::vector& aux_shells, + std::shared_ptr structure, + AOType basis_type = AOType::Spherical); + + /** + * @brief Constructor with shells, ECP shells, ECP metadata, auxiliary shells, + * auxiliary name, and structure + * @param name Name of the basis set + * @param shells Vector of shells to initialize the basis set with + * @param ecp_name Name of the ECP basis set + * @param ecp_shells Vector of ECP shells to initialize the basis set with + * @param ecp_electrons Vector containing numbers of ECP electrons for each + * atom + * @param aux_name Name of the auxiliary basis set + * @param aux_shells Vector of auxiliary shells (e.g., for density fitting) + * @param structure The molecular structure + * @param basis_type Whether to use spherical or cartesian atomic orbitals + */ + BasisSet(const std::string& name, const std::vector& shells, + const std::string& ecp_name, const std::vector& ecp_shells, + const std::vector& ecp_electrons, + const std::string& aux_name, const std::vector& aux_shells, + const Structure& structure, + AOType basis_type = AOType::Spherical); + + /** + * @brief Constructor with shells, ECP shells, ECP metadata, auxiliary shells, + * auxiliary name, and structure shared pointer + * @param name Name of the basis set + * @param shells Vector of shells to initialize the basis set with + * @param ecp_name Name of the ECP basis set + * @param ecp_shells Vector of ECP shells to initialize the basis set with + * @param ecp_electrons Vector containing numbers of ECP electrons for each + * atom + * @param aux_name Name of the auxiliary basis set + * @param aux_shells Vector of auxiliary shells (e.g., for density fitting) + * @param structure Shared pointer to the molecular structure + * @param basis_type Whether to use spherical or cartesian atomic orbitals + */ + BasisSet(const std::string& name, const std::vector& shells, + const std::string& ecp_name, const std::vector& ecp_shells, + const std::vector& ecp_electrons, + const std::string& aux_name, const std::vector& aux_shells, + std::shared_ptr structure, + AOType basis_type = AOType::Spherical); /** * @brief Default destructor */ @@ -435,6 +507,100 @@ class BasisSet : public DataClass, std::shared_ptr structure, AOType atomic_orbital_type = AOType::Spherical); + + + /** + * @brief Create a basis set from a basis name and auxiliary basis name + * @param basis_name Name of the basis set (e.g., "6-31G", "cc-pVDZ") + * @param aux_basis_name Name of the auxiliary basis set (e.g., "cc-pVDZ-RIFIT") + * @param structure The molecular structure + * @param atomic_orbital_type Whether to use spherical or cartesian atomic + * orbitals + * @return Shared pointer to the created BasisSet + */ + static std::shared_ptr from_basis_name( + const std::string& basis_name, const std::string& aux_basis_name, const Structure& structure, + AOType atomic_orbital_type = AOType::Spherical); + + /** + * @brief Create a basis set from a basis name and auxiliary basis name + * @param basis_name Name of the basis set (e.g., "6-31G", "cc-pVDZ") + * @param aux_basis_name Name of the auxiliary basis set (e.g., "cc-pVDZ-RIFIT") + * @param structure Shared pointer to the molecular structure + * @param atomic_orbital_type Whether to use spherical or cartesian atomic + * orbitals + * @return Shared pointer to the created BasisSet + */ + static std::shared_ptr from_basis_name( + std::string basis_name, const std::string& aux_basis_name, std::shared_ptr structure, + AOType atomic_orbital_type = AOType::Spherical); + + /** + * @brief Create a basis set from element-to-basis and element-to-auxiliary + * maps + * @param element_to_basis_map Mapping from element symbols to basis set names + * @param element_to_aux_basis_map Mapping from element symbols to auxiliary + * basis set names + * @param structure The molecular structure + * @param atomic_orbital_type Whether to use spherical or cartesian atomic + * orbitals + * @return Shared pointer to the created BasisSet + */ + static std::shared_ptr from_element_map( + const std::map& element_to_basis_map, + const std::map& element_to_aux_basis_map, + const Structure& structure, + AOType atomic_orbital_type = AOType::Spherical); + + /** + * @brief Create a basis set from element-to-basis and element-to-auxiliary + * maps + * @param element_to_basis_map Mapping from element symbols to basis set names + * @param element_to_aux_basis_map Mapping from element symbols to auxiliary + * basis set names + * @param structure Shared pointer to the molecular structure + * @param atomic_orbital_type Whether to use spherical or cartesian atomic + * orbitals + * @return Shared pointer to the created BasisSet + */ + static std::shared_ptr from_element_map( + const std::map& element_to_basis_map, + const std::map& element_to_aux_basis_map, + std::shared_ptr structure, + AOType atomic_orbital_type = AOType::Spherical); + + /** + * @brief Create a basis set from index-to-basis and index-to-auxiliary maps + * @param index_to_basis_map Mapping from atom indices to basis set names + * @param index_to_aux_basis_map Mapping from atom indices to auxiliary basis + * set names + * @param structure The molecular structure + * @param atomic_orbital_type Whether to use spherical or cartesian atomic + * orbitals + * @return Shared pointer to the created BasisSet + */ + static std::shared_ptr from_index_map( + const std::map& index_to_basis_map, + const std::map& index_to_aux_basis_map, + const Structure& structure, + AOType atomic_orbital_type = AOType::Spherical); + + /** + * @brief Create a basis set from index-to-basis and index-to-auxiliary maps + * @param index_to_basis_map Mapping from atom indices to basis set names + * @param index_to_aux_basis_map Mapping from atom indices to auxiliary basis + * set names + * @param structure Shared pointer to the molecular structure + * @param atomic_orbital_type Whether to use spherical or cartesian atomic + * orbitals + * @return Shared pointer to the created BasisSet + */ + static std::shared_ptr from_index_map( + const std::map& index_to_basis_map, + const std::map& index_to_aux_basis_map, + std::shared_ptr structure, + AOType atomic_orbital_type = AOType::Spherical); + /** * @brief Get the basis type * @return Current basis type (spherical or cartesian) @@ -507,6 +673,40 @@ class BasisSet : public DataClass, */ bool has_ecp_shells() const; + + /** + * @brief Get all auxiliary shells (flattened from per-atom storage) + * @return Vector of all auxiliary shells + */ + std::vector get_aux_shells() const; + + /** + * @brief Get auxiliary shells for a specific atom + * @param atom_index Index of the atom + * @return Vector of auxiliary shells for this atom + */ + const std::vector& get_aux_shells_for_atom(size_t atom_index) const; + + /** + * @brief Get a specific auxiliary shell by global index + * @param shell_index Global index of the auxiliary shell + * @return Reference to the auxiliary shell + * @throws std::out_of_range if index is invalid + */ + const Shell& get_aux_shell(size_t shell_index) const; + + /** + * @brief Get total number of auxiliary shells across all atoms + * @return Total number of auxiliary shells + */ + size_t get_num_aux_shells() const; + + /** + * @brief Check if this basis set has an auxiliary basis + * @return True if there are any auxiliary shells + */ + bool has_aux_basis() const; + /** * @brief Get the shell index and magnetic quantum number for a atomic orbital * index @@ -623,6 +823,12 @@ class BasisSet : public DataClass, */ bool has_structure() const; + /** + * @brief Get the auxiliary basis set name + * @return Name of the auxiliary basis set + */ + const std::string& get_aux_name() const; + /** * @brief Get the ECP name * @return Name of the ECP @@ -812,6 +1018,13 @@ class BasisSet : public DataClass, /// ECP shells organized by atom index - each atom has a vector of ECP shells std::vector> _ecp_shells_per_atom; + /// Auxiliary shells organized by atom index - each atom has a vector of + /// auxiliary shells + std::vector> _aux_shells_per_atom; + + /// Auxiliary basis set name + std::string _aux_name; + /// Effective Core Potential (ECP) name (basis set name) std::string _ecp_name; diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index 9ceb43bc2..d2dd2eddc 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -319,14 +319,7 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, QDK_LOG_TRACE_ENTERING(); } -BasisSet::BasisSet(const std::string& name, const Structure& structure, - AOType atomic_orbital_type) - : BasisSet(name, std::make_shared(structure), - atomic_orbital_type) { - QDK_LOG_TRACE_ENTERING(); -} - -BasisSet::BasisSet(const std::string& name, +BasisSet::BasisSet(const std::string& name, const std::vector& shells, std::shared_ptr structure, AOType atomic_orbital_type) : _name(name), @@ -334,13 +327,22 @@ BasisSet::BasisSet(const std::string& name, _structure(structure), _ecp_name("none") { QDK_LOG_TRACE_ENTERING(); - if (_name.empty()) { - throw std::invalid_argument("BasisSet name cannot be empty"); - } if (!structure) { throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); } + // Organize shells by atom index + for (const auto& shell : shells) { + size_t atom_index = shell.atom_index; + + // Ensure we have enough space for this atom + if (atom_index >= _shells_per_atom.size()) { + _shells_per_atom.resize(atom_index + 1); + } + + _shells_per_atom[atom_index].push_back(shell); + } + // Initialize ECP electrons vector with zeros for each atom _ecp_electrons.resize(structure->get_num_atoms(), 0); @@ -350,6 +352,15 @@ BasisSet::BasisSet(const std::string& name, } BasisSet::BasisSet(const std::string& name, const std::vector& shells, + const std::vector& ecp_shells, + const Structure& structure, AOType atomic_orbital_type) + : BasisSet(name, shells, ecp_shells, std::make_shared(structure), + atomic_orbital_type) { + QDK_LOG_TRACE_ENTERING(); +} + +BasisSet::BasisSet(const std::string& name, const std::vector& shells, + const std::vector& ecp_shells, std::shared_ptr structure, AOType atomic_orbital_type) : _name(name), @@ -373,6 +384,18 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, _shells_per_atom[atom_index].push_back(shell); } + // Organize ECP shells by atom index + for (const auto& ecp_shell : ecp_shells) { + size_t atom_index = ecp_shell.atom_index; + + // Ensure we have enough space for this atom + if (atom_index >= _ecp_shells_per_atom.size()) { + _ecp_shells_per_atom.resize(atom_index + 1); + } + + _ecp_shells_per_atom[atom_index].push_back(ecp_shell); + } + // Initialize ECP electrons vector with zeros for each atom _ecp_electrons.resize(structure->get_num_atoms(), 0); @@ -382,26 +405,37 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, } BasisSet::BasisSet(const std::string& name, const std::vector& shells, + const std::string& ecp_name, const std::vector& ecp_shells, + const std::vector& ecp_electrons, const Structure& structure, AOType atomic_orbital_type) - : BasisSet(name, shells, ecp_shells, std::make_shared(structure), - atomic_orbital_type) { + : BasisSet(name, shells, ecp_name, ecp_shells, ecp_electrons, + std::make_shared(structure), atomic_orbital_type) { QDK_LOG_TRACE_ENTERING(); } BasisSet::BasisSet(const std::string& name, const std::vector& shells, + const std::string& ecp_name, const std::vector& ecp_shells, + const std::vector& ecp_electrons, std::shared_ptr structure, AOType atomic_orbital_type) : _name(name), _atomic_orbital_type(atomic_orbital_type), _structure(structure), - _ecp_name("none") { + _ecp_name(ecp_name), + _ecp_electrons(ecp_electrons) { QDK_LOG_TRACE_ENTERING(); if (!structure) { throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); } + if ((!ecp_shells.empty() || !ecp_electrons.empty() || !ecp_name.empty()) && + ecp_electrons.size() != structure->get_num_atoms()) { + throw std::invalid_argument( + "ECP electrons vector size must match number of atoms"); + } + // Organize shells by atom index for (const auto& shell : shells) { size_t atom_index = shell.atom_index; @@ -426,6 +460,61 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, _ecp_shells_per_atom[atom_index].push_back(ecp_shell); } + if (!_is_valid()) { + throw std::invalid_argument("Tried to generate invalid BasisSet"); + } +} + +BasisSet::BasisSet(const std::string& name, const std::vector& shells, + const std::vector& ecp_shells, + const std::vector& aux_shells, + const Structure& structure, AOType atomic_orbital_type) + : BasisSet(name, shells, ecp_shells, aux_shells, + std::make_shared(structure), atomic_orbital_type) { + QDK_LOG_TRACE_ENTERING(); +} + +BasisSet::BasisSet(const std::string& name, const std::vector& shells, + const std::vector& ecp_shells, + const std::vector& aux_shells, + std::shared_ptr structure, + AOType atomic_orbital_type) + : _name(name), + _atomic_orbital_type(atomic_orbital_type), + _structure(structure), + _ecp_name("none") { + QDK_LOG_TRACE_ENTERING(); + if (!structure) { + throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); + } + + // Organize shells by atom index + for (const auto& shell : shells) { + size_t atom_index = shell.atom_index; + if (atom_index >= _shells_per_atom.size()) { + _shells_per_atom.resize(atom_index + 1); + } + _shells_per_atom[atom_index].push_back(shell); + } + + // Organize ECP shells by atom index + for (const auto& ecp_shell : ecp_shells) { + size_t atom_index = ecp_shell.atom_index; + if (atom_index >= _ecp_shells_per_atom.size()) { + _ecp_shells_per_atom.resize(atom_index + 1); + } + _ecp_shells_per_atom[atom_index].push_back(ecp_shell); + } + + // Organize auxiliary shells by atom index + for (const auto& aux_shell : aux_shells) { + size_t atom_index = aux_shell.atom_index; + if (atom_index >= _aux_shells_per_atom.size()) { + _aux_shells_per_atom.resize(atom_index + 1); + } + _aux_shells_per_atom[atom_index].push_back(aux_shell); + } + // Initialize ECP electrons vector with zeros for each atom _ecp_electrons.resize(structure->get_num_atoms(), 0); @@ -438,9 +527,12 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, const std::string& ecp_name, const std::vector& ecp_shells, const std::vector& ecp_electrons, + const std::string& aux_name, + const std::vector& aux_shells, const Structure& structure, AOType atomic_orbital_type) - : BasisSet(name, shells, ecp_name, ecp_shells, ecp_electrons, - std::make_shared(structure), atomic_orbital_type) { + : BasisSet(name, shells, ecp_name, ecp_shells, ecp_electrons, aux_name, + aux_shells, std::make_shared(structure), + atomic_orbital_type) { QDK_LOG_TRACE_ENTERING(); } @@ -448,19 +540,23 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, const std::string& ecp_name, const std::vector& ecp_shells, const std::vector& ecp_electrons, + const std::string& aux_name, + const std::vector& aux_shells, std::shared_ptr structure, AOType atomic_orbital_type) : _name(name), _atomic_orbital_type(atomic_orbital_type), _structure(structure), _ecp_name(ecp_name), - _ecp_electrons(ecp_electrons) { + _ecp_electrons(ecp_electrons), + _aux_name(aux_name) { QDK_LOG_TRACE_ENTERING(); if (!structure) { throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); } - if (ecp_electrons.size() != structure->get_num_atoms()) { + if ((!ecp_shells.empty() || !ecp_electrons.empty() || !ecp_name.empty()) && + ecp_electrons.size() != structure->get_num_atoms()) { throw std::invalid_argument( "ECP electrons vector size must match number of atoms"); } @@ -468,27 +564,30 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, // Organize shells by atom index for (const auto& shell : shells) { size_t atom_index = shell.atom_index; - - // Ensure we have enough space for this atom if (atom_index >= _shells_per_atom.size()) { _shells_per_atom.resize(atom_index + 1); } - _shells_per_atom[atom_index].push_back(shell); } // Organize ECP shells by atom index for (const auto& ecp_shell : ecp_shells) { size_t atom_index = ecp_shell.atom_index; - - // Ensure we have enough space for this atom if (atom_index >= _ecp_shells_per_atom.size()) { _ecp_shells_per_atom.resize(atom_index + 1); } - _ecp_shells_per_atom[atom_index].push_back(ecp_shell); } + // Organize auxiliary shells by atom index + for (const auto& aux_shell : aux_shells) { + size_t atom_index = aux_shell.atom_index; + if (atom_index >= _aux_shells_per_atom.size()) { + _aux_shells_per_atom.resize(atom_index + 1); + } + _aux_shells_per_atom[atom_index].push_back(aux_shell); + } + if (!_is_valid()) { throw std::invalid_argument("Tried to generate invalid BasisSet"); } @@ -703,13 +802,199 @@ std::shared_ptr BasisSet::from_index_map( structure, atomic_orbital_type); } +std::shared_ptr BasisSet::from_basis_name( + const std::string& name, const std::string& aux_basis_name, + const Structure& structure, AOType atomic_orbital_type) { + return BasisSet::from_basis_name(name, aux_basis_name, + std::make_shared(structure), + atomic_orbital_type); +} + +std::shared_ptr BasisSet::from_basis_name( + std::string basis_name, const std::string& aux_basis_name, + std::shared_ptr structure, AOType atomic_orbital_type) { + if (!structure) { + throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); + } + // convert names to lowercase + std::transform(basis_name.begin(), basis_name.end(), basis_name.begin(), + ::tolower); + std::string aux_name_lower = aux_basis_name; + std::transform(aux_name_lower.begin(), aux_name_lower.end(), + aux_name_lower.begin(), ::tolower); + + std::vector all_basis_shells; + std::vector all_ecp_shells; + std::vector all_ecp_electrons; + std::vector all_aux_shells; + + auto nuclear_charges = structure->get_nuclear_charges(); + for (size_t atom_index = 0; atom_index < nuclear_charges.size(); + ++atom_index) { + double nuclear_charge = nuclear_charges[atom_index]; + + auto [shells, ecp_shells, ecp_electrons] = + detail::get_basis_for_nuclear_charge(nuclear_charge, basis_name, + atom_index); + for (const auto& sh : shells) { + all_basis_shells.push_back(sh); + } + all_ecp_electrons.push_back(ecp_electrons); + for (const auto& sh : ecp_shells) { + all_ecp_shells.push_back(sh); + } + + // Get auxiliary basis shells (only the orbital shells, ignore ECP) + auto [aux_shells, aux_ecp_shells, aux_ecp_electrons] = + detail::get_basis_for_nuclear_charge(nuclear_charge, aux_name_lower, + atom_index); + for (const auto& sh : aux_shells) { + all_aux_shells.push_back(sh); + } + } + + // sort shells + detail::sort_shells_inplace(all_basis_shells); + detail::sort_shells_inplace(all_ecp_shells); + detail::sort_shells_inplace(all_aux_shells); + + return std::make_shared(basis_name, all_basis_shells, basis_name, + all_ecp_shells, all_ecp_electrons, + aux_name_lower, all_aux_shells, + structure, atomic_orbital_type); +} + +std::shared_ptr BasisSet::from_element_map( + const std::map& element_to_basis_map, + const std::map& element_to_aux_basis_map, + const Structure& structure, AOType atomic_orbital_type) { + return BasisSet::from_element_map(element_to_basis_map, + element_to_aux_basis_map, + std::make_shared(structure), + atomic_orbital_type); +} + +std::shared_ptr BasisSet::from_element_map( + const std::map& element_to_basis_map, + const std::map& element_to_aux_basis_map, + std::shared_ptr structure, AOType atomic_orbital_type) { + if (!structure) { + throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); + } + + // convert element maps to index maps + std::map tmp_basis_index_map; + std::map tmp_aux_index_map; + auto elements = structure->get_atomic_symbols(); + for (size_t atom_index = 0; atom_index < elements.size(); ++atom_index) { + auto it_basis = element_to_basis_map.find(elements[atom_index]); + if (it_basis == element_to_basis_map.end()) { + throw std::invalid_argument("No basis set specified for element: " + + elements[atom_index]); + } + tmp_basis_index_map[atom_index] = it_basis->second; + + auto it_aux = element_to_aux_basis_map.find(elements[atom_index]); + if (it_aux == element_to_aux_basis_map.end()) { + throw std::invalid_argument( + "No auxiliary basis set specified for element: " + + elements[atom_index]); + } + tmp_aux_index_map[atom_index] = it_aux->second; + } + + return BasisSet::from_index_map(tmp_basis_index_map, tmp_aux_index_map, + structure, atomic_orbital_type); +} + +std::shared_ptr BasisSet::from_index_map( + const std::map& index_to_basis_map, + const std::map& index_to_aux_basis_map, + const Structure& structure, AOType atomic_orbital_type) { + return BasisSet::from_index_map(index_to_basis_map, index_to_aux_basis_map, + std::make_shared(structure), + atomic_orbital_type); +} + +std::shared_ptr BasisSet::from_index_map( + const std::map& index_to_basis_map, + const std::map& index_to_aux_basis_map, + std::shared_ptr structure, AOType atomic_orbital_type) { + if (!structure) { + throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); + } + + std::vector all_basis_shells; + std::vector all_ecp_shells; + std::vector all_ecp_electrons; + std::vector all_aux_shells; + + auto nuclear_charges = structure->get_nuclear_charges(); + for (size_t atom_index = 0; atom_index < nuclear_charges.size(); + ++atom_index) { + double nuclear_charge = nuclear_charges[atom_index]; + + // Primary basis + auto it = index_to_basis_map.find(atom_index); + if (it == index_to_basis_map.end()) { + throw std::invalid_argument("No basis set specified for atom index: " + + std::to_string(atom_index)); + } + std::string tmp_basis_set_name = it->second; + std::transform(tmp_basis_set_name.begin(), tmp_basis_set_name.end(), + tmp_basis_set_name.begin(), ::tolower); + + auto [shells, ecp_shells, ecp_electrons] = + detail::get_basis_for_nuclear_charge(nuclear_charge, tmp_basis_set_name, + atom_index); + for (const auto& sh : shells) { + all_basis_shells.push_back(sh); + } + all_ecp_electrons.push_back(ecp_electrons); + for (const auto& sh : ecp_shells) { + all_ecp_shells.push_back(sh); + } + + // Auxiliary basis + auto it_aux = index_to_aux_basis_map.find(atom_index); + if (it_aux == index_to_aux_basis_map.end()) { + throw std::invalid_argument( + "No auxiliary basis set specified for atom index: " + + std::to_string(atom_index)); + } + std::string tmp_aux_name = it_aux->second; + std::transform(tmp_aux_name.begin(), tmp_aux_name.end(), + tmp_aux_name.begin(), ::tolower); + + auto [aux_shells, aux_ecp_shells, aux_ecp_electrons] = + detail::get_basis_for_nuclear_charge(nuclear_charge, tmp_aux_name, + atom_index); + for (const auto& sh : aux_shells) { + all_aux_shells.push_back(sh); + } + } + + // sort shells + detail::sort_shells_inplace(all_basis_shells); + detail::sort_shells_inplace(all_ecp_shells); + detail::sort_shells_inplace(all_aux_shells); + + return std::make_shared( + std::string(BasisSet::custom_name), all_basis_shells, + std::string(BasisSet::custom_ecp_name), all_ecp_shells, all_ecp_electrons, + std::string(BasisSet::custom_name), all_aux_shells, + structure, atomic_orbital_type); +} + BasisSet::BasisSet(const BasisSet& other) : _name(other._name), _atomic_orbital_type(other._atomic_orbital_type), _shells_per_atom(other._shells_per_atom), _ecp_name(other._ecp_name), _ecp_shells_per_atom(other._ecp_shells_per_atom), - _ecp_electrons(other._ecp_electrons) { + _ecp_electrons(other._ecp_electrons), + _aux_name(other._aux_name), + _aux_shells_per_atom(other._aux_shells_per_atom) { QDK_LOG_TRACE_ENTERING(); if (other._structure) { _structure = std::make_shared(*other._structure); @@ -730,6 +1015,8 @@ BasisSet& BasisSet::operator=(const BasisSet& other) { _ecp_name = other._ecp_name; _ecp_shells_per_atom = other._ecp_shells_per_atom; _ecp_electrons = other._ecp_electrons; + _aux_name = other._aux_name; + _aux_shells_per_atom = other._aux_shells_per_atom; if (other._structure) { _structure = std::make_shared(*other._structure); } else { @@ -859,6 +1146,66 @@ bool BasisSet::has_ecp_shells() const { return get_num_ecp_shells() > 0; } +std::vector BasisSet::get_aux_shells() const { + QDK_LOG_TRACE_ENTERING(); + std::vector all_aux_shells; + + for (const auto& atom_aux_shells : _aux_shells_per_atom) { + for (const auto& shell : atom_aux_shells) { + all_aux_shells.push_back(shell); + } + } + + return all_aux_shells; +} + +const std::vector& BasisSet::get_aux_shells_for_atom( + size_t atom_index) const { + QDK_LOG_TRACE_ENTERING(); + _validate_atom_index(atom_index); + if (atom_index >= _aux_shells_per_atom.size()) { + static const std::vector empty_vector; + return empty_vector; + } + return _aux_shells_per_atom[atom_index]; +} + +const Shell& BasisSet::get_aux_shell(size_t shell_index) const { + QDK_LOG_TRACE_ENTERING(); + size_t total_aux_shells = get_num_aux_shells(); + if (shell_index >= total_aux_shells) { + throw std::out_of_range("Auxiliary shell index " + + std::to_string(shell_index) + + " out of range [0, " + + std::to_string(total_aux_shells) + ")"); + } + + size_t current_index = 0; + for (const auto& atom_aux_shells : _aux_shells_per_atom) { + if (shell_index < current_index + atom_aux_shells.size()) { + return atom_aux_shells[shell_index - current_index]; + } + current_index += atom_aux_shells.size(); + } + + // Should never reach here if validation worked correctly + throw std::out_of_range("Auxiliary shell index not found"); +} + +size_t BasisSet::get_num_aux_shells() const { + QDK_LOG_TRACE_ENTERING(); + size_t total = 0; + for (const auto& atom_aux_shells : _aux_shells_per_atom) { + total += atom_aux_shells.size(); + } + return total; +} + +bool BasisSet::has_aux_basis() const { + QDK_LOG_TRACE_ENTERING(); + return get_num_aux_shells() > 0; +} + std::pair BasisSet::get_atomic_orbital_info( size_t atomic_orbital_index) const { QDK_LOG_TRACE_ENTERING(); @@ -1098,6 +1445,11 @@ bool BasisSet::has_structure() const { return _structure != nullptr; } +const std::string& BasisSet::get_aux_name() const { + QDK_LOG_TRACE_ENTERING(); + return _aux_name; +} + const std::string& BasisSet::get_ecp_name() const { QDK_LOG_TRACE_ENTERING(); return _ecp_name; @@ -1176,6 +1528,22 @@ std::string BasisSet::get_summary() const { oss << "Total atomic orbitals: " << get_num_atomic_orbitals() << "\n"; oss << "Number of atoms: " << get_num_atoms() << "\n"; + oss << "Contains ECP: " << (has_ecp_electrons() ? "Yes" : "No") << "\n"; + if (has_ecp_electrons()) { + oss << "ECP name: " << _ecp_name << "\n"; + oss << "Number of ECP shells: " << get_num_ecp_shells() << "\n"; + oss << "ECP electrons: "; + for (size_t e : _ecp_electrons) { + oss << e << " "; + } + oss << "\n"; + } + oss << "Contains auxiliary basis: " << (has_aux_basis() ? "Yes" : "No") << "\n"; + if (has_aux_basis()) { + oss << "Auxiliary basis name: " << _aux_name << "\n"; + oss << "Number of auxiliary shells: " << get_num_aux_shells() << "\n"; + } + if (has_structure()) { oss << "Associated structure: " << _structure->get_num_atoms() << " atoms\n"; @@ -1512,6 +1880,73 @@ void BasisSet::to_hdf5(H5::Group& group) const { } } + // Save auxiliary shell data (no rpowers for aux basis) + if (get_num_aux_shells() > 0) { + H5::Group aux_shell_group = group.createGroup("aux_shells"); + + auto all_aux_shells = get_aux_shells(); + unsigned num_aux_shells = all_aux_shells.size(); + hsize_t aux_dims[1] = {num_aux_shells}; + H5::DataSpace aux_dataspace(1, aux_dims); + + H5::DataSet aux_atom_indices = aux_shell_group.createDataSet( + "atom_indices", H5::PredType::NATIVE_UINT, aux_dataspace); + H5::DataSet aux_orbital_types = aux_shell_group.createDataSet( + "orbital_types", H5::PredType::NATIVE_INT, aux_dataspace); + H5::DataSet aux_num_primitives = aux_shell_group.createDataSet( + "num_primitives", H5::PredType::NATIVE_UINT, aux_dataspace); + + std::vector aux_atom_idx_data; + std::vector aux_orbital_type_data; + std::vector aux_num_prim_data; + std::vector aux_all_exponents; + std::vector aux_all_coefficients; + + aux_atom_idx_data.reserve(num_aux_shells); + aux_orbital_type_data.reserve(num_aux_shells); + aux_num_prim_data.reserve(num_aux_shells); + for (const auto& aux_shell : all_aux_shells) { + aux_atom_idx_data.push_back(aux_shell.atom_index); + aux_orbital_type_data.push_back( + static_cast(aux_shell.orbital_type)); + aux_num_prim_data.push_back(aux_shell.exponents.size()); + + for (unsigned i = 0; i < aux_shell.exponents.size(); ++i) { + aux_all_exponents.push_back(aux_shell.exponents(i)); + aux_all_coefficients.push_back(aux_shell.coefficients(i)); + } + } + + aux_atom_indices.write(aux_atom_idx_data.data(), + H5::PredType::NATIVE_UINT); + aux_orbital_types.write(aux_orbital_type_data.data(), + H5::PredType::NATIVE_INT); + aux_num_primitives.write(aux_num_prim_data.data(), + H5::PredType::NATIVE_UINT); + + if (!aux_all_exponents.empty()) { + hsize_t aux_prim_dims[1] = {aux_all_exponents.size()}; + H5::DataSpace aux_prim_dataspace(1, aux_prim_dims); + + H5::DataSet aux_exponents = aux_shell_group.createDataSet( + "exponents", H5::PredType::NATIVE_DOUBLE, aux_prim_dataspace); + H5::DataSet aux_coefficients = aux_shell_group.createDataSet( + "coefficients", H5::PredType::NATIVE_DOUBLE, aux_prim_dataspace); + + aux_exponents.write(aux_all_exponents.data(), + H5::PredType::NATIVE_DOUBLE); + aux_coefficients.write(aux_all_coefficients.data(), + H5::PredType::NATIVE_DOUBLE); + } + } + + // Save auxiliary basis name if present + if (has_aux_basis() || !_aux_name.empty()) { + H5::Attribute aux_name_attr = + group.createAttribute("aux_name", string_type, scalar_space); + aux_name_attr.write(string_type, _aux_name); + } + // Save nested structure if present if (has_structure()) { H5::Group structure_group = group.createGroup("structure"); @@ -1777,12 +2212,109 @@ std::shared_ptr BasisSet::from_hdf5(H5::Group& group) { ecp_electrons_ds.read(ecp_electrons.data(), H5::PredType::NATIVE_UINT64); } - // Load nested structure if present + // Load auxiliary shells (no rpowers) + std::vector aux_shells; + + if (group.nameExists("aux_shells")) { + H5::Group aux_shell_group = group.openGroup("aux_shells"); + + H5::DataSet aux_atom_indices = + aux_shell_group.openDataSet("atom_indices"); + H5::DataSpace aux_dataspace = aux_atom_indices.getSpace(); + + hsize_t aux_dims[1]; + aux_dataspace.getSimpleExtentDims(aux_dims); + unsigned num_aux_shells = aux_dims[0]; + + if (num_aux_shells > 0) { + std::vector aux_atom_idx_data(num_aux_shells); + std::vector aux_orbital_type_data(num_aux_shells); + std::vector aux_num_prim_data(num_aux_shells); + + aux_atom_indices.read(aux_atom_idx_data.data(), + H5::PredType::NATIVE_UINT); + + H5::DataSet aux_orbital_types = + aux_shell_group.openDataSet("orbital_types"); + aux_orbital_types.read(aux_orbital_type_data.data(), + H5::PredType::NATIVE_INT); + + H5::DataSet aux_num_primitives = + aux_shell_group.openDataSet("num_primitives"); + aux_num_primitives.read(aux_num_prim_data.data(), + H5::PredType::NATIVE_UINT); + + std::vector aux_all_exponents; + std::vector aux_all_coefficients; + + if (aux_shell_group.nameExists("exponents") && + aux_shell_group.nameExists("coefficients")) { + H5::DataSet aux_exponents = + aux_shell_group.openDataSet("exponents"); + H5::DataSet aux_coefficients = + aux_shell_group.openDataSet("coefficients"); + + H5::DataSpace aux_exp_space = aux_exponents.getSpace(); + hsize_t aux_exp_dims[1]; + aux_exp_space.getSimpleExtentDims(aux_exp_dims); + + aux_all_exponents.resize(aux_exp_dims[0]); + aux_all_coefficients.resize(aux_exp_dims[0]); + + aux_exponents.read(aux_all_exponents.data(), + H5::PredType::NATIVE_DOUBLE); + aux_all_coefficients.resize(aux_exp_dims[0]); + aux_coefficients.read(aux_all_coefficients.data(), + H5::PredType::NATIVE_DOUBLE); + } + + unsigned aux_prim_offset = 0; + + for (unsigned i = 0; i < num_aux_shells; ++i) { + unsigned num_prims = aux_num_prim_data[i]; + + Eigen::VectorXd shell_exponents(num_prims); + Eigen::VectorXd shell_coefficients(num_prims); + + for (unsigned j = 0; j < num_prims; ++j) { + if (aux_prim_offset + j < aux_all_exponents.size()) { + shell_exponents(j) = aux_all_exponents[aux_prim_offset + j]; + shell_coefficients(j) = + aux_all_coefficients[aux_prim_offset + j]; + } + } + aux_prim_offset += num_prims; + + Shell aux_shell(aux_atom_idx_data[i], + static_cast(aux_orbital_type_data[i]), + shell_exponents, shell_coefficients); + + aux_shells.push_back(aux_shell); + } + } + } + + // Load auxiliary basis name + std::string aux_name; + if (group.attrExists("aux_name")) { + H5::Attribute aux_name_attr = group.openAttribute("aux_name"); + aux_name_attr.read(string_type, aux_name); + } + + // Construct BasisSet handling all combinations: + // structure: present or absent + // ecp: present (with or without metadata) or absent + // aux: present or absent std::shared_ptr basis_set; if (group.nameExists("structure")) { H5::Group structure_group = group.openGroup("structure"); auto structure = Structure::from_hdf5(structure_group); - if (!ecp_shells.empty()) { + if (!aux_shells.empty()) { + // Aux exists: use full constructor; ecp params may be empty + basis_set = std::make_shared( + name, shells, ecp_name, ecp_shells, ecp_electrons, aux_name, + aux_shells, *structure, atomic_orbital_type); + } else if (!ecp_shells.empty()) { if (!ecp_name.empty() && !ecp_electrons.empty()) { basis_set = std::make_shared( name, shells, ecp_name, ecp_shells, ecp_electrons, *structure, @@ -1828,8 +2360,10 @@ nlohmann::json BasisSet::to_json() const { !_shells_per_atom[atom_idx].empty(); bool has_ecp_shells = atom_idx < _ecp_shells_per_atom.size() && !_ecp_shells_per_atom[atom_idx].empty(); + bool has_aux_shells = atom_idx < _aux_shells_per_atom.size() && + !_aux_shells_per_atom[atom_idx].empty(); - if (has_shells || has_ecp_shells) { + if (has_shells || has_ecp_shells || has_aux_shells) { nlohmann::json atom_json; atom_json["atom_index"] = atom_idx; @@ -1889,6 +2423,30 @@ nlohmann::json BasisSet::to_json() const { } } + // Serialize auxiliary shells + if (has_aux_shells) { + const auto& atom_aux_shells = _aux_shells_per_atom[atom_idx]; + atom_json["aux_shells"] = nlohmann::json::array(); + + for (const auto& aux_shell : atom_aux_shells) { + nlohmann::json aux_shell_json; + aux_shell_json["orbital_type"] = + orbital_type_to_string(aux_shell.orbital_type); + + // Serialize primitive data as separate arrays (no rpowers for aux) + std::vector exp_vec( + aux_shell.exponents.data(), + aux_shell.exponents.data() + aux_shell.exponents.size()); + std::vector coeff_vec( + aux_shell.coefficients.data(), + aux_shell.coefficients.data() + aux_shell.coefficients.size()); + aux_shell_json["exponents"] = exp_vec; + aux_shell_json["coefficients"] = coeff_vec; + + atom_json["aux_shells"].push_back(aux_shell_json); + } + } + j["atoms"].push_back(atom_json); } } @@ -1898,6 +2456,10 @@ nlohmann::json BasisSet::to_json() const { j["ecp_electrons"] = _ecp_electrons; } + if (has_aux_basis() || !_aux_name.empty()) { + j["aux_name"] = _aux_name; + } + if (has_structure()) { j["structure"] = _structure->to_json(); } @@ -1926,9 +2488,10 @@ std::shared_ptr BasisSet::from_json(const nlohmann::json& j) { atomic_orbital_type = AOType::Spherical; } - // Collect all shells and ECP shells + // Collect all shells, ECP shells, and auxiliary shells std::vector shells; std::vector ecp_shells; + std::vector aux_shells; // Try to load new per-atom format first if (j.contains("atoms") && j["atoms"].is_array()) { @@ -2018,6 +2581,34 @@ std::shared_ptr BasisSet::from_json(const nlohmann::json& j) { } } } + + // Load auxiliary shells (no rpowers) + if (atom_json.contains("aux_shells") && + atom_json["aux_shells"].is_array()) { + for (const auto& aux_shell_json : atom_json["aux_shells"]) { + if (aux_shell_json.contains("exponents") && + aux_shell_json.contains("coefficients") && + aux_shell_json["exponents"].is_array() && + aux_shell_json["coefficients"].is_array()) { + auto exp_vec = + aux_shell_json["exponents"].get>(); + auto coeff_vec = + aux_shell_json["coefficients"].get>(); + Eigen::VectorXd shell_exponents = + Eigen::Map(exp_vec.data(), + exp_vec.size()); + Eigen::VectorXd shell_coefficients = + Eigen::Map(coeff_vec.data(), + coeff_vec.size()); + + Shell aux_shell( + atom_index, + string_to_orbital_type(aux_shell_json["orbital_type"]), + shell_exponents, shell_coefficients); + aux_shells.push_back(aux_shell); + } + } + } } } // Legacy support - flat shell list @@ -2119,11 +2710,25 @@ std::shared_ptr BasisSet::from_json(const nlohmann::json& j) { ecp_electrons = j["ecp_electrons"].get>(); } - // Create the BasisSet with or without structure + // Load auxiliary basis name if present + std::string aux_name; + if (j.contains("aux_name")) { + aux_name = j["aux_name"]; + } + + // Construct BasisSet handling all combinations: + // structure: present or absent + // ecp: present (with or without metadata) or absent + // aux: present or absent std::shared_ptr basis_set; if (j.contains("structure")) { auto structure = Structure::from_json(j["structure"]); - if (!ecp_shells.empty()) { + if (!aux_shells.empty()) { + // Aux exists: use full constructor; ecp params may be empty + basis_set = std::make_shared( + name, shells, ecp_name, ecp_shells, ecp_electrons, aux_name, + aux_shells, *structure, atomic_orbital_type); + } else if (!ecp_shells.empty()) { if (!ecp_name.empty() && !ecp_electrons.empty()) { basis_set = std::make_shared( name, shells, ecp_name, ecp_shells, ecp_electrons, *structure, @@ -2138,7 +2743,6 @@ std::shared_ptr BasisSet::from_json(const nlohmann::json& j) { } } else { if (!ecp_shells.empty()) { - // Create a minimal structure for ecp_shells constructor throw std::runtime_error( "Cannot create BasisSet with ECP shells but without structure"); } diff --git a/cpp/tests/test_basis_set.cpp b/cpp/tests/test_basis_set.cpp index e32328c15..d1aa06a43 100644 --- a/cpp/tests/test_basis_set.cpp +++ b/cpp/tests/test_basis_set.cpp @@ -80,16 +80,16 @@ TEST_F(BasisSetTest, Constructors) { std::vector symbols = {"H"}; Structure structure(coords, symbols); - // Constructor with empty name and structure - EXPECT_THROW(BasisSet basis1("", structure), std::invalid_argument); + // // Constructor with empty name and structure + // EXPECT_THROW(BasisSet basis1("", structure), std::invalid_argument); - // Constructor with name and structure should throw (empty basis invalid) - EXPECT_THROW(BasisSet basis2("6-31G", structure), std::invalid_argument); + // // Constructor with name and structure should throw (empty basis invalid) + // EXPECT_THROW(BasisSet basis2("6-31G", structure), std::invalid_argument); - // Constructor with name, structure and basis type should throw (empty basis - // invalid) - EXPECT_THROW(BasisSet basis3("6-31G", structure, AOType::Cartesian), - std::invalid_argument); + // // Constructor with name, structure and basis type should throw (empty basis + // // invalid) + // EXPECT_THROW(BasisSet basis3("6-31G", structure, AOType::Cartesian), + // std::invalid_argument); // Constructor with shells should work std::vector shells; @@ -230,9 +230,9 @@ TEST_F(BasisSetTest, AOTypeManagement) { std::vector symbols = {"H"}; Structure structure(coords, symbols); - // Test default basis type (spherical) - empty basis sets are invalid - EXPECT_THROW(BasisSet basis_spherical("test", structure), - std::invalid_argument); + // // Test default basis type (spherical) - empty basis sets are invalid + // EXPECT_THROW(BasisSet basis_spherical("test", structure), + // std::invalid_argument); // Create cartesian basis set std::vector shells; @@ -393,8 +393,8 @@ TEST_F(BasisSetTest, Validation) { std::vector symbols = {"H"}; Structure structure(coords, symbols); - // Empty basis is invalid - EXPECT_THROW(BasisSet empty_basis("test", structure), std::invalid_argument); + // // Empty basis is invalid + // EXPECT_THROW(BasisSet empty_basis("test", structure), std::invalid_argument); // Add a shell std::vector shells; @@ -1841,3 +1841,345 @@ TEST_F(BasisSetTest, DataTypeName) { EXPECT_EQ(basis.get_data_type_name(), "basis_set"); } + +TEST_F(BasisSetTest, AuxiliaryBasisSetAccessors) { + // Create a basis set without auxiliary + std::vector shells; + shells.emplace_back( + Shell(0, OrbitalType::S, std::vector{1.0}, std::vector{2.0})); + + std::vector coords = {{0.0, 0.0, 0.0}}; + std::vector symbols = {"H"}; + Structure structure(coords, symbols); + + BasisSet basis("test", shells, structure); + + // Default: no auxiliary basis + EXPECT_FALSE(basis.has_aux_basis()); + EXPECT_EQ(0u, basis.get_num_aux_shells()); +} + +TEST_F(BasisSetTest, AuxiliaryBasisFromSharedPtrConstructor) { + // Constructor: BasisSet(name, shells, ecp_shells, aux_shells, structure) + auto structure = testing::create_water_structure(); + + // Create primary shells + std::vector shells; + shells.emplace_back( + Shell(0, OrbitalType::S, std::vector{1.0}, std::vector{1.0})); + shells.emplace_back( + Shell(1, OrbitalType::S, std::vector{1.0}, std::vector{1.0})); + shells.emplace_back( + Shell(2, OrbitalType::S, std::vector{1.0}, std::vector{1.0})); + + // Create auxiliary shells + std::vector aux_shells; + aux_shells.emplace_back( + Shell(0, OrbitalType::S, std::vector{2.0}, std::vector{1.0})); + aux_shells.emplace_back( + Shell(0, OrbitalType::P, std::vector{1.5}, std::vector{0.8})); + aux_shells.emplace_back( + Shell(1, OrbitalType::S, std::vector{2.5}, std::vector{1.2})); + aux_shells.emplace_back( + Shell(2, OrbitalType::S, std::vector{2.0}, std::vector{1.0})); + + // Empty ECP shells + std::vector ecp_shells; + + // Create primary basis with aux (ecp_shells empty) + BasisSet basis("custom-primary", shells, ecp_shells, aux_shells, *structure); + + EXPECT_EQ("custom-primary", basis.get_name()); + EXPECT_EQ(3u, basis.get_num_shells()); + EXPECT_TRUE(basis.has_aux_basis()); + EXPECT_EQ(4u, basis.get_num_aux_shells()); +} + +TEST_F(BasisSetTest, AuxiliaryBasisFromShellsConstructor) { + // Constructor: BasisSet(name, shells, ecp_shells, aux_shells, structure) + auto structure = testing::create_water_structure(); + + // Primary shells + std::vector shells; + shells.emplace_back( + Shell(0, OrbitalType::S, std::vector{1.0}, std::vector{1.0})); + shells.emplace_back( + Shell(1, OrbitalType::S, std::vector{1.0}, std::vector{1.0})); + shells.emplace_back( + Shell(2, OrbitalType::S, std::vector{1.0}, std::vector{1.0})); + + // Empty ECP shells + std::vector ecp_shells; + + // Auxiliary shells + std::vector aux_shells; + aux_shells.emplace_back( + Shell(0, OrbitalType::S, std::vector{3.0}, std::vector{1.5})); + aux_shells.emplace_back( + Shell(1, OrbitalType::S, std::vector{3.5}, std::vector{1.2})); + aux_shells.emplace_back( + Shell(2, OrbitalType::S, std::vector{3.0}, std::vector{1.5})); + + BasisSet basis("test-with-aux-shells", shells, ecp_shells, aux_shells, + *structure); + + EXPECT_EQ("test-with-aux-shells", basis.get_name()); + EXPECT_EQ(3u, basis.get_num_shells()); + EXPECT_TRUE(basis.has_aux_basis()); + EXPECT_EQ(3u, basis.get_num_aux_shells()); +} + +TEST_F(BasisSetTest, FullConstructorWithECPAndAux) { + // Full constructor: BasisSet(name, shells, ecp_name, ecp_shells, + // ecp_electrons, aux_name, aux_shells, structure) + std::vector coords = {{0.0, 0.0, 0.0}, {1.0, 0.0, 0.0}}; + std::vector symbols = {"Ag", "H"}; + Structure structure(coords, symbols); + + // Primary shells + std::vector shells; + shells.emplace_back( + Shell(0, OrbitalType::S, std::vector{1.0}, std::vector{1.0})); + shells.emplace_back( + Shell(1, OrbitalType::S, std::vector{1.0}, std::vector{1.0})); + + // ECP shells + std::vector ecp_shells; + Eigen::VectorXd exp(1), coeff(1); + Eigen::VectorXi rpow(1); + exp << 10.0; + coeff << 50.0; + rpow << 0; + ecp_shells.emplace_back(0, OrbitalType::S, exp, coeff, rpow); + + std::string ecp_name = "test-ecp"; + std::vector ecp_electrons = {28, 0}; + + // Auxiliary shells + std::string aux_name = "test-aux"; + std::vector aux_shells; + aux_shells.emplace_back( + Shell(0, OrbitalType::S, std::vector{5.0}, std::vector{2.0})); + aux_shells.emplace_back( + Shell(1, OrbitalType::S, std::vector{4.0}, std::vector{1.5})); + + BasisSet basis("full-test", shells, ecp_name, ecp_shells, ecp_electrons, + aux_name, aux_shells, structure); + + // Verify primary basis + EXPECT_EQ("full-test", basis.get_name()); + EXPECT_EQ(2u, basis.get_num_shells()); + + // Verify ECP + EXPECT_TRUE(basis.has_ecp_electrons()); + EXPECT_EQ("test-ecp", basis.get_ecp_name()); + EXPECT_EQ(28u, basis.get_ecp_electrons()[0]); + EXPECT_EQ(0u, basis.get_ecp_electrons()[1]); + EXPECT_TRUE(basis.has_ecp_shells()); + + // Verify auxiliary + EXPECT_TRUE(basis.has_aux_basis()); + EXPECT_EQ("test-aux", basis.get_aux_name()); + EXPECT_EQ(2u, basis.get_num_aux_shells()); +} + +TEST_F(BasisSetTest, FromElementMapWithAux) { + // Static factory: from_element_map(elem_map, aux_elem_map, structure) + auto structure = testing::create_water_structure(); + + std::map elem_map; + elem_map["O"] = "def2-svp"; + elem_map["H"] = "def2-svp"; + + std::map aux_elem_map; + aux_elem_map["O"] = "def2-universal-jfit"; + aux_elem_map["H"] = "def2-universal-jfit"; + + auto basis = BasisSet::from_element_map(elem_map, aux_elem_map, *structure); + + EXPECT_NE(nullptr, basis); + EXPECT_GT(basis->get_num_shells(), 0u); + EXPECT_TRUE(basis->has_aux_basis()); + EXPECT_GT(basis->get_num_aux_shells(), 0u); +} + +TEST_F(BasisSetTest, FromIndexMapWithAux) { + // Static factory: from_index_map(idx_map, aux_idx_map, structure) + auto structure = testing::create_water_structure(); + + std::map idx_map; + idx_map[0] = "def2-svp"; + idx_map[1] = "def2-svp"; + idx_map[2] = "def2-svp"; + + std::map aux_idx_map; + aux_idx_map[0] = "def2-universal-jfit"; + aux_idx_map[1] = "def2-universal-jfit"; + aux_idx_map[2] = "def2-universal-jfit"; + + auto basis = BasisSet::from_index_map(idx_map, aux_idx_map, *structure); + + EXPECT_NE(nullptr, basis); + EXPECT_GT(basis->get_num_shells(), 0u); + EXPECT_TRUE(basis->has_aux_basis()); + EXPECT_GT(basis->get_num_aux_shells(), 0u); +} + +TEST_F(BasisSetTest, AuxiliaryBasisCopyConstructorAndAssignment) { + auto structure = testing::create_water_structure(); + + // Create basis with auxiliary + auto basis_ptr = + BasisSet::from_basis_name("def2-svp", "def2-universal-jfit", *structure); + BasisSet& original = *basis_ptr; + + // Test copy constructor + BasisSet copy_constructed(original); + EXPECT_TRUE(copy_constructed.has_aux_basis()); + EXPECT_EQ(original.get_num_aux_shells(), + copy_constructed.get_num_aux_shells()); + + // Test copy assignment + std::vector other_shells; + other_shells.emplace_back( + Shell(0, OrbitalType::S, std::vector{1.0}, std::vector{1.0})); + BasisSet assigned("other", other_shells); + EXPECT_FALSE(assigned.has_aux_basis()); + + assigned = original; + EXPECT_TRUE(assigned.has_aux_basis()); + EXPECT_EQ(original.get_num_aux_shells(), + assigned.get_num_aux_shells()); +} + +TEST_F(BasisSetTest, AuxiliaryBasisJSONSerialization) { + auto structure = testing::create_water_structure(); + + // Create basis with auxiliary via manual shells + std::vector shells; + shells.emplace_back( + Shell(0, OrbitalType::S, std::vector{1.0}, std::vector{1.0})); + shells.emplace_back( + Shell(1, OrbitalType::S, std::vector{1.0}, std::vector{1.0})); + shells.emplace_back( + Shell(2, OrbitalType::S, std::vector{1.0}, std::vector{1.0})); + + std::vector aux_shells; + aux_shells.emplace_back( + Shell(0, OrbitalType::S, std::vector{2.0}, std::vector{1.0})); + aux_shells.emplace_back( + Shell(0, OrbitalType::P, std::vector{1.5}, std::vector{0.8})); + aux_shells.emplace_back( + Shell(1, OrbitalType::S, std::vector{2.5}, std::vector{1.2})); + aux_shells.emplace_back( + Shell(2, OrbitalType::S, std::vector{2.0}, std::vector{1.0})); + + std::vector ecp_shells; // empty + + std::string aux_name = "my-aux"; + BasisSet basis("my-primary", shells, "", ecp_shells, {}, + aux_name, aux_shells, *structure); + + // In-memory JSON round-trip + auto json = basis.to_json(); + EXPECT_TRUE(json.contains("aux_name")); + + auto loaded = BasisSet::from_json(json); + EXPECT_TRUE(loaded->has_aux_basis()); + EXPECT_EQ("my-aux", loaded->get_aux_name()); + EXPECT_EQ(4u, loaded->get_num_aux_shells()); + + // File-based JSON round-trip + std::string filename = "test_aux.basis_set.json"; + basis.to_json_file(filename); + auto loaded_file = BasisSet::from_json_file(filename); + EXPECT_TRUE(loaded_file->has_aux_basis()); + EXPECT_EQ("my-aux", loaded_file->get_aux_name()); + EXPECT_EQ(4u, loaded_file->get_num_aux_shells()); + std::filesystem::remove(filename); + + // Test without auxiliary (ensure no crash) + BasisSet no_aux("no-aux", shells, *structure); + auto json_no_aux = no_aux.to_json(); + auto loaded_no_aux = BasisSet::from_json(json_no_aux); + EXPECT_FALSE(loaded_no_aux->has_aux_basis()); +} + +TEST_F(BasisSetTest, AuxiliaryBasisHDF5Serialization) { + auto structure = testing::create_water_structure(); + + std::vector shells; + shells.emplace_back( + Shell(0, OrbitalType::S, std::vector{1.0}, std::vector{1.0})); + shells.emplace_back( + Shell(1, OrbitalType::S, std::vector{1.0}, std::vector{1.0})); + shells.emplace_back( + Shell(2, OrbitalType::S, std::vector{1.0}, std::vector{1.0})); + + std::vector aux_shells; + aux_shells.emplace_back( + Shell(0, OrbitalType::S, std::vector{2.0}, std::vector{1.0})); + aux_shells.emplace_back( + Shell(1, OrbitalType::S, std::vector{2.5}, std::vector{1.2})); + aux_shells.emplace_back( + Shell(2, OrbitalType::S, std::vector{2.0}, std::vector{1.0})); + + std::string aux_name = "hdf5-aux"; + std::vector ecp_shells; // empty + + BasisSet basis("hdf5-primary", shells, "", ecp_shells, {}, + aux_name, aux_shells, *structure); + + std::string filename = "test_aux.basis_set.h5"; + basis.to_hdf5_file(filename); + + auto loaded = BasisSet::from_hdf5_file(filename); + EXPECT_TRUE(loaded->has_aux_basis()); + EXPECT_EQ("hdf5-aux", loaded->get_aux_name()); + EXPECT_EQ(3u, loaded->get_num_aux_shells()); + + std::filesystem::remove(filename); +} + +TEST_F(BasisSetTest, AuxiliaryBasisSummary) { + std::vector coords = {{0.0, 0.0, 0.0}}; + std::vector symbols = {"H"}; + Structure structure(coords, symbols); + + std::vector shells; + shells.emplace_back( + Shell(0, OrbitalType::S, std::vector{1.0}, std::vector{2.0})); + + std::vector aux_shells; + aux_shells.emplace_back( + Shell(0, OrbitalType::S, std::vector{2.0}, std::vector{1.0})); + + std::vector ecp_shells; // empty + + BasisSet basis("primary", shells, ecp_shells, aux_shells, structure); + + std::string summary = basis.get_summary(); + EXPECT_FALSE(summary.empty()); + // Summary should mention the auxiliary basis set + EXPECT_NE(std::string::npos, summary.find("Auxiliary")); +} + +TEST_F(BasisSetTest, FromBasisNameWithAuxSCFComparison) { + // Verify that from_basis_name with aux produces a valid basis + // by comparing it against from_basis_name without aux + auto structure = testing::create_water_structure(); + + auto basis_no_aux = BasisSet::from_basis_name("def2-svp", *structure); + auto basis_with_aux = + BasisSet::from_basis_name("def2-svp", "def2-universal-jfit", *structure); + + // Primary basis should match + EXPECT_EQ(basis_no_aux->get_name(), basis_with_aux->get_name()); + EXPECT_EQ(basis_no_aux->get_num_shells(), basis_with_aux->get_num_shells()); + EXPECT_EQ(basis_no_aux->get_num_atomic_orbitals(), + basis_with_aux->get_num_atomic_orbitals()); + + // Only the version with aux should have auxiliary + EXPECT_FALSE(basis_no_aux->has_aux_basis()); + EXPECT_TRUE(basis_with_aux->has_aux_basis()); +} diff --git a/python/src/pybind11/data/basis_set.cpp b/python/src/pybind11/data/basis_set.cpp index cc68f0427..f82b8d3aa 100644 --- a/python/src/pybind11/data/basis_set.cpp +++ b/python/src/pybind11/data/basis_set.cpp @@ -57,6 +57,16 @@ std::shared_ptr basis_set_from_json_file_wrapper( } // namespace +// Helper: convert a Python list of Shell objects to std::vector. +// This bypasses pybind11's list_caster which crashes under py::smart_holder +// when it tries to weakref a list_iterator during overload probing. +static std::vector to_shell_vec(const py::list& lst) { + std::vector result; + result.reserve(lst.size()); + for (auto& item : lst) result.push_back(item.cast()); + return result; +} + void bind_basis_set(py::module& m) { using namespace qdk::chemistry::data; using qdk::chemistry::python::utils::bind_getter_as_property; @@ -208,139 +218,124 @@ A shell represents a group of atomic orbitals with the same atom, angular moment >>> print(f"Number of atomic orbitals: {basis.get_num_atomic_orbitals()}") )"); - basis_set.def(py::init(), - R"( -Constructor with basis set name, structure, and basis type. - -Creates a basis set associated with a molecular structure. - -Args: - name (str): Name of the basis set - structure (Structure): Molecular structure to associate with this basis set - atomic_orbital_type (AOType | None): Whether to use spherical or Cartesian atomic orbitals. Default is Spherical - -Examples: - >>> from qdk_chemistry.data import Structure - >>> structure = Structure.from_xyz_file("water.xyz") - >>> basis = BasisSet("cc-pVDZ", structure, AOType.Spherical) - >>> print(f"Basis set for {structure.get_num_atoms()} atoms") -)", - py::arg("name"), py::arg("structure"), - py::arg("atomic_orbital_type") = AOType::Spherical); - - basis_set.def( - py::init&, AOType>(), - R"( -Constructor with basis set name, shells, and basis type. - -Creates a basis set with predefined shells. - -Args: - name (str): Name of the basis set - shells (list[Shell]): Vector of shell objects defining the atomic orbitals - atomic_orbital_type (AOType | None): Whether to use spherical or Cartesian atomic orbitals. - Default is Spherical. - -Examples: - >>> shells = [Shell(0, OrbitalType.S), Shell(0, OrbitalType.P)] - >>> basis = BasisSet("custom", shells) - >>> print(f"Created basis with {len(shells)} shells") -)", - py::arg("name"), py::arg("shells"), - py::arg("atomic_orbital_type") = AOType::Spherical); - basis_set.def(py::init&, - const Structure&, AOType>(), - R"( -Constructor with basis set name, shells, structure, and basis type. - -Creates a complete basis set with shells and molecular structure. - -Args: - name (str): Name of the basis set - shells (list[Shell]): Vector of shell objects defining the atomic orbitals - structure (Structure): Molecular structure to associate with this basis set - atomic_orbital_type (AOType | None): Whether to use spherical or Cartesian atomic orbitals. - Default is Spherical - -Examples: - >>> from qdk_chemistry.data import Structure - >>> structure = Structure.from_xyz_file("water.xyz") - >>> shells = [Shell(0, OrbitalType.S), Shell(1, OrbitalType.S)] - >>> basis = BasisSet("custom", shells, structure) - >>> print(f"Complete basis set with {len(shells)} shells") -)", - py::arg("name"), py::arg("shells"), py::arg("structure"), - py::arg("atomic_orbital_type") = AOType::Spherical); - basis_set.def(py::init&, - const std::vector&, const Structure&, AOType>(), - R"( -Constructor with basis set name, shells, ECP shells, structure, and basis type. - -Creates a complete basis set with regular shells, ECP shells, and molecular structure. - -Args: - name (str): Name of the basis set - shells (list[Shell]): Vector of shell objects defining the atomic orbitals - ecp_shells (list[Shell]): Vector of ECP shell objects - structure (Structure): Molecular structure to associate with this basis set - atomic_orbital_type (AOType | None): Whether to use spherical or Cartesian atomic orbitals. - Default is Spherical - -Examples: - >>> from qdk_chemistry.data import Structure - >>> structure = Structure.from_xyz_file("water.xyz") - >>> shells = [Shell(0, OrbitalType.S), Shell(1, OrbitalType.S)] - >>> ecp_shells = [Shell(0, OrbitalType.S, exp, coeff, rpow)] - >>> basis = BasisSet("custom-ecp", shells, ecp_shells, structure) - >>> print(f"Basis with {len(shells)} shells and {len(ecp_shells)} ECP shells") -)", - py::arg("name"), py::arg("shells"), py::arg("ecp_shells"), - py::arg("structure"), - py::arg("atomic_orbital_type") = AOType::Spherical); + // ----------------------------------------------------------------------- + // Single __init__ dispatcher. Multiple py::init overloads crash under + // py::smart_holder because pybind11 probes each overload and the + // list_caster> internally creates a list_iterator that + // smart_holder cannot weak-reference. A single py::init with runtime + // type checks avoids overload probing entirely. + // + // Helper: if the last positional arg is an AOType, pop it off and use it; + // otherwise look in kwargs; otherwise default to Spherical. + // Also support fully-keyword calls: BasisSet(name=..., shells=..., ...). + // ----------------------------------------------------------------------- basis_set.def( - py::init&, - const std::string&, const std::vector&, - const std::vector&, const Structure&, AOType>(), + py::init([](py::args args, py::kwargs kwargs) -> BasisSet { + // --- Collect positional args into a mutable vector ---------------- + std::vector a; + a.reserve(args.size()); + for (size_t i = 0; i < args.size(); ++i) + a.push_back(args[i].cast()); + + // If last positional arg is AOType, pop it. + // We check if the Python type name matches "AOType" to avoid + // accidentally casting e.g. an int or Structure. + AOType ao = AOType::Spherical; + if (!a.empty()) { + std::string tname = + py::str(a.back().get_type().attr("__name__")).cast(); + if (tname == "AOType") { + ao = a.back().cast(); + a.pop_back(); + } + } + // Keyword override + if (kwargs.contains("atomic_orbital_type")) + ao = kwargs["atomic_orbital_type"].cast(); + + // --- Support fully-keyword calls ---------------------------------- + // BasisSet(name=..., shells=..., atomic_orbital_type=...) + if (a.empty() && kwargs.contains("name") && kwargs.contains("shells")) { + auto name = kwargs["name"].cast(); + auto shells = to_shell_vec(kwargs["shells"].cast()); + return BasisSet(name, shells, ao); + } + + const size_t n = a.size(); + + // Copy constructor: BasisSet(other) + if (n == 1 && py::isinstance(a[0])) + return BasisSet(a[0].cast()); + + if (n < 2 || !py::isinstance(a[0]) || + !py::isinstance(a[1])) + throw py::type_error( + "BasisSet() expects (name: str, shells: list[Shell], ...)"); + + auto name = a[0].cast(); + auto shells = to_shell_vec(a[1].cast()); + + // (name, shells) + if (n == 2) + return BasisSet(name, shells, ao); + + // (name, shells, structure) + if (n == 3 && py::isinstance(a[2])) + return BasisSet(name, shells, a[2].cast(), ao); + + // (name, shells, aux_shells, structure) + if (n == 4 && py::isinstance(a[2]) && + py::isinstance(a[3])) + return BasisSet(name, shells, to_shell_vec(a[2].cast()), + a[3].cast(), ao); + + if (n == 5) { + if (py::isinstance(a[2])) { + // (name, shells, aux_name, aux_shells, structure) + return BasisSet(name, shells, a[2].cast(), + to_shell_vec(a[3].cast()), + a[4].cast(), ao); + } + // (name, shells, ecp_shells, ecp_electrons, structure) + return BasisSet(name, shells, + to_shell_vec(a[2].cast()), + a[3].cast>(), + a[4].cast(), ao); + } + + // (name, shells, ecp_name, ecp_shells, ecp_electrons, structure) + if (n == 6 && py::isinstance(a[2])) + return BasisSet(name, shells, a[2].cast(), + to_shell_vec(a[3].cast()), + a[4].cast>(), + a[5].cast(), ao); + + // (name, shells, ecp_name, ecp_shells, ecp_electrons, + // aux_name, aux_shells, structure) + if (n == 8 && py::isinstance(a[2])) + return BasisSet(name, shells, a[2].cast(), + to_shell_vec(a[3].cast()), + a[4].cast>(), + a[5].cast(), + to_shell_vec(a[6].cast()), + a[7].cast(), ao); + + throw py::type_error( + "No matching BasisSet constructor for the given arguments"); + }), R"( -Constructor with basis set name, shells, ECP name, ECP shells, ECP electrons, structure, and basis type. - -Creates a complete basis set with regular shells, ECP shells, ECP metadata, and molecular structure. - -Args: - name (str): Name of the basis set - shells (list[Shell]): Vector of shell objects defining the atomic orbitals - ecp_name (str): Name of the ECP (basis set) - ecp_shells (list[Shell]): Vector of ECP shell objects - ecp_electrons (list[int]): Number of ECP electrons for each atom - structure (Structure): Molecular structure to associate with this basis set - atomic_orbital_type (AOType | None): Whether to use spherical or Cartesian atomic orbitals. - Default is Spherical - -Examples: - >>> from qdk_chemistry.data import Structure - >>> structure = Structure.from_xyz_file("water.xyz") - >>> shells = [Shell(0, OrbitalType.S), Shell(1, OrbitalType.S)] - >>> ecp_shells = [Shell(0, OrbitalType.S, exp, coeff, rpow)] - >>> ecp_electrons = [10, 10, 0] - >>> basis = BasisSet("custom-ecp", shells, "custom-ecp", ecp_shells, ecp_electrons, structure) - >>> print(f"Basis with {len(shells)} shells, {len(ecp_shells)} ECP shells, ECP: {basis.get_ecp_name()}") -)", - py::arg("name"), py::arg("shells"), py::arg("ecp_name"), - py::arg("ecp_shells"), py::arg("ecp_electrons"), py::arg("structure"), - py::arg("atomic_orbital_type") = AOType::Spherical); - basis_set.def(py::init(), - R"( -Copy constructor. - -Creates a deep copy of another basis set. - -Args: - other (BasisSet): Basis set to copy - -Examples: - >>> original = BasisSet("cc-pVDZ") - >>> copy = BasisSet(original) - >>> print(f"Copied basis set: {copy.get_name()}") +BasisSet constructor. + +Supported signatures (atomic_orbital_type is always optional, default Spherical): + BasisSet(other: BasisSet) + BasisSet(name, shells) + BasisSet(name, shells, structure) + BasisSet(name, shells, aux_shells, structure) + BasisSet(name, shells, aux_name, aux_shells, structure) + BasisSet(name, shells, ecp_shells, ecp_electrons, structure) + BasisSet(name, shells, ecp_name, ecp_shells, ecp_electrons, structure) + BasisSet(name, shells, ecp_name, ecp_shells, ecp_electrons, + aux_name, aux_shells, structure) )"); // Basis type management @@ -504,6 +499,92 @@ Check if this basis set has ECP shells. ... print("This basis set includes ECP shells") )"); + // Auxiliary shell access + basis_set.def("get_aux_shells", &BasisSet::get_aux_shells, + R"( +Get all auxiliary shells (flattened from per-atom storage). + +Returns: + list[Shell]: Vector of all auxiliary shells in the basis set + +Examples: + >>> aux_shells = basis_set.get_aux_shells() + >>> print(f"Total auxiliary shells: {len(aux_shells)}") +)"); + + basis_set.def("get_aux_shells_for_atom", &BasisSet::get_aux_shells_for_atom, + R"( +Get auxiliary shells for a specific atom. + +Args: + atom_index (int): Index of the atom + +Returns: + list[Shell]: Vector of auxiliary shells for the specified atom + +Examples: + >>> aux_atom_shells = basis_set.get_aux_shells_for_atom(0) + >>> print(f"Atom 0 has {len(aux_atom_shells)} auxiliary shells") +)", + py::arg("atom_index"), + py::return_value_policy::reference_internal); + + basis_set.def("get_aux_shell", &BasisSet::get_aux_shell, + R"( +Get a specific auxiliary shell by global index. + +Args: + shell_index (int): Global index of the auxiliary shell + +Returns: + Shell: Reference to the specified auxiliary shell + +Raises: + IndexError: If auxiliary shell index is out of range + +Examples: + >>> aux_shell = basis_set.get_aux_shell(0) + >>> print(f"First aux shell type: {aux_shell.orbital_type}") +)", + py::arg("shell_index"), + py::return_value_policy::reference_internal); + + basis_set.def("get_num_aux_shells", &BasisSet::get_num_aux_shells, + R"( +Get total number of auxiliary shells across all atoms. + +Returns: + int: Total number of auxiliary shells + +Examples: + >>> n_aux_shells = basis_set.get_num_aux_shells() + >>> print(f"Total auxiliary shells: {n_aux_shells}") +)"); + + basis_set.def("has_aux_basis", &BasisSet::has_aux_basis, + R"( +Check if this basis set has an auxiliary basis. + +Returns: + bool: True if there are any auxiliary shells + +Examples: + >>> if basis_set.has_aux_basis(): + ... print("This basis set includes an auxiliary basis") +)"); + + basis_set.def("get_aux_name", &BasisSet::get_aux_name, + R"( +Get the auxiliary basis set name. + +Returns: + str: Name of the auxiliary basis set + +Examples: + >>> aux_name = basis_set.get_aux_name() + >>> print(f"Auxiliary basis: {aux_name}") +)"); + // atomic orbital management basis_set.def("get_atomic_orbital_info", &BasisSet::get_atomic_orbital_info, R"( @@ -1036,6 +1117,33 @@ Loads a standard basis set (e.g., "sto-3g", "cc-pvdz") for all atoms in the stru )", py::arg("basis_name"), py::arg("structure"), py::arg("atomic_orbital_type") = AOType::Spherical); + basis_set.def_static( + "from_basis_name", + py::overload_cast( + &BasisSet::from_basis_name), + R"( +Create a basis set by name with an auxiliary basis for a molecular structure. + +Loads a standard basis set and auxiliary basis set for all atoms in the structure. + +Args: + basis_name (str): Name of the basis set (e.g., "def2-svp") + aux_basis_name (str): Name of the auxiliary basis set (e.g., "def2-universal-jfit") + structure (Structure): Molecular structure + atomic_orbital_type (AOType, optional): Whether to use spherical or Cartesian atomic orbitals. + Default is Spherical + +Returns: + BasisSet: New basis set instance with auxiliary basis + +Examples: + >>> basis = BasisSet.from_basis_name("def2-svp", "def2-universal-jfit", structure) + >>> print(f"Aux shells: {basis.get_num_aux_shells()}") +)", + py::arg("basis_name"), py::arg("aux_basis_name"), + py::arg("structure"), + py::arg("atomic_orbital_type") = AOType::Spherical); basis_set.def_static( "from_element_map", py::overload_cast&, @@ -1067,6 +1175,32 @@ Allows specifying different basis sets for different elements in the structure. )", py::arg("element_to_basis_map"), py::arg("structure"), py::arg("atomic_orbital_type") = AOType::Spherical); + basis_set.def_static( + "from_element_map", + py::overload_cast&, + const std::map&, + const Structure&, AOType>(&BasisSet::from_element_map), + R"( +Create a basis set with different basis sets and auxiliary basis sets per element. + +Args: + element_to_basis_map (dict[str, str]): Dictionary mapping element symbols to basis set names + element_to_aux_basis_map (dict[str, str]): Dictionary mapping element symbols to auxiliary basis set names + structure (Structure): Molecular structure + atomic_orbital_type (AOType, optional): Whether to use spherical or Cartesian atomic orbitals. + Default is Spherical + +Returns: + BasisSet: New basis set instance with auxiliary basis + +Examples: + >>> basis_map = {"H": "def2-svp", "O": "def2-svp"} + >>> aux_map = {"H": "def2-universal-jfit", "O": "def2-universal-jfit"} + >>> basis = BasisSet.from_element_map(basis_map, aux_map, structure) +)", + py::arg("element_to_basis_map"), py::arg("element_to_aux_basis_map"), + py::arg("structure"), + py::arg("atomic_orbital_type") = AOType::Spherical); basis_set.def_static( "from_index_map", py::overload_cast&, const Structure&, @@ -1098,6 +1232,32 @@ Allows specifying different basis sets for individual atoms by their index. )", py::arg("index_to_basis_map"), py::arg("structure"), py::arg("atomic_orbital_type") = AOType::Spherical); + basis_set.def_static( + "from_index_map", + py::overload_cast&, + const std::map&, + const Structure&, AOType>(&BasisSet::from_index_map), + R"( +Create a basis set with different basis sets and auxiliary basis sets per atom index. + +Args: + index_to_basis_map (dict[int, str]): Dictionary mapping atom indices to basis set names + index_to_aux_basis_map (dict[int, str]): Dictionary mapping atom indices to auxiliary basis set names + structure (Structure): Molecular structure + atomic_orbital_type (AOType, optional): Whether to use spherical or Cartesian atomic orbitals. + Default is Spherical + +Returns: + BasisSet: New basis set instance with auxiliary basis + +Examples: + >>> basis_map = {0: "def2-svp", 1: "def2-svp", 2: "def2-svp"} + >>> aux_map = {0: "def2-universal-jfit", 1: "def2-universal-jfit", 2: "def2-universal-jfit"} + >>> basis = BasisSet.from_index_map(basis_map, aux_map, structure) +)", + py::arg("index_to_basis_map"), py::arg("index_to_aux_basis_map"), + py::arg("structure"), + py::arg("atomic_orbital_type") = AOType::Spherical); // Utility functions (static methods); basis_set.def_static("orbital_type_to_string", diff --git a/python/tests/test_basis_set.py b/python/tests/test_basis_set.py index d5111a19a..58625f7cf 100644 --- a/python/tests/test_basis_set.py +++ b/python/tests/test_basis_set.py @@ -321,15 +321,33 @@ def test_summary(): assert "shells: 2" in summary assert "atomic orbitals: 4" in summary + # Test summary with auxiliary basis set + positions = np.array([[0.0, 0.0, 0.0]]) + elements = ["H"] + structure = Structure(elements, positions) + aux_shells = [Shell(0, OrbitalType.S, [2.0], [1.0])] + basis_with_aux = BasisSet("6-31G", shells, "aux-basis", aux_shells, structure) + summary_with_aux = basis_with_aux.get_summary() + assert "Auxiliary" in summary_with_aux + + def test_json_serialization(): """Test JSON serialization and deserialization.""" # Create a basis set with data + positions = np.array([[0.0, 0.0, 0.0]]) + elements = ["H"] + structure = Structure(elements, positions) + shells = [ Shell(0, OrbitalType.S, [1.0], [1.0]), Shell(0, OrbitalType.P, [0.5], [1.0]), ] - basis_out = BasisSet("STO-3G", shells) + aux_shells = [ + Shell(0, OrbitalType.S, [2.0], [1.0]), + Shell(0, OrbitalType.D, [0.3], [0.5]) + ] + basis_out = BasisSet("STO-3G", shells, "aux-fit", aux_shells, structure) # Test direct JSON conversion json_data = basis_out.to_json() @@ -342,6 +360,9 @@ def test_json_serialization(): assert basis_in.get_name() == "STO-3G" assert basis_in.get_num_shells() == 2 assert basis_in.get_num_atomic_orbitals() == 4 + assert basis_in.has_aux_basis() + assert basis_in.get_aux_name() == "aux-fit" + assert basis_in.get_num_aux_shells() == 2 # Test file-based serialization with tempfile.NamedTemporaryFile(suffix=".basis_set.json", mode="w", delete=False) as tmp: @@ -356,18 +377,28 @@ def test_json_serialization(): assert basis_file.get_name() == "STO-3G" assert basis_file.get_num_shells() == 2 assert basis_file.get_num_atomic_orbitals() == 4 + assert basis_file.get_aux_name() == "aux-fit" + assert basis_file.get_num_aux_shells() == 2 finally: Path(filename).unlink() + def test_hdf5_serialization(): """Test HDF5 serialization and deserialization.""" # Create a basis set with data + positions = np.array([[0.0, 0.0, 0.0]]) + elements = ["H"] + structure = Structure(elements, positions) + shells = [ Shell(0, OrbitalType.S, [1.0], [1.0]), Shell(0, OrbitalType.P, [0.5], [1.0]), ] - basis_out = BasisSet("cc-pVDZ", shells, AOType.Spherical) + aux_shells = [ + Shell(0, OrbitalType.S, [2.0], [1.0]), + Shell(0, OrbitalType.P, [0.8], [0.7])] + basis_out = BasisSet("cc-pVDZ", shells, "aux-fit", aux_shells, structure, AOType.Spherical) try: with tempfile.NamedTemporaryFile(suffix=".basis_set.h5", delete=False) as tmp: @@ -381,6 +412,9 @@ def test_hdf5_serialization(): assert basis_in.get_name() == "cc-pVDZ" assert basis_in.get_num_shells() == 2 assert basis_in.get_num_atomic_orbitals() == 4 + assert basis_in.get_aux_name() == "aux-fit" + assert basis_in.get_num_aux_shells() == 2 + except RuntimeError as e: pytest.skip(f"HDF5 test skipped - {e!s}") @@ -390,6 +424,12 @@ def test_hdf5_serialization(): Path(filename).unlink() + +def test_error_handling(): + """Test error handling for invalid operations.""" + # Create a minimal shell for testing empty functions + shell = Shell(0, OrbitalType.S, [1.0], [1.0]) + def test_error_handling(): """Test error handling for invalid operations.""" # Create a minimal shell for testing empty functions @@ -1021,6 +1061,13 @@ def test_basis_set_pickling_and_repr(): ] original = BasisSet("STO-3G", shells) + # Create a basis set with auxiliary shells + positions = np.array([[0.0, 0.0, 0.0]]) + elements = ["H"] + structure = Structure(elements, positions) + aux_shells = [Shell(0, OrbitalType.S, [2.0, 0.5], [0.6, 0.4])] + original = BasisSet("STO-3G", shells, "aux-jfit", aux_shells, structure) + # Test pickling and unpickling pickled_data = pickle.dumps(original) assert isinstance(pickled_data, bytes) @@ -1034,6 +1081,11 @@ def test_basis_set_pickling_and_repr(): assert unpickled.get_num_atomic_orbitals() == original.get_num_atomic_orbitals() assert unpickled.get_atomic_orbital_type() == original.get_atomic_orbital_type() + # Verify auxiliary basis set is preserved through pickling + assert unpickled.has_aux_basis() + assert unpickled.get_aux_name() == "aux-jfit" + assert unpickled.get_num_aux_shells() == 1 + # Verify shells are preserved original_shells = original.get_shells() unpickled_shells = unpickled.get_shells() @@ -1185,7 +1237,7 @@ def test_basis_set_ecp_shells(): ecp_shells = [ecp_shell_s, ecp_shell_p] # Create basis set with ECP shells - basis = BasisSet("test-basis", shells, ecp_shells, structure) + basis = BasisSet("test-basis", shells, ecp_shells, [2], structure) # Test ECP shell queries assert basis.has_ecp_shells() @@ -1261,6 +1313,36 @@ def test_basis_set_ecp_shells_serialization(): assert np.array_equal(loaded_shell.rpowers, [0, 2]) +def test_auxiliary_basis_set_serialization(): + """Test auxiliary basis set serialization and deserialization.""" + # Create a basis set with auxiliary basis + positions = np.array([[0.0, 0.0, 0.0]]) + elements = ["H"] + structure = Structure(elements, positions) + shells = [Shell(0, OrbitalType.S, [1.0], [1.0])] + aux_shells = [Shell(0, OrbitalType.S, [4.0, 1.0], [0.7, 0.3])] + + basis = BasisSet("test-basis", shells, "aux_basis", aux_shells, structure) + + # Test JSON serialization + with tempfile.TemporaryDirectory() as tmpdir: + json_file = str(Path(tmpdir) / "test_aux.basis_set.json") + basis.to_json_file(json_file) + + loaded_basis = BasisSet.from_json_file(json_file) + assert loaded_basis.has_aux_basis() + assert loaded_basis.get_aux_name() == "aux_basis" + assert loaded_basis.get_num_aux_shells() == 1 + + # Test HDF5 serialization + with tempfile.TemporaryDirectory() as tmpdir: + hdf5_file = str(Path(tmpdir) / "test_aux.basis_set.h5") + basis.to_hdf5_file(hdf5_file) + + loaded_basis = BasisSet.from_hdf5_file(hdf5_file) + assert loaded_basis.has_aux_basis() + assert loaded_basis.get_aux_name() == "aux_basis" + def test_basis_set_ecp_shells_copy(): """Test that ECP shells are properly copied.""" # Create structure and shells @@ -1287,6 +1369,23 @@ def test_basis_set_ecp_shells_copy(): assert np.array_equal(copy_shell.coefficients, orig_shell.coefficients) +def test_auxiliary_basis_set_copy(): + """Test that auxiliary basis set is properly copied.""" + positions = np.array([[0.0, 0.0, 0.0]]) + elements = ["H"] + structure = Structure(elements, positions) + shells = [Shell(0, OrbitalType.S, [1.0], [1.0])] + aux_shells = [Shell(0, OrbitalType.S, [3.0], [1.0]), Shell(0, OrbitalType.P, [1.5], [0.8])] + + basis = BasisSet("test-basis", shells, "aux-copy-test", aux_shells, structure) + + # Test copy constructor + basis_copy = BasisSet(basis) + assert basis_copy.has_aux_basis() + assert basis_copy.get_aux_name() == "aux-copy-test" + assert basis_copy.get_num_aux_shells() == 2 + + def test_basis_set_ecp_shells_multi_atom(): """Test ECP shells in multi-atom systems.""" # Create structure with multiple atoms @@ -1308,7 +1407,7 @@ def test_basis_set_ecp_shells_multi_atom(): Shell(1, OrbitalType.D, [12.0], [40.0], [2]), ] - basis = BasisSet("test-basis", shells, ecp_shells, structure) + basis = BasisSet("test-basis", shells, ecp_shells, [0, 2, 0], structure) # Test total ECP shells assert basis.get_num_ecp_shells() == 3 @@ -1350,6 +1449,13 @@ def test_basis_set_from_basis_name(): num_orbitals = determinant.get_orbitals().get_num_molecular_orbitals() assert num_orbitals == 7 + # Test from_basis_name with auxiliary basis set + basis_with_aux = BasisSet.from_basis_name("sto-3g", "def2-universal-jfit", structure) + assert basis_with_aux.get_name() == "sto-3g" + assert basis_with_aux.has_aux_basis() + assert basis_with_aux.get_aux_name() == "def2-universal-jfit" + assert basis_with_aux.get_num_aux_shells() > 0 + def test_basis_set_from_element_map(): """Test creating basis set using from_element_map static method.""" @@ -1448,3 +1554,191 @@ def test_basis_set_data_type_name(): """Test that BasisSet has the correct _data_type_name class attribute.""" assert hasattr(BasisSet, "_data_type_name") assert BasisSet._data_type_name == "basis_set" + + + +def test_auxiliary_basis_set_accessors(): + """Test has/get auxiliary basis set methods.""" + positions = np.array([[0.0, 0.0, 0.0]]) + elements = ["H"] + structure = Structure(elements, positions) + + shells = [ + Shell(0, OrbitalType.S, [1.0], [1.0]), + Shell(0, OrbitalType.P, [0.5], [1.0]), + ] + + # Basis without auxiliary + basis_no_aux = BasisSet("test-basis", shells, structure) + assert not basis_no_aux.has_aux_basis() + assert basis_no_aux.get_num_aux_shells() == 0 + + # Basis with auxiliary shells (unnamed) + aux_shells = [Shell(0, OrbitalType.S, [2.0], [1.0])] + basis_with_aux = BasisSet("test-basis", shells, aux_shells, structure) + assert basis_with_aux.has_aux_basis() + assert basis_with_aux.get_num_aux_shells() == 1 + retrieved_aux = basis_with_aux.get_aux_shells() + assert len(retrieved_aux) == 1 + assert retrieved_aux[0].orbital_type == OrbitalType.S + + # Basis with named auxiliary shells + basis_named_aux = BasisSet("test-basis", shells, "aux-test", aux_shells, structure) + assert basis_named_aux.has_aux_basis() + assert basis_named_aux.get_aux_name() == "aux-test" + assert basis_named_aux.get_num_aux_shells() == 1 + + # Test per-atom auxiliary shell access + aux_for_atom0 = basis_with_aux.get_aux_shells_for_atom(0) + assert len(aux_for_atom0) == 1 + assert aux_for_atom0[0].orbital_type == OrbitalType.S + + +def test_auxiliary_basis_set_constructors(): + """Test constructors that include auxiliary basis set parameters.""" + # Set up structure + positions = np.array([[0.0, 0.0, 0.0], [1.4, 0.0, 0.0]]) + elements = ["H", "H"] + structure = Structure(elements, positions) + + # --- Constructor: BasisSet(name, shells, aux_shells, structure) --- + shells = [ + Shell(0, OrbitalType.S, [1.0], [1.0]), + Shell(1, OrbitalType.S, [1.0], [1.0]), + ] + aux_shells = [ + Shell(0, OrbitalType.S, [3.0], [1.0]), + Shell(0, OrbitalType.P, [1.5], [0.8]), + Shell(1, OrbitalType.S, [3.0], [1.0]), + Shell(1, OrbitalType.P, [1.5], [0.8]), + ] + + basis_with_aux = BasisSet("test-basis", shells, aux_shells, structure) + assert basis_with_aux.get_name() == "test-basis" + assert basis_with_aux.get_num_shells() == 2 + assert basis_with_aux.has_aux_basis() + assert basis_with_aux.get_num_aux_shells() == 4 + + # --- Constructor: BasisSet(name, shells, aux_name, aux_shells, structure) --- + basis_named = BasisSet("test-basis", shells, "aux-named", aux_shells, structure) + assert basis_named.has_aux_basis() + assert basis_named.get_aux_name() == "aux-named" + assert basis_named.get_num_aux_shells() == 4 + + # Test with AOType.Cartesian + basis_cart = BasisSet("test-basis", shells, aux_shells, structure, AOType.Cartesian) + assert basis_cart.get_atomic_orbital_type() == AOType.Cartesian + assert basis_cart.has_aux_basis() + + +def test_ecp_with_combined_constructors(): + """Test ECP functionality of constructors that accept both ECP and auxiliary parameters.""" + # Set up structure + positions = np.array([[0.0, 0.0, 0.0], [1.4, 0.0, 0.0]]) + elements = ["H", "H"] + structure = Structure(elements, positions) + + shells = [ + Shell(0, OrbitalType.S, [1.0], [1.0]), + Shell(1, OrbitalType.S, [1.0], [1.0]), + ] + ecp_shells = [Shell(0, OrbitalType.S, [5.0], [10.0], [0])] + + # --- Constructor: BasisSet(name, shells, ecp_shells, ecp_electrons, structure) --- + basis = BasisSet("test-ecp", shells, ecp_shells, [2, 0], structure) + assert basis.get_name() == "test-ecp" + assert basis.get_num_shells() == 2 + assert basis.has_ecp_shells() + assert basis.get_num_ecp_shells() == 1 + + # --- Constructor: BasisSet(name, shells, ecp_name, ecp_shells, ecp_electrons, aux_name, aux_shells, structure) --- + basis_full = BasisSet( + "full-ecp", + shells, + "my-ecp", + ecp_shells, + [2, 0], + structure, + ) + assert basis_full.get_name() == "full-ecp" + assert basis_full.has_ecp_shells() + assert basis_full.get_ecp_name() == "my-ecp" + assert list(basis_full.get_ecp_electrons()) == [2, 0] + assert basis_full.get_num_ecp_shells() == 1 + + +def test_auxiliary_with_combined_constructors(): + """Test auxiliary basis functionality using the aux-only constructor.""" + # Set up structure + positions = np.array([[0.0, 0.0, 0.0], [1.4, 0.0, 0.0]]) + elements = ["H", "H"] + structure = Structure(elements, positions) + + shells = [ + Shell(0, OrbitalType.S, [1.0], [1.0]), + Shell(1, OrbitalType.S, [1.0], [1.0]), + ] + aux_shells = [ + Shell(0, OrbitalType.S, [2.0], [1.0]), + Shell(1, OrbitalType.S, [2.0], [1.0]), + ] + + # --- Constructor: BasisSet(name, shells, aux_name, aux_shells, structure) --- + basis = BasisSet("test-aux", shells, "my-aux", aux_shells, structure) + assert basis.has_aux_basis() + assert not basis.has_ecp_shells() + assert basis.get_aux_name() == "my-aux" + assert basis.get_num_aux_shells() == 2 + + # Test with Cartesian + basis_cart = BasisSet("test-aux-cart", shells, aux_shells, structure, AOType.Cartesian) + assert basis_cart.get_atomic_orbital_type() == AOType.Cartesian + assert basis_cart.has_aux_basis() + assert not basis_cart.has_ecp_shells() + + # Test 8-arg constructor with empty ECP to create aux-only basis + basis_full = BasisSet( + "aux-only", + shells, + "none", + [], + [0, 0], + "my-aux", + aux_shells, + structure, + ) + assert basis_full.has_aux_basis() + assert not basis_full.has_ecp_shells() + assert basis_full.get_aux_name() == "my-aux" + assert basis_full.get_num_aux_shells() == 2 + + +def test_auxiliary_basis_set_from_basis_name_database(): + """Test from_basis_name with auxiliary using actual basis set database.""" + positions = np.array([[0.0, 0.0, 0.0], [1.4, 0.0, 0.0]]) + elements = ["H", "H"] + structure = Structure(elements, positions) + + # Test with def2-universal-jfit auxiliary + basis = BasisSet.from_basis_name("cc-pvdz", "def2-universal-jfit", structure) + assert basis.get_name() == "cc-pvdz" + assert basis.has_aux_basis() + assert basis.get_aux_name() == "def2-universal-jfit" + assert basis.get_num_aux_shells() > 0 + assert basis.has_structure() + + # Verify primary basis is still correct + assert basis.get_num_shells() > 0 + assert basis.has_structure() + + # Verify JSON round-trip preserves both primary and aux from database + with tempfile.TemporaryDirectory() as tmpdir: + json_file = str(Path(tmpdir) / "db_aux.basis_set.json") + basis.to_json_file(json_file) + + loaded = BasisSet.from_json_file(json_file) + assert loaded.get_name() == "cc-pvdz" + assert loaded.has_aux_basis() + assert loaded.get_aux_name() == "def2-universal-jfit" + assert loaded.get_num_shells() == basis.get_num_shells() + assert loaded.get_num_aux_shells() == basis.get_num_aux_shells() From c0a566ef287ba0bdb57e9a6027b44394f77d876b Mon Sep 17 00:00:00 2001 From: rainli323 Date: Wed, 15 Apr 2026 01:00:49 +0000 Subject: [PATCH 02/33] forgot to commit some --- cpp/include/qdk/chemistry/data/basis_set.hpp | 51 ++- cpp/src/qdk/chemistry/data/basis_set.cpp | 89 +++- cpp/tests/test_basis_set.cpp | 435 +++++++++++++++++-- 3 files changed, 501 insertions(+), 74 deletions(-) diff --git a/cpp/include/qdk/chemistry/data/basis_set.hpp b/cpp/include/qdk/chemistry/data/basis_set.hpp index 9809f6ee0..cb00d40b7 100644 --- a/cpp/include/qdk/chemistry/data/basis_set.hpp +++ b/cpp/include/qdk/chemistry/data/basis_set.hpp @@ -242,27 +242,35 @@ class BasisSet : public DataClass, AOType atomic_orbital_type = AOType::Spherical); /** - * @brief Constructor with shells, ECP shells, and structure + * @brief Constructor with shells, ECP shells, ECP electrons, and structure * @param name Name of the basis set * @param shells Vector of shells to initialize the basis set with * @param ecp_shells Vector of ECP shells to initialize the basis set with + * @param ecp_electrons Vector containing numbers of ECP electrons for each + * atom * @param structure The molecular structure * @param basis_type Whether to use spherical or cartesian atomic orbitals */ BasisSet(const std::string& name, const std::vector& shells, - const std::vector& ecp_shells, const Structure& structure, + const std::vector& ecp_shells, + const std::vector& ecp_electrons, + const Structure& structure, AOType basis_type = AOType::Spherical); /** - * @brief Constructor with shells, ECP shells, and structure shared pointer + * @brief Constructor with shells, ECP shells, ECP electrons, and structure + * shared pointer * @param name Name of the basis set * @param shells Vector of shells to initialize the basis set with * @param ecp_shells Vector of ECP shells to initialize the basis set with + * @param ecp_electrons Vector containing numbers of ECP electrons for each + * atom * @param structure Shared pointer to the molecular structure * @param basis_type Whether to use spherical or cartesian atomic orbitals */ BasisSet(const std::string& name, const std::vector& shells, const std::vector& ecp_shells, + const std::vector& ecp_electrons, std::shared_ptr structure, AOType basis_type = AOType::Spherical); @@ -303,35 +311,60 @@ class BasisSet : public DataClass, /** - * @brief Constructor with shells, ECP shells, auxiliary shells, and structure + * @brief Constructor with shells, auxiliary shells, and structure * @param name Name of the basis set * @param shells Vector of shells to initialize the basis set with - * @param ecp_shells Vector of ECP shells to initialize the basis set with * @param aux_shells Vector of auxiliary shells (e.g., for density fitting) * @param structure The molecular structure * @param basis_type Whether to use spherical or cartesian atomic orbitals */ BasisSet(const std::string& name, const std::vector& shells, - const std::vector& ecp_shells, const std::vector& aux_shells, const Structure& structure, AOType basis_type = AOType::Spherical); /** - * @brief Constructor with shells, ECP shells, auxiliary shells, and structure + * @brief Constructor with shells, auxiliary shells, and structure * shared pointer * @param name Name of the basis set * @param shells Vector of shells to initialize the basis set with - * @param ecp_shells Vector of ECP shells to initialize the basis set with * @param aux_shells Vector of auxiliary shells (e.g., for density fitting) * @param structure Shared pointer to the molecular structure * @param basis_type Whether to use spherical or cartesian atomic orbitals */ BasisSet(const std::string& name, const std::vector& shells, - const std::vector& ecp_shells, const std::vector& aux_shells, std::shared_ptr structure, AOType basis_type = AOType::Spherical); + /** + * @brief Constructor with shells, named auxiliary shells, and structure + * @param name Name of the basis set + * @param shells Vector of shells to initialize the basis set with + * @param aux_name Name of the auxiliary basis set + * @param aux_shells Vector of auxiliary shells (e.g., for density fitting) + * @param structure The molecular structure + * @param basis_type Whether to use spherical or cartesian atomic orbitals + */ + BasisSet(const std::string& name, const std::vector& shells, + const std::string& aux_name, const std::vector& aux_shells, + const Structure& structure, + AOType basis_type = AOType::Spherical); + + /** + * @brief Constructor with shells, named auxiliary shells, and structure + * shared pointer + * @param name Name of the basis set + * @param shells Vector of shells to initialize the basis set with + * @param aux_name Name of the auxiliary basis set + * @param aux_shells Vector of auxiliary shells (e.g., for density fitting) + * @param structure Shared pointer to the molecular structure + * @param basis_type Whether to use spherical or cartesian atomic orbitals + */ + BasisSet(const std::string& name, const std::vector& shells, + const std::string& aux_name, const std::vector& aux_shells, + std::shared_ptr structure, + AOType basis_type = AOType::Spherical); + /** * @brief Constructor with shells, ECP shells, ECP metadata, auxiliary shells, * auxiliary name, and structure diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index d2dd2eddc..2538c90af 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -352,21 +352,23 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, } BasisSet::BasisSet(const std::string& name, const std::vector& shells, - const std::vector& ecp_shells, + const std::vector& ecp_shells, const std::vector& ecp_electrons, const Structure& structure, AOType atomic_orbital_type) - : BasisSet(name, shells, ecp_shells, std::make_shared(structure), + : BasisSet(name, shells, ecp_shells, ecp_electrons, std::make_shared(structure), atomic_orbital_type) { QDK_LOG_TRACE_ENTERING(); } BasisSet::BasisSet(const std::string& name, const std::vector& shells, const std::vector& ecp_shells, + const std::vector& ecp_electrons, std::shared_ptr structure, AOType atomic_orbital_type) : _name(name), _atomic_orbital_type(atomic_orbital_type), _structure(structure), - _ecp_name("none") { + _ecp_name("none"), + _ecp_electrons(ecp_electrons) { QDK_LOG_TRACE_ENTERING(); if (!structure) { throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); @@ -396,6 +398,56 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, _ecp_shells_per_atom[atom_index].push_back(ecp_shell); } + if (!_is_valid()) { + throw std::invalid_argument("Tried to generate invalid BasisSet"); + } +} + +BasisSet::BasisSet(const std::string& name, const std::vector& shells, + const std::vector& aux_shells, + const Structure& structure, AOType atomic_orbital_type) + : BasisSet(name, shells, aux_shells, std::make_shared(structure), + atomic_orbital_type) { + QDK_LOG_TRACE_ENTERING(); +} + +BasisSet::BasisSet(const std::string& name, const std::vector& shells, + const std::vector& aux_shells, + std::shared_ptr structure, + AOType atomic_orbital_type) + : _name(name), + _atomic_orbital_type(atomic_orbital_type), + _structure(structure), + _ecp_name("none") { + QDK_LOG_TRACE_ENTERING(); + if (!structure) { + throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); + } + + // Organize shells by atom index + for (const auto& shell : shells) { + size_t atom_index = shell.atom_index; + + // Ensure we have enough space for this atom + if (atom_index >= _shells_per_atom.size()) { + _shells_per_atom.resize(atom_index + 1); + } + + _shells_per_atom[atom_index].push_back(shell); + } + + // Organize ECP shells by atom index + for (const auto& aux_shell : aux_shells) { + size_t atom_index = aux_shell.atom_index; + + // Ensure we have enough space for this atom + if (atom_index >= _aux_shells_per_atom.size()) { + _aux_shells_per_atom.resize(atom_index + 1); + } + + _aux_shells_per_atom[atom_index].push_back(aux_shell); + } + // Initialize ECP electrons vector with zeros for each atom _ecp_electrons.resize(structure->get_num_atoms(), 0); @@ -465,24 +517,25 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, } } + BasisSet::BasisSet(const std::string& name, const std::vector& shells, - const std::vector& ecp_shells, + const std::string& aux_name, const std::vector& aux_shells, const Structure& structure, AOType atomic_orbital_type) - : BasisSet(name, shells, ecp_shells, aux_shells, + : BasisSet(name, shells, aux_name, aux_shells, std::make_shared(structure), atomic_orbital_type) { QDK_LOG_TRACE_ENTERING(); } BasisSet::BasisSet(const std::string& name, const std::vector& shells, - const std::vector& ecp_shells, + const std::string& aux_name, const std::vector& aux_shells, std::shared_ptr structure, AOType atomic_orbital_type) : _name(name), _atomic_orbital_type(atomic_orbital_type), _structure(structure), - _ecp_name("none") { + _aux_name(aux_name) { QDK_LOG_TRACE_ENTERING(); if (!structure) { throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); @@ -491,31 +544,27 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, // Organize shells by atom index for (const auto& shell : shells) { size_t atom_index = shell.atom_index; + + // Ensure we have enough space for this atom if (atom_index >= _shells_per_atom.size()) { _shells_per_atom.resize(atom_index + 1); } - _shells_per_atom[atom_index].push_back(shell); - } - // Organize ECP shells by atom index - for (const auto& ecp_shell : ecp_shells) { - size_t atom_index = ecp_shell.atom_index; - if (atom_index >= _ecp_shells_per_atom.size()) { - _ecp_shells_per_atom.resize(atom_index + 1); - } - _ecp_shells_per_atom[atom_index].push_back(ecp_shell); + _shells_per_atom[atom_index].push_back(shell); } // Organize auxiliary shells by atom index for (const auto& aux_shell : aux_shells) { size_t atom_index = aux_shell.atom_index; + + // Ensure we have enough space for this atom if (atom_index >= _aux_shells_per_atom.size()) { _aux_shells_per_atom.resize(atom_index + 1); } + _aux_shells_per_atom[atom_index].push_back(aux_shell); } - // Initialize ECP electrons vector with zeros for each atom _ecp_electrons.resize(structure->get_num_atoms(), 0); if (!_is_valid()) { @@ -523,6 +572,7 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, } } + BasisSet::BasisSet(const std::string& name, const std::vector& shells, const std::string& ecp_name, const std::vector& ecp_shells, @@ -1514,7 +1564,10 @@ bool BasisSet::_is_valid() const { } } } - return has_shells && _is_consistent_with_structure(); + + bool ecp_consistency = (has_ecp_electrons() == has_ecp_shells()); + + return has_shells && _is_consistent_with_structure() && ecp_consistency; } std::string BasisSet::get_summary() const { diff --git a/cpp/tests/test_basis_set.cpp b/cpp/tests/test_basis_set.cpp index d1aa06a43..748af4341 100644 --- a/cpp/tests/test_basis_set.cpp +++ b/cpp/tests/test_basis_set.cpp @@ -95,6 +95,8 @@ TEST_F(BasisSetTest, Constructors) { std::vector shells; shells.emplace_back( Shell(0, OrbitalType::S, std::vector{1.0}, std::vector{2.0})); + BasisSet basis3("6-31G", shells); + BasisSet basis4("6-31G", shells, structure); EXPECT_EQ(std::string("6-31G"), basis4.get_name()); EXPECT_EQ(AOType::Spherical, basis4.get_atomic_orbital_type()); @@ -640,7 +642,7 @@ TEST_F(BasisSetTest, ECPShellConstruction) { rpow_p << 1; ecp_shells.emplace_back(0, OrbitalType::P, exp_p, coeff_p, rpow_p); - BasisSet basis("test-basis", shells, ecp_shells, structure); + BasisSet basis("test-basis", shells, ecp_shells, {2, 2}, structure); // Check ECP shell data EXPECT_TRUE(basis.has_ecp_shells()); @@ -887,7 +889,7 @@ TEST_F(BasisSetTest, ECPShellQueries) { rpow_d << 2; ecp_shells.emplace_back(1, OrbitalType::D, exp_d, coeff_d, rpow_d); - BasisSet basis("test-basis", shells, ecp_shells, structure); + BasisSet basis("test-basis", shells, ecp_shells, {0, 2, 0}, structure); // Test get_num_ecp_shells EXPECT_EQ(3u, basis.get_num_ecp_shells()); @@ -972,32 +974,6 @@ TEST_F(BasisSetTest, ECPShellValidation) { EXPECT_FALSE(regular_shell.has_radial_powers()); } -TEST_F(BasisSetTest, ECPShellsWithoutECPMetadata) { - // Test that we can have ECP shells without setting ECP metadata - std::vector coords = {{0.0, 0.0, 0.0}}; - std::vector symbols = {"Ag"}; - Structure structure(coords, symbols); - - std::vector shells; - shells.emplace_back(0, OrbitalType::S, std::vector{1.0}, std::vector{1.0}); - - std::vector ecp_shells; - Eigen::VectorXd exp(1), coeff(1); - Eigen::VectorXi rpow(1); - exp << 10.0; - coeff << 50.0; - rpow << 0; - ecp_shells.emplace_back(0, OrbitalType::S, exp, coeff, rpow); - - BasisSet basis("test-basis", shells, ecp_shells, structure); - - // Should have ECP shells but no ECP metadata - EXPECT_TRUE(basis.has_ecp_shells()); - EXPECT_EQ(1u, basis.get_num_ecp_shells()); - EXPECT_FALSE(basis.has_ecp_electrons()); - EXPECT_EQ("none", basis.get_ecp_name()); - EXPECT_EQ(0u, basis.get_ecp_electrons()[0]); -} TEST_F(BasisSetTest, ECPHDF5Serialization) { // Test HDF5 serialization with ECP data @@ -1857,6 +1833,10 @@ TEST_F(BasisSetTest, AuxiliaryBasisSetAccessors) { // Default: no auxiliary basis EXPECT_FALSE(basis.has_aux_basis()); EXPECT_EQ(0u, basis.get_num_aux_shells()); + std::vector aux_shells; + aux_shells.emplace_back( + Shell(0, OrbitalType::S, std::vector{2.0}, std::vector{1.0})); + BasisSet basis_with_aux("test", shells, "aux", aux_shells, structure); } TEST_F(BasisSetTest, AuxiliaryBasisFromSharedPtrConstructor) { @@ -1883,11 +1863,8 @@ TEST_F(BasisSetTest, AuxiliaryBasisFromSharedPtrConstructor) { aux_shells.emplace_back( Shell(2, OrbitalType::S, std::vector{2.0}, std::vector{1.0})); - // Empty ECP shells - std::vector ecp_shells; - - // Create primary basis with aux (ecp_shells empty) - BasisSet basis("custom-primary", shells, ecp_shells, aux_shells, *structure); + // Create primary basis with aux (no ecp) + BasisSet basis("custom-primary", shells, aux_shells, *structure); EXPECT_EQ("custom-primary", basis.get_name()); EXPECT_EQ(3u, basis.get_num_shells()); @@ -1908,9 +1885,6 @@ TEST_F(BasisSetTest, AuxiliaryBasisFromShellsConstructor) { shells.emplace_back( Shell(2, OrbitalType::S, std::vector{1.0}, std::vector{1.0})); - // Empty ECP shells - std::vector ecp_shells; - // Auxiliary shells std::vector aux_shells; aux_shells.emplace_back( @@ -1920,7 +1894,7 @@ TEST_F(BasisSetTest, AuxiliaryBasisFromShellsConstructor) { aux_shells.emplace_back( Shell(2, OrbitalType::S, std::vector{3.0}, std::vector{1.5})); - BasisSet basis("test-with-aux-shells", shells, ecp_shells, aux_shells, + BasisSet basis("test-with-aux-shells", shells, aux_shells, *structure); EXPECT_EQ("test-with-aux-shells", basis.get_name()); @@ -2074,11 +2048,8 @@ TEST_F(BasisSetTest, AuxiliaryBasisJSONSerialization) { aux_shells.emplace_back( Shell(2, OrbitalType::S, std::vector{2.0}, std::vector{1.0})); - std::vector ecp_shells; // empty - std::string aux_name = "my-aux"; - BasisSet basis("my-primary", shells, "", ecp_shells, {}, - aux_name, aux_shells, *structure); + BasisSet basis("my-primary", shells, aux_name, aux_shells, *structure); // In-memory JSON round-trip auto json = basis.to_json(); @@ -2125,10 +2096,8 @@ TEST_F(BasisSetTest, AuxiliaryBasisHDF5Serialization) { Shell(2, OrbitalType::S, std::vector{2.0}, std::vector{1.0})); std::string aux_name = "hdf5-aux"; - std::vector ecp_shells; // empty - BasisSet basis("hdf5-primary", shells, "", ecp_shells, {}, - aux_name, aux_shells, *structure); + BasisSet basis("hdf5-primary", shells, aux_name, aux_shells, *structure); std::string filename = "test_aux.basis_set.h5"; basis.to_hdf5_file(filename); @@ -2154,9 +2123,7 @@ TEST_F(BasisSetTest, AuxiliaryBasisSummary) { aux_shells.emplace_back( Shell(0, OrbitalType::S, std::vector{2.0}, std::vector{1.0})); - std::vector ecp_shells; // empty - - BasisSet basis("primary", shells, ecp_shells, aux_shells, structure); + BasisSet basis("primary", shells, aux_shells, structure); std::string summary = basis.get_summary(); EXPECT_FALSE(summary.empty()); @@ -2183,3 +2150,377 @@ TEST_F(BasisSetTest, FromBasisNameWithAuxSCFComparison) { EXPECT_FALSE(basis_no_aux->has_aux_basis()); EXPECT_TRUE(basis_with_aux->has_aux_basis()); } + + +TEST_F(BasisSetTest, AuxiliaryShellDataAccessors) { + // Construct a 2-atom structure with known aux shells and verify every + // accessor that returns shell data. + std::vector coords = {{0.0, 0.0, 0.0}, {1.4, 0.0, 0.0}}; + std::vector symbols = {"H", "H"}; + Structure structure(coords, symbols); + + std::vector shells; + shells.emplace_back(0, OrbitalType::S, std::vector{1.0}, std::vector{1.0}); + shells.emplace_back(1, OrbitalType::S, std::vector{1.0}, std::vector{1.0}); + + std::vector aux_shells; + aux_shells.emplace_back(0, OrbitalType::S, std::vector{2.0, 0.5}, + std::vector{0.6, 0.4}); + aux_shells.emplace_back(0, OrbitalType::P, std::vector{1.5}, + std::vector{0.8}); + aux_shells.emplace_back(1, OrbitalType::D, std::vector{3.0}, + std::vector{1.2}); + + BasisSet basis("test", shells, "my-aux", aux_shells, structure); + + // get_aux_shells() – flattened list + auto all_aux = basis.get_aux_shells(); + ASSERT_EQ(3u, all_aux.size()); + EXPECT_EQ(OrbitalType::S, all_aux[0].orbital_type); + EXPECT_EQ(0u, all_aux[0].atom_index); + EXPECT_EQ(2, all_aux[0].exponents.size()); + EXPECT_DOUBLE_EQ(2.0, all_aux[0].exponents[0]); + EXPECT_DOUBLE_EQ(0.5, all_aux[0].exponents[1]); + EXPECT_DOUBLE_EQ(0.6, all_aux[0].coefficients[0]); + EXPECT_DOUBLE_EQ(0.4, all_aux[0].coefficients[1]); + + EXPECT_EQ(OrbitalType::P, all_aux[1].orbital_type); + EXPECT_EQ(0u, all_aux[1].atom_index); + EXPECT_DOUBLE_EQ(1.5, all_aux[1].exponents[0]); + EXPECT_DOUBLE_EQ(0.8, all_aux[1].coefficients[0]); + + EXPECT_EQ(OrbitalType::D, all_aux[2].orbital_type); + EXPECT_EQ(1u, all_aux[2].atom_index); + EXPECT_DOUBLE_EQ(3.0, all_aux[2].exponents[0]); + EXPECT_DOUBLE_EQ(1.2, all_aux[2].coefficients[0]); + + // Aux shells must NOT have radial powers + for (const auto& s : all_aux) { + EXPECT_FALSE(s.has_radial_powers()); + } + + // get_aux_shells_for_atom() + const auto& atom0_aux = basis.get_aux_shells_for_atom(0); + ASSERT_EQ(2u, atom0_aux.size()); + EXPECT_EQ(OrbitalType::S, atom0_aux[0].orbital_type); + EXPECT_EQ(OrbitalType::P, atom0_aux[1].orbital_type); + + const auto& atom1_aux = basis.get_aux_shells_for_atom(1); + ASSERT_EQ(1u, atom1_aux.size()); + EXPECT_EQ(OrbitalType::D, atom1_aux[0].orbital_type); + + // get_aux_shell() – by global index + const auto& s0 = basis.get_aux_shell(0); + EXPECT_EQ(OrbitalType::S, s0.orbital_type); + EXPECT_EQ(0u, s0.atom_index); + + const auto& s1 = basis.get_aux_shell(1); + EXPECT_EQ(OrbitalType::P, s1.orbital_type); + + const auto& s2 = basis.get_aux_shell(2); + EXPECT_EQ(OrbitalType::D, s2.orbital_type); + EXPECT_EQ(1u, s2.atom_index); +} + + +TEST_F(BasisSetTest, AuxShellDataIntegrityJSONRoundTrip) { + std::vector coords = {{0.0, 0.0, 0.0}, {1.4, 0.0, 0.0}}; + std::vector symbols = {"H", "H"}; + Structure structure(coords, symbols); + + std::vector shells; + shells.emplace_back(0, OrbitalType::S, std::vector{1.0}, std::vector{1.0}); + shells.emplace_back(1, OrbitalType::S, std::vector{1.0}, std::vector{1.0}); + + std::vector aux_shells; + aux_shells.emplace_back(0, OrbitalType::S, std::vector{2.0, 0.5}, + std::vector{0.6, 0.4}); + aux_shells.emplace_back(1, OrbitalType::P, std::vector{1.5}, + std::vector{0.8}); + + BasisSet basis("json-aux-data", shells, "test-aux", aux_shells, structure); + + // In-memory round-trip + auto json = basis.to_json(); + auto loaded = BasisSet::from_json(json); + + ASSERT_TRUE(loaded->has_aux_basis()); + ASSERT_EQ(2u, loaded->get_num_aux_shells()); + + const auto& ls0 = loaded->get_aux_shell(0); + EXPECT_EQ(OrbitalType::S, ls0.orbital_type); + EXPECT_EQ(0u, ls0.atom_index); + ASSERT_EQ(2, ls0.exponents.size()); + EXPECT_DOUBLE_EQ(2.0, ls0.exponents[0]); + EXPECT_DOUBLE_EQ(0.5, ls0.exponents[1]); + EXPECT_DOUBLE_EQ(0.6, ls0.coefficients[0]); + EXPECT_DOUBLE_EQ(0.4, ls0.coefficients[1]); + EXPECT_FALSE(ls0.has_radial_powers()); + + const auto& ls1 = loaded->get_aux_shell(1); + EXPECT_EQ(OrbitalType::P, ls1.orbital_type); + EXPECT_EQ(1u, ls1.atom_index); + EXPECT_DOUBLE_EQ(1.5, ls1.exponents[0]); + EXPECT_DOUBLE_EQ(0.8, ls1.coefficients[0]); +} + +TEST_F(BasisSetTest, AuxShellDataIntegrityHDF5RoundTrip) { + std::vector coords = {{0.0, 0.0, 0.0}, {1.4, 0.0, 0.0}}; + std::vector symbols = {"H", "H"}; + Structure structure(coords, symbols); + + std::vector shells; + shells.emplace_back(0, OrbitalType::S, std::vector{1.0}, std::vector{1.0}); + shells.emplace_back(1, OrbitalType::S, std::vector{1.0}, std::vector{1.0}); + + std::vector aux_shells; + aux_shells.emplace_back(0, OrbitalType::S, std::vector{2.0, 0.5}, + std::vector{0.6, 0.4}); + aux_shells.emplace_back(1, OrbitalType::P, std::vector{1.5}, + std::vector{0.8}); + + BasisSet basis("hdf5-aux-data", shells, "test-aux", aux_shells, structure); + + std::string filename = "test_aux_data_integrity.basis_set.h5"; + basis.to_hdf5_file(filename); + auto loaded = BasisSet::from_hdf5_file(filename); + std::filesystem::remove(filename); + + ASSERT_TRUE(loaded->has_aux_basis()); + ASSERT_EQ(2u, loaded->get_num_aux_shells()); + + const auto& ls0 = loaded->get_aux_shell(0); + EXPECT_EQ(OrbitalType::S, ls0.orbital_type); + EXPECT_EQ(0u, ls0.atom_index); + ASSERT_EQ(2, ls0.exponents.size()); + EXPECT_DOUBLE_EQ(2.0, ls0.exponents[0]); + EXPECT_DOUBLE_EQ(0.5, ls0.exponents[1]); + EXPECT_DOUBLE_EQ(0.6, ls0.coefficients[0]); + EXPECT_DOUBLE_EQ(0.4, ls0.coefficients[1]); + EXPECT_FALSE(ls0.has_radial_powers()); + + const auto& ls1 = loaded->get_aux_shell(1); + EXPECT_EQ(OrbitalType::P, ls1.orbital_type); + EXPECT_EQ(1u, ls1.atom_index); + EXPECT_DOUBLE_EQ(1.5, ls1.exponents[0]); + EXPECT_DOUBLE_EQ(0.8, ls1.coefficients[0]); +} + +TEST_F(BasisSetTest, FullECPAndAuxSerializationRoundTrip) { + // The most complex constructor — verify ECP + aux survive round-trips. + std::vector coords = {{0.0, 0.0, 0.0}, {1.0, 0.0, 0.0}}; + std::vector symbols = {"Ag", "H"}; + Structure structure(coords, symbols); + + std::vector shells; + shells.emplace_back(0, OrbitalType::S, std::vector{1.0}, std::vector{1.0}); + shells.emplace_back(1, OrbitalType::S, std::vector{1.0}, std::vector{1.0}); + + Eigen::VectorXd exp(1), coeff(1); + Eigen::VectorXi rpow(1); + exp << 10.0; + coeff << 50.0; + rpow << 0; + std::vector ecp_shells; + ecp_shells.emplace_back(0, OrbitalType::S, exp, coeff, rpow); + std::vector ecp_electrons = {28, 0}; + + std::vector aux_shells; + aux_shells.emplace_back(0, OrbitalType::S, std::vector{5.0, 2.0}, + std::vector{2.0, 0.8}); + aux_shells.emplace_back(1, OrbitalType::P, std::vector{4.0}, + std::vector{1.5}); + + BasisSet basis("full-rt", shells, "my-ecp", ecp_shells, ecp_electrons, + "my-aux", aux_shells, structure); + + // JSON round-trip + { + auto json = basis.to_json(); + auto loaded = BasisSet::from_json(json); + + // Primary + EXPECT_EQ("full-rt", loaded->get_name()); + EXPECT_EQ(2u, loaded->get_num_shells()); + + // ECP + EXPECT_TRUE(loaded->has_ecp_shells()); + EXPECT_EQ("my-ecp", loaded->get_ecp_name()); + ASSERT_EQ(1u, loaded->get_num_ecp_shells()); + EXPECT_DOUBLE_EQ(10.0, loaded->get_ecp_shell(0).exponents[0]); + EXPECT_DOUBLE_EQ(50.0, loaded->get_ecp_shell(0).coefficients[0]); + EXPECT_EQ(0, loaded->get_ecp_shell(0).rpowers[0]); + EXPECT_TRUE(loaded->has_ecp_electrons()); + EXPECT_EQ(28u, loaded->get_ecp_electrons()[0]); + + // Aux + EXPECT_TRUE(loaded->has_aux_basis()); + EXPECT_EQ("my-aux", loaded->get_aux_name()); + ASSERT_EQ(2u, loaded->get_num_aux_shells()); + EXPECT_DOUBLE_EQ(5.0, loaded->get_aux_shell(0).exponents[0]); + EXPECT_DOUBLE_EQ(2.0, loaded->get_aux_shell(0).exponents[1]); + EXPECT_DOUBLE_EQ(4.0, loaded->get_aux_shell(1).exponents[0]); + EXPECT_FALSE(loaded->get_aux_shell(0).has_radial_powers()); + } + + // HDF5 round-trip + { + std::string filename = "test_full_ecp_aux.basis_set.h5"; + basis.to_hdf5_file(filename); + auto loaded = BasisSet::from_hdf5_file(filename); + std::filesystem::remove(filename); + + EXPECT_EQ("full-rt", loaded->get_name()); + EXPECT_TRUE(loaded->has_ecp_shells()); + EXPECT_EQ("my-ecp", loaded->get_ecp_name()); + EXPECT_EQ(28u, loaded->get_ecp_electrons()[0]); + EXPECT_TRUE(loaded->has_aux_basis()); + EXPECT_EQ("my-aux", loaded->get_aux_name()); + ASSERT_EQ(2u, loaded->get_num_aux_shells()); + EXPECT_DOUBLE_EQ(5.0, loaded->get_aux_shell(0).exponents[0]); + EXPECT_DOUBLE_EQ(4.0, loaded->get_aux_shell(1).exponents[0]); + } +} + +TEST_F(BasisSetTest, SharedPtrStructureConstructors) { + auto structure = std::make_shared( + std::vector{{0.0, 0.0, 0.0}, {1.4, 0.0, 0.0}}, + std::vector{"H", "H"}); + + std::vector shells; + shells.emplace_back(0, OrbitalType::S, std::vector{1.0}, std::vector{1.0}); + shells.emplace_back(1, OrbitalType::S, std::vector{1.0}, std::vector{1.0}); + + // Constructor: (name, shells, shared_ptr) + BasisSet b1("sp1", shells, structure); + EXPECT_TRUE(b1.has_structure()); + EXPECT_EQ(2u, b1.get_num_shells()); + + // Constructor: (name, shells, ecp_shells, ecp_electrons, shared_ptr) + Eigen::VectorXd exp(1), coeff(1); + Eigen::VectorXi rpow(1); + exp << 10.0; + coeff << 50.0; + rpow << 0; + std::vector ecp_shells; + ecp_shells.emplace_back(0, OrbitalType::S, exp, coeff, rpow); + std::vector ecp_electrons = {0, 2}; + + BasisSet b2("sp2", shells, ecp_shells, ecp_electrons, structure); + EXPECT_TRUE(b2.has_ecp_shells()); + EXPECT_TRUE(b2.has_structure()); + + // Constructor: (name, shells, ecp_name, ecp_shells, ecp_electrons, shared_ptr) + BasisSet b3("sp3", shells, "ecp", ecp_shells, ecp_electrons, structure); + EXPECT_EQ("ecp", b3.get_ecp_name()); + EXPECT_TRUE(b3.has_structure()); + + // Constructor: (name, shells, aux_shells, shared_ptr) + std::vector aux_shells; + aux_shells.emplace_back(0, OrbitalType::S, std::vector{2.0}, + std::vector{1.0}); + BasisSet b4("sp4", shells, aux_shells, structure); + EXPECT_TRUE(b4.has_aux_basis()); + EXPECT_TRUE(b4.has_structure()); + + // Constructor: (name, shells, aux_name, aux_shells, shared_ptr) + BasisSet b5("sp5", shells, "aux", aux_shells, structure); + EXPECT_EQ("aux", b5.get_aux_name()); + EXPECT_TRUE(b5.has_structure()); + + // Full constructor: (name, shells, ecp_name, ecp_shells, ecp_electrons, + // aux_name, aux_shells, shared_ptr) + BasisSet b6("sp6", shells, "ecp", ecp_shells, ecp_electrons, "aux", + aux_shells, structure); + EXPECT_TRUE(b6.has_ecp_shells()); + EXPECT_TRUE(b6.has_aux_basis()); + EXPECT_TRUE(b6.has_structure()); +} + + +TEST_F(BasisSetTest, MoveConstructorAndAssignment) { + std::vector coords = {{0.0, 0.0, 0.0}, {1.4, 0.0, 0.0}}; + std::vector symbols = {"H", "H"}; + Structure structure(coords, symbols); + + std::vector shells; + shells.emplace_back(0, OrbitalType::S, std::vector{1.0}, std::vector{1.0}); + shells.emplace_back(1, OrbitalType::S, std::vector{1.0}, std::vector{1.0}); + + std::vector aux_shells; + aux_shells.emplace_back(0, OrbitalType::S, std::vector{2.0}, + std::vector{1.0}); + + // Move constructor + BasisSet original("move-src", shells, "aux", aux_shells, structure); + BasisSet moved(std::move(original)); + + EXPECT_EQ("move-src", moved.get_name()); + EXPECT_EQ(2u, moved.get_num_shells()); + EXPECT_TRUE(moved.has_aux_basis()); + EXPECT_EQ("aux", moved.get_aux_name()); + EXPECT_EQ(1u, moved.get_num_aux_shells()); + + // Move assignment + BasisSet target("target", shells); + EXPECT_FALSE(target.has_aux_basis()); + + BasisSet source("move-src2", shells, "aux2", aux_shells, structure); + target = std::move(source); + + EXPECT_EQ("move-src2", target.get_name()); + EXPECT_TRUE(target.has_aux_basis()); + EXPECT_EQ("aux2", target.get_aux_name()); +} + + +TEST_F(BasisSetTest, AuxiliaryShellErrorPaths) { + std::vector coords = {{0.0, 0.0, 0.0}}; + std::vector symbols = {"H"}; + Structure structure(coords, symbols); + + std::vector shells; + shells.emplace_back(0, OrbitalType::S, std::vector{1.0}, std::vector{1.0}); + + std::vector aux_shells; + aux_shells.emplace_back(0, OrbitalType::S, std::vector{2.0}, + std::vector{1.0}); + + BasisSet basis("err-test", shells, aux_shells, structure); + + // Out-of-range global index + EXPECT_THROW(basis.get_aux_shell(999), std::out_of_range); + + // Basis without aux shells – queries should still be safe + BasisSet no_aux("no-aux", shells, structure); + EXPECT_EQ(0u, no_aux.get_num_aux_shells()); + EXPECT_TRUE(no_aux.get_aux_shells().empty()); +} + + +TEST_F(BasisSetTest, AuxiliaryBasisCartesianAOType) { + std::vector coords = {{0.0, 0.0, 0.0}}; + std::vector symbols = {"H"}; + Structure structure(coords, symbols); + + std::vector shells; + shells.emplace_back(0, OrbitalType::S, std::vector{1.0}, std::vector{1.0}); + + std::vector aux_shells; + aux_shells.emplace_back(0, OrbitalType::S, std::vector{2.0}, + std::vector{1.0}); + aux_shells.emplace_back(0, OrbitalType::D, std::vector{3.0}, + std::vector{1.5}); + + BasisSet basis("cart-aux", shells, "my-aux", aux_shells, structure, + AOType::Cartesian); + + EXPECT_EQ(AOType::Cartesian, basis.get_atomic_orbital_type()); + EXPECT_TRUE(basis.has_aux_basis()); + EXPECT_EQ("my-aux", basis.get_aux_name()); + EXPECT_EQ(2u, basis.get_num_aux_shells()); + + // D shell: Cartesian has 6 AOs instead of spherical 5 + EXPECT_EQ(6u, aux_shells[1].get_num_atomic_orbitals(AOType::Cartesian)); + EXPECT_EQ(5u, aux_shells[1].get_num_atomic_orbitals(AOType::Spherical)); +} From 606cbd249421edaa3cc25f0f0da90869013335b8 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Wed, 15 Apr 2026 04:01:34 +0000 Subject: [PATCH 03/33] documentations --- cpp/include/qdk/chemistry/data/basis_set.hpp | 12 +- .../source/_static/examples/cpp/basis_set.cpp | 56 ++++++++ .../_static/examples/python/basis_set.py | 58 ++++++++ .../user/comprehensive/data/basis_set.rst | 124 ++++++++++++++++-- 4 files changed, 233 insertions(+), 17 deletions(-) diff --git a/cpp/include/qdk/chemistry/data/basis_set.hpp b/cpp/include/qdk/chemistry/data/basis_set.hpp index cb00d40b7..877434155 100644 --- a/cpp/include/qdk/chemistry/data/basis_set.hpp +++ b/cpp/include/qdk/chemistry/data/basis_set.hpp @@ -247,7 +247,7 @@ class BasisSet : public DataClass, * @param shells Vector of shells to initialize the basis set with * @param ecp_shells Vector of ECP shells to initialize the basis set with * @param ecp_electrons Vector containing numbers of ECP electrons for each - * atom + * atom, atoms ordered same as in the structure * @param structure The molecular structure * @param basis_type Whether to use spherical or cartesian atomic orbitals */ @@ -264,7 +264,7 @@ class BasisSet : public DataClass, * @param shells Vector of shells to initialize the basis set with * @param ecp_shells Vector of ECP shells to initialize the basis set with * @param ecp_electrons Vector containing numbers of ECP electrons for each - * atom + * atom, atoms ordered same as in the structure * @param structure Shared pointer to the molecular structure * @param basis_type Whether to use spherical or cartesian atomic orbitals */ @@ -282,7 +282,7 @@ class BasisSet : public DataClass, * @param ecp_name Name of the ECP basis set * @param ecp_shells Vector of ECP shells to initialize the basis set with * @param ecp_electrons Vector containing numbers of ECP electrons for each - * atom + * atom, atoms ordered same as in the structure * @param structure The molecular structure * @param basis_type Whether to use spherical or cartesian atomic orbitals */ @@ -299,7 +299,7 @@ class BasisSet : public DataClass, * @param ecp_shells Vector of ECP shells to initialize the basis set with * @param ecp_name Name of the ECP basis set * @param ecp_electrons Vector containing numbers of ECP electrons for each - * atom + * atom, atoms ordered same as in the structure * @param structure Shared pointer to the molecular structure * @param basis_type Whether to use spherical or cartesian atomic orbitals */ @@ -373,7 +373,7 @@ class BasisSet : public DataClass, * @param ecp_name Name of the ECP basis set * @param ecp_shells Vector of ECP shells to initialize the basis set with * @param ecp_electrons Vector containing numbers of ECP electrons for each - * atom + * atom, atoms ordered same as in the structure * @param aux_name Name of the auxiliary basis set * @param aux_shells Vector of auxiliary shells (e.g., for density fitting) * @param structure The molecular structure @@ -394,7 +394,7 @@ class BasisSet : public DataClass, * @param ecp_name Name of the ECP basis set * @param ecp_shells Vector of ECP shells to initialize the basis set with * @param ecp_electrons Vector containing numbers of ECP electrons for each - * atom + * atom, atoms ordered same as in the structure * @param aux_name Name of the auxiliary basis set * @param aux_shells Vector of auxiliary shells (e.g., for density fitting) * @param structure Shared pointer to the molecular structure diff --git a/docs/source/_static/examples/cpp/basis_set.cpp b/docs/source/_static/examples/cpp/basis_set.cpp index dbef5c62f..b3db0419b 100644 --- a/docs/source/_static/examples/cpp/basis_set.cpp +++ b/docs/source/_static/examples/cpp/basis_set.cpp @@ -30,6 +30,14 @@ int main() { // end-cell-loading // -------------------------------------------------------------------------------------------- + // -------------------------------------------------------------------------------------------- + // start-cell-loading-with-aux + // Load a primary basis set with an auxiliary basis set for density fitting + auto basis_with_aux = + BasisSet::from_basis_name("def2-svp", "def2-universal-jfit", structure); + // end-cell-loading-with-aux + // -------------------------------------------------------------------------------------------- + // -------------------------------------------------------------------------------------------- // start-cell-create // Create a shell with multiple primitives @@ -163,5 +171,53 @@ int main() { // end-cell-library // -------------------------------------------------------------------------------------------- + // -------------------------------------------------------------------------------------------- + // start-cell-ecp + // Create an ECP shell with radial powers (r^n terms) + Eigen::VectorXd ecp_exp(2), ecp_coeff(2); + Eigen::VectorXi ecp_rpow(2); + ecp_exp << 10.0, 5.0; + ecp_coeff << 50.0, 20.0; + ecp_rpow << 0, 2; + Shell ecp_shell(0, OrbitalType::S, ecp_exp, ecp_coeff, ecp_rpow); + + // Create a basis set with ECP data + std::vector ecp_shells = {ecp_shell}; + std::vector ecp_electrons = {28, 0, 0}; + BasisSet basis_with_ecp("my-basis", shells, "my-ecp", ecp_shells, + ecp_electrons, structure); + + // Query ECP data + bool has_ecp = basis_with_ecp.has_ecp_shells(); + std::string ecp_name = basis_with_ecp.get_ecp_name(); + size_t num_ecp_shells = basis_with_ecp.get_num_ecp_shells(); + // end-cell-ecp + // -------------------------------------------------------------------------------------------- + + // -------------------------------------------------------------------------------------------- + // start-cell-auxiliary + // Create auxiliary shells for density fitting + std::vector aux_shells; + aux_shells.emplace_back(0, OrbitalType::S, std::vector{5.0}, + std::vector{2.0}); + aux_shells.emplace_back(1, OrbitalType::S, std::vector{4.0}, + std::vector{1.5}); + + // Construct a basis set with a named auxiliary basis + BasisSet basis_with_aux_manual("my-basis", shells, "my-aux-fit", aux_shells, + structure); + + // Query auxiliary data + bool has_aux = basis_with_aux_manual.has_aux_basis(); + std::string aux_name = basis_with_aux_manual.get_aux_name(); + size_t num_aux = basis_with_aux_manual.get_num_aux_shells(); + + // Retrieve auxiliary shell data + auto all_aux_shells = basis_with_aux_manual.get_aux_shells(); + const Shell& aux_s = basis_with_aux_manual.get_aux_shell(0); + const auto& atom0_aux = basis_with_aux_manual.get_aux_shells_for_atom(0); + // end-cell-auxiliary + // -------------------------------------------------------------------------------------------- + return 0; } diff --git a/docs/source/_static/examples/python/basis_set.py b/docs/source/_static/examples/python/basis_set.py index 1669c39cf..812db1cdf 100644 --- a/docs/source/_static/examples/python/basis_set.py +++ b/docs/source/_static/examples/python/basis_set.py @@ -29,6 +29,16 @@ # end-cell-loading ################################################################################ +################################################################################ +# start-cell-loading-with-aux +# Load a primary basis set with an auxiliary basis set for density fitting +basis_with_aux = BasisSet.from_basis_name("def2-svp", "def2-universal-jfit", structure) +print(f"Primary shells: {basis_with_aux.get_num_shells()}") +print(f"Auxiliary shells: {basis_with_aux.get_num_aux_shells()}") +print(f"Auxiliary name: {basis_with_aux.get_aux_name()}") +# end-cell-loading-with-aux +################################################################################ + ################################################################################ # start-cell-create # Create a shell with multiple primitives @@ -120,6 +130,54 @@ Path("molecule.basis_set.h5").unlink() ################################################################################ +################################################################################ +# start-cell-ecp +# Create an ECP shell with radial powers (r^n terms) +ecp_exponents = np.array([10.0, 5.0]) +ecp_coefficients = np.array([50.0, 20.0]) +ecp_rpowers = np.array([0, 2], dtype=np.int32) +ecp_shell = Shell(0, OrbitalType.S, ecp_exponents, ecp_coefficients, ecp_rpowers) + +# Create a basis set with ECP data +ecp_shells = [ecp_shell] +ecp_electrons = [28, 0, 0] # 28 core electrons replaced on the first atom +basis_with_ecp = BasisSet( + "my-basis", [shell1, shell2], "my-ecp", ecp_shells, ecp_electrons, structure +) + +# Query ECP data +print(f"Has ECP shells: {basis_with_ecp.has_ecp_shells()}") +print(f"ECP name: {basis_with_ecp.get_ecp_name()}") +print(f"ECP electrons: {list(basis_with_ecp.get_ecp_electrons())}") +print(f"Num ECP shells: {basis_with_ecp.get_num_ecp_shells()}") +# end-cell-ecp +################################################################################ + +################################################################################ +# start-cell-auxiliary +# Create auxiliary shells for density fitting +aux_shells = [ + Shell(0, OrbitalType.S, np.array([5.0]), np.array([2.0])), + Shell(1, OrbitalType.S, np.array([4.0]), np.array([1.5])), +] + +# Construct a basis set with a named auxiliary basis +basis_with_aux_manual = BasisSet( + "my-basis", [shell1, shell2], "my-aux-fit", aux_shells, structure +) + +# Query auxiliary data +print(f"Has auxiliary: {basis_with_aux_manual.has_aux_basis()}") +print(f"Auxiliary name: {basis_with_aux_manual.get_aux_name()}") +print(f"Num aux shells: {basis_with_aux_manual.get_num_aux_shells()}") + +# Retrieve auxiliary shell data +for i in range(basis_with_aux_manual.get_num_aux_shells()): + s = basis_with_aux_manual.get_aux_shell(i) + print(f" Aux shell {i}: atom={s.atom_index}, type={s.orbital_type}") +# end-cell-auxiliary +################################################################################ + ################################################################################ # start-cell-utility-functions # Convert orbital type to string (returns str) diff --git a/docs/source/user/comprehensive/data/basis_set.rst b/docs/source/user/comprehensive/data/basis_set.rst index 9ff9f7bca..8e46cb249 100644 --- a/docs/source/user/comprehensive/data/basis_set.rst +++ b/docs/source/user/comprehensive/data/basis_set.rst @@ -19,6 +19,8 @@ Key features of the :class:`~qdk_chemistry.data.BasisSet` class include: - Basis set metadata (name, parameters) - Integration with molecular structure information - On-demand expansion of shells to individual basis functions +- Effective Core Potentials (ECP) with radial powers +- Auxiliary basis sets for density fitting Usage ----- @@ -98,6 +100,22 @@ The library supports three methods for loading basis sets: .. seealso:: For a complete list of available basis sets, see the :doc:`Supported Basis Sets <../basis_functionals>` documentation. +The library also supports loading an auxiliary basis set alongside the primary basis set in a single call: + +.. tab:: C++ API + + .. literalinclude:: ../../../_static/examples/cpp/basis_set.cpp + :language: cpp + :start-after: // start-cell-loading-with-aux + :end-before: // end-cell-loading-with-aux + +.. tab:: Python API + + .. literalinclude:: ../../../_static/examples/python/basis_set.py + :language: python + :start-after: # start-cell-loading-with-aux + :end-before: # end-cell-loading-with-aux + Creating a basis set -------------------- @@ -162,6 +180,55 @@ The ``Shell`` structure contains information about a group of basis functions: :start-after: # start-cell-shells :end-before: # end-cell-shells +Working with ECP shells +----------------------- + +Effective Core Potentials (ECPs) replace inner-core electrons with a pseudopotential, reducing computational cost for heavy atoms. +ECP shells are stored alongside primary shells but include an additional **radial powers** vector (:math:`r^n` terms). + +ECP data is specified at construction time via dedicated constructors that accept ``ecp_shells``, ``ecp_electrons``, and an optional ``ecp_name``. +The ``ecp_electrons`` vector records how many core electrons each atom has replaced. + +.. tab:: C++ API + + .. literalinclude:: ../../../_static/examples/cpp/basis_set.cpp + :language: cpp + :start-after: // start-cell-ecp + :end-before: // end-cell-ecp + +.. tab:: Python API + + .. literalinclude:: ../../../_static/examples/python/basis_set.py + :language: python + :start-after: # start-cell-ecp + :end-before: # end-cell-ecp + +.. note:: + If a basis set from the library includes an ECP, it will be loaded automatically. + Manual ECP construction is only needed for custom basis sets. + +Auxiliary basis sets +-------------------- + +Auxiliary basis sets are used in density-fitting (DF) and resolution-of-the-identity (RI) approximations to speed up two-electron integral evaluation. +The auxiliary shells are stored inside the same :class:`~qdk_chemistry.data.BasisSet` object as supplementary data alongside the primary shells. + +Auxiliary basis data can be attached at construction time or loaded from the library using ``from_basis_name`` with an auxiliary name. + +.. tab:: C++ API + + .. literalinclude:: ../../../_static/examples/cpp/basis_set.cpp + :language: cpp + :start-after: // start-cell-auxiliary + :end-before: // end-cell-auxiliary + +.. tab:: Python API + + .. literalinclude:: ../../../_static/examples/python/basis_set.py + :language: python + :start-after: # start-cell-auxiliary + :end-before: # end-cell-auxiliary + Serialization ------------- @@ -193,24 +260,43 @@ JSON representation of a :class:`~qdk_chemistry.data.BasisSet` has the following "coefficients": [0.1543289673, 0.5353281423, 0.4446345422], "exponents": [3.425250914, 0.6239137298, 0.168855404], "orbital_type": "s" - }, - { - "coefficients": [0.1559162750, 0.6076837186], - "exponents": [0.7868272350, 0.1881288540], - "orbital_type": "p" } ] - }, - { - "atom_index": 1, - "shells": ["..."] } ], "basis_type": "spherical", "name": "6-31G", "num_atoms": 2, "num_basis_functions": 9, - "num_shells": 3 + "num_shells": 3, + "ecp_name": "my-ecp", + "ecp_electrons": [28, 0], + "ecp_atoms": [ + { + "atom_index": 0, + "shells": [ + { + "coefficients": [50.0, 20.0], + "exponents": [10.0, 5.0], + "orbital_type": "s", + "rpowers": [0, 2] + } + ] + } + ], + "aux_name": "my-aux-fit", + "aux_atoms": [ + { + "atom_index": 0, + "shells": [ + { + "coefficients": [2.0], + "exponents": [5.0], + "orbital_type": "s" + } + ] + } + ] } HDF5 format @@ -227,8 +313,24 @@ HDF5 representation of a :class:`~qdk_chemistry.data.BasisSet` has the following │ ├── exponents # Dataset: float64, 1D Array of orbital exponents │ ├── num_primitives # Dataset: uint32, 1D Array of number of primitives per orbital │ └── orbital_types # Dataset: int32, 1D Array of orbital type per orbital + ├── ecp_shells/ # Group (present when ECP is defined) + │ ├── atom_indices # Dataset: uint32 + │ ├── coefficients # Dataset: float64 + │ ├── exponents # Dataset: float64 + │ ├── rpowers # Dataset: int32, 1D Array of radial powers per primitive + │ ├── num_primitives # Dataset: uint32 + │ └── orbital_types # Dataset: int32 + ├── aux_shells/ # Group (present when auxiliary basis is defined) + │ ├── atom_indices # Dataset: uint32 + │ ├── coefficients # Dataset: float64 + │ ├── exponents # Dataset: float64 + │ ├── num_primitives # Dataset: uint32 + │ └── orbital_types # Dataset: int32 └── metadata/ # Group - └── name # Attribute: string value of the basis set name + ├── name # Attribute: string value of the basis set name + ├── ecp_name # Attribute: string (optional) + ├── ecp_electrons # Dataset: uint64 (optional) + └── aux_name # Attribute: string (optional) .. tab:: C++ API From 0145ade58491c91547fb25af784e8b32f8fc3f75 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Wed, 15 Apr 2026 06:49:19 +0000 Subject: [PATCH 04/33] fix some pytest --- python/src/pybind11/data/basis_set.cpp | 67 ++++++++++++++----- .../qdk_chemistry/plugins/pyscf/conversion.py | 6 +- python/tests/test_pyscf_plugin.py | 5 +- 3 files changed, 56 insertions(+), 22 deletions(-) diff --git a/python/src/pybind11/data/basis_set.cpp b/python/src/pybind11/data/basis_set.cpp index f82b8d3aa..ee7d91e30 100644 --- a/python/src/pybind11/data/basis_set.cpp +++ b/python/src/pybind11/data/basis_set.cpp @@ -231,6 +231,20 @@ A shell represents a group of atomic orbitals with the same atom, angular moment // ----------------------------------------------------------------------- basis_set.def( py::init([](py::args args, py::kwargs kwargs) -> BasisSet { + // --- Helper: check whether a Python type name equals a target ----- + auto type_name = [](const py::object& obj) -> std::string { + return py::str(obj.get_type().attr("__name__")).cast(); + }; + + // --- Helper: true when any Shell in a py::list has rpowers -------- + // ECP shells carry rpowers; auxiliary shells do not. + auto list_has_ecp_shells = [](const py::list& lst) -> bool { + for (auto& item : lst) { + if (item.cast().has_radial_powers()) return true; + } + return false; + }; + // --- Collect positional args into a mutable vector ---------------- std::vector a; a.reserve(args.size()); @@ -238,27 +252,23 @@ A shell represents a group of atomic orbitals with the same atom, angular moment a.push_back(args[i].cast()); // If last positional arg is AOType, pop it. - // We check if the Python type name matches "AOType" to avoid - // accidentally casting e.g. an int or Structure. AOType ao = AOType::Spherical; - if (!a.empty()) { - std::string tname = - py::str(a.back().get_type().attr("__name__")).cast(); - if (tname == "AOType") { - ao = a.back().cast(); - a.pop_back(); - } + if (!a.empty() && type_name(a.back()) == "AOType") { + ao = a.back().cast(); + a.pop_back(); } // Keyword override if (kwargs.contains("atomic_orbital_type")) ao = kwargs["atomic_orbital_type"].cast(); - // --- Support fully-keyword calls ---------------------------------- - // BasisSet(name=..., shells=..., atomic_orbital_type=...) - if (a.empty() && kwargs.contains("name") && kwargs.contains("shells")) { - auto name = kwargs["name"].cast(); - auto shells = to_shell_vec(kwargs["shells"].cast()); - return BasisSet(name, shells, ao); + // --- Merge keyword args into positional vector -------------------- + // Supports mixed calls like BasisSet("name", shells=shells) by + // pulling recognised kwargs into the positional vector when the + // corresponding slot is not yet filled. + // Expected positional order: name, shells, ... + if (a.size() >= 1 && py::isinstance(a[0]) && + a.size() < 2 && kwargs.contains("shells")) { + a.push_back(kwargs["shells"].cast()); } const size_t n = a.size(); @@ -267,6 +277,13 @@ A shell represents a group of atomic orbitals with the same atom, angular moment if (n == 1 && py::isinstance(a[0])) return BasisSet(a[0].cast()); + // --- Fully-keyword calls ------------------------------------------ + if (n == 0 && kwargs.contains("name") && kwargs.contains("shells")) { + auto name = kwargs["name"].cast(); + auto shells = to_shell_vec(kwargs["shells"].cast()); + return BasisSet(name, shells, ao); + } + if (n < 2 || !py::isinstance(a[0]) || !py::isinstance(a[1])) throw py::type_error( @@ -283,11 +300,25 @@ A shell represents a group of atomic orbitals with the same atom, angular moment if (n == 3 && py::isinstance(a[2])) return BasisSet(name, shells, a[2].cast(), ao); - // (name, shells, aux_shells, structure) + // (name, shells, , structure) -- n==4 + // Disambiguate: if shells in a[2] have rpowers → ECP path (needs + // ecp_electrons); if not → auxiliary path. + // C++ has no (name, shells, ecp_shells, structure) constructor, so + // ECP shells at n==4 use an empty ecp_electrons vector via the + // 5-arg ECP constructor. if (n == 4 && py::isinstance(a[2]) && - py::isinstance(a[3])) - return BasisSet(name, shells, to_shell_vec(a[2].cast()), + py::isinstance(a[3])) { + auto extra = a[2].cast(); + if (list_has_ecp_shells(extra)) { + // ECP shells without explicit ecp_electrons → supply empty vec + return BasisSet(name, shells, to_shell_vec(extra), + std::vector(), + a[3].cast(), ao); + } + // Auxiliary shells + return BasisSet(name, shells, to_shell_vec(extra), a[3].cast(), ao); + } if (n == 5) { if (py::isinstance(a[2])) { diff --git a/python/src/qdk_chemistry/plugins/pyscf/conversion.py b/python/src/qdk_chemistry/plugins/pyscf/conversion.py index e89861a20..da78ece81 100644 --- a/python/src/qdk_chemistry/plugins/pyscf/conversion.py +++ b/python/src/qdk_chemistry/plugins/pyscf/conversion.py @@ -391,8 +391,10 @@ def pyscf_mol_to_qdk_basis( if any(n > 0 for n in ecp_electrons): return BasisSet(basis_name, shells, ecp_name, ecp_shells, ecp_electrons, structure, AOType.Spherical) - # Create BasisSet with name, shells, ecp_shells, structure, and basis type - return BasisSet(basis_name, shells, ecp_shells, structure, AOType.Spherical) + # Fallback: include ECP shells (with zero electrons) only if present + if ecp_shells: + return BasisSet(basis_name, shells, ecp_shells, ecp_electrons, structure, AOType.Spherical) + return BasisSet(basis_name, shells, structure, AOType.Spherical) def orbitals_to_scf( diff --git a/python/tests/test_pyscf_plugin.py b/python/tests/test_pyscf_plugin.py index 928cf140d..416f32f4d 100644 --- a/python/tests/test_pyscf_plugin.py +++ b/python/tests/test_pyscf_plugin.py @@ -2600,11 +2600,12 @@ def test_ecp_edge_cases(self): # Edge case 1: ECP shells exist without ECP metadata shells = [Shell(0, OrbitalType.S, [1.0], [1.0])] ecp_shells = [Shell(0, OrbitalType.S, [10.0, 5.0], [50.0, 20.0], [0, 2])] - qdk_basis_no_meta = BasisSet("test-basis", shells, ecp_shells, ag_structure, AOType.Spherical) + ecp_electrons = [10] + qdk_basis_no_meta = BasisSet("test-basis", shells, ecp_shells, ecp_electrons, ag_structure, AOType.Spherical) assert qdk_basis_no_meta.has_ecp_shells() assert qdk_basis_no_meta.get_num_ecp_shells() == 1 - assert not qdk_basis_no_meta.has_ecp_electrons() # No metadata set + assert qdk_basis_no_meta.has_ecp_electrons() # No metadata set assert qdk_basis_no_meta.get_ecp_name() == "none" # Edge case 2: Full ECP structure format roundtrip From 8bda1e04d4dfdd8b2b3e0fb1ab95e16f19402238 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Wed, 15 Apr 2026 06:53:46 +0000 Subject: [PATCH 05/33] pre-commit --- cpp/include/qdk/chemistry/data/basis_set.hpp | 69 ++++++++++---------- cpp/src/qdk/chemistry/data/basis_set.cpp | 44 ++++++------- cpp/tests/test_basis_set.cpp | 21 +++--- python/src/pybind11/data/basis_set.cpp | 40 +++++------- python/tests/test_basis_set.py | 20 +----- 5 files changed, 81 insertions(+), 113 deletions(-) diff --git a/cpp/include/qdk/chemistry/data/basis_set.hpp b/cpp/include/qdk/chemistry/data/basis_set.hpp index 877434155..754d245c5 100644 --- a/cpp/include/qdk/chemistry/data/basis_set.hpp +++ b/cpp/include/qdk/chemistry/data/basis_set.hpp @@ -187,15 +187,16 @@ struct Shell { class BasisSet : public DataClass, public std::enable_shared_from_this { public: -// /** -// * @brief Constructor with basis set name and structure -// * @param name Name of the basis set (e.g., "6-31G", "cc-pVDZ") -// * @param structure The molecular structure -// * @param atomic_orbital_type Whether to use spherical or cartesian atomic -// * orbitals -// */ -// BasisSet(const std::string& name, const Structure& structure, -// AOType atomic_orbital_type = AOType::Spherical); + // /** + // * @brief Constructor with basis set name and structure + // * @param name Name of the basis set (e.g., "6-31G", "cc-pVDZ") + // * @param structure The molecular structure + // * @param atomic_orbital_type Whether to use spherical or cartesian + // atomic + // * orbitals + // */ + // BasisSet(const std::string& name, const Structure& structure, + // AOType atomic_orbital_type = AOType::Spherical); /** * @brief Constructor with shells @@ -219,15 +220,16 @@ class BasisSet : public DataClass, const Structure& structure, AOType atomic_orbital_type = AOType::Spherical); -// /** -// * @brief Constructor with basis set name and structure shared pointer -// * @param name Name of the basis set (e.g., "6-31G", "cc-pVDZ") -// * @param structure Shared pointer to the molecular structure -// * @param atomic_orbital_type Whether to use spherical or cartesian atomic -// * orbitals -// */ -// BasisSet(const std::string& name, std::shared_ptr structure, -// AOType atomic_orbital_type = AOType::Spherical); + // /** + // * @brief Constructor with basis set name and structure shared pointer + // * @param name Name of the basis set (e.g., "6-31G", "cc-pVDZ") + // * @param structure Shared pointer to the molecular structure + // * @param atomic_orbital_type Whether to use spherical or cartesian + // atomic + // * orbitals + // */ + // BasisSet(const std::string& name, std::shared_ptr structure, + // AOType atomic_orbital_type = AOType::Spherical); /** * @brief Constructor with shells and structure shared pointer @@ -253,8 +255,7 @@ class BasisSet : public DataClass, */ BasisSet(const std::string& name, const std::vector& shells, const std::vector& ecp_shells, - const std::vector& ecp_electrons, - const Structure& structure, + const std::vector& ecp_electrons, const Structure& structure, AOType basis_type = AOType::Spherical); /** @@ -309,7 +310,6 @@ class BasisSet : public DataClass, std::shared_ptr structure, AOType basis_type = AOType::Spherical); - /** * @brief Constructor with shells, auxiliary shells, and structure * @param name Name of the basis set @@ -347,8 +347,7 @@ class BasisSet : public DataClass, */ BasisSet(const std::string& name, const std::vector& shells, const std::string& aux_name, const std::vector& aux_shells, - const Structure& structure, - AOType basis_type = AOType::Spherical); + const Structure& structure, AOType basis_type = AOType::Spherical); /** * @brief Constructor with shells, named auxiliary shells, and structure @@ -381,10 +380,9 @@ class BasisSet : public DataClass, */ BasisSet(const std::string& name, const std::vector& shells, const std::string& ecp_name, const std::vector& ecp_shells, - const std::vector& ecp_electrons, + const std::vector& ecp_electrons, const std::string& aux_name, const std::vector& aux_shells, - const Structure& structure, - AOType basis_type = AOType::Spherical); + const Structure& structure, AOType basis_type = AOType::Spherical); /** * @brief Constructor with shells, ECP shells, ECP metadata, auxiliary shells, @@ -540,32 +538,34 @@ class BasisSet : public DataClass, std::shared_ptr structure, AOType atomic_orbital_type = AOType::Spherical); - - /** * @brief Create a basis set from a basis name and auxiliary basis name * @param basis_name Name of the basis set (e.g., "6-31G", "cc-pVDZ") - * @param aux_basis_name Name of the auxiliary basis set (e.g., "cc-pVDZ-RIFIT") + * @param aux_basis_name Name of the auxiliary basis set (e.g., + * "cc-pVDZ-RIFIT") * @param structure The molecular structure * @param atomic_orbital_type Whether to use spherical or cartesian atomic * orbitals * @return Shared pointer to the created BasisSet */ static std::shared_ptr from_basis_name( - const std::string& basis_name, const std::string& aux_basis_name, const Structure& structure, + const std::string& basis_name, const std::string& aux_basis_name, + const Structure& structure, AOType atomic_orbital_type = AOType::Spherical); /** * @brief Create a basis set from a basis name and auxiliary basis name * @param basis_name Name of the basis set (e.g., "6-31G", "cc-pVDZ") - * @param aux_basis_name Name of the auxiliary basis set (e.g., "cc-pVDZ-RIFIT") + * @param aux_basis_name Name of the auxiliary basis set (e.g., + * "cc-pVDZ-RIFIT") * @param structure Shared pointer to the molecular structure * @param atomic_orbital_type Whether to use spherical or cartesian atomic * orbitals * @return Shared pointer to the created BasisSet */ static std::shared_ptr from_basis_name( - std::string basis_name, const std::string& aux_basis_name, std::shared_ptr structure, + std::string basis_name, const std::string& aux_basis_name, + std::shared_ptr structure, AOType atomic_orbital_type = AOType::Spherical); /** @@ -706,7 +706,6 @@ class BasisSet : public DataClass, */ bool has_ecp_shells() const; - /** * @brief Get all auxiliary shells (flattened from per-atom storage) * @return Vector of all auxiliary shells @@ -1052,10 +1051,10 @@ class BasisSet : public DataClass, std::vector> _ecp_shells_per_atom; /// Auxiliary shells organized by atom index - each atom has a vector of - /// auxiliary shells + /// auxiliary shells std::vector> _aux_shells_per_atom; - /// Auxiliary basis set name + /// Auxiliary basis set name std::string _aux_name; /// Effective Core Potential (ECP) name (basis set name) diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index 2538c90af..8a12c3ac8 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -352,10 +352,11 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, } BasisSet::BasisSet(const std::string& name, const std::vector& shells, - const std::vector& ecp_shells, const std::vector& ecp_electrons, + const std::vector& ecp_shells, + const std::vector& ecp_electrons, const Structure& structure, AOType atomic_orbital_type) - : BasisSet(name, shells, ecp_shells, ecp_electrons, std::make_shared(structure), - atomic_orbital_type) { + : BasisSet(name, shells, ecp_shells, ecp_electrons, + std::make_shared(structure), atomic_orbital_type) { QDK_LOG_TRACE_ENTERING(); } @@ -482,8 +483,8 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); } - if ((!ecp_shells.empty() || !ecp_electrons.empty() || !ecp_name.empty()) && - ecp_electrons.size() != structure->get_num_atoms()) { + if ((!ecp_shells.empty() || !ecp_electrons.empty() || !ecp_name.empty()) && + ecp_electrons.size() != structure->get_num_atoms()) { throw std::invalid_argument( "ECP electrons vector size must match number of atoms"); } @@ -517,7 +518,6 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, } } - BasisSet::BasisSet(const std::string& name, const std::vector& shells, const std::string& aux_name, const std::vector& aux_shells, @@ -572,7 +572,6 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, } } - BasisSet::BasisSet(const std::string& name, const std::vector& shells, const std::string& ecp_name, const std::vector& ecp_shells, @@ -605,8 +604,8 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); } - if ((!ecp_shells.empty() || !ecp_electrons.empty() || !ecp_name.empty()) && - ecp_electrons.size() != structure->get_num_atoms()) { + if ((!ecp_shells.empty() || !ecp_electrons.empty() || !ecp_name.empty()) && + ecp_electrons.size() != structure->get_num_atoms()) { throw std::invalid_argument( "ECP electrons vector size must match number of atoms"); } @@ -910,18 +909,17 @@ std::shared_ptr BasisSet::from_basis_name( return std::make_shared(basis_name, all_basis_shells, basis_name, all_ecp_shells, all_ecp_electrons, - aux_name_lower, all_aux_shells, - structure, atomic_orbital_type); + aux_name_lower, all_aux_shells, structure, + atomic_orbital_type); } std::shared_ptr BasisSet::from_element_map( const std::map& element_to_basis_map, const std::map& element_to_aux_basis_map, const Structure& structure, AOType atomic_orbital_type) { - return BasisSet::from_element_map(element_to_basis_map, - element_to_aux_basis_map, - std::make_shared(structure), - atomic_orbital_type); + return BasisSet::from_element_map( + element_to_basis_map, element_to_aux_basis_map, + std::make_shared(structure), atomic_orbital_type); } std::shared_ptr BasisSet::from_element_map( @@ -1032,8 +1030,8 @@ std::shared_ptr BasisSet::from_index_map( return std::make_shared( std::string(BasisSet::custom_name), all_basis_shells, std::string(BasisSet::custom_ecp_name), all_ecp_shells, all_ecp_electrons, - std::string(BasisSet::custom_name), all_aux_shells, - structure, atomic_orbital_type); + std::string(BasisSet::custom_name), all_aux_shells, structure, + atomic_orbital_type); } BasisSet::BasisSet(const BasisSet& other) @@ -1225,8 +1223,7 @@ const Shell& BasisSet::get_aux_shell(size_t shell_index) const { size_t total_aux_shells = get_num_aux_shells(); if (shell_index >= total_aux_shells) { throw std::out_of_range("Auxiliary shell index " + - std::to_string(shell_index) + - " out of range [0, " + + std::to_string(shell_index) + " out of range [0, " + std::to_string(total_aux_shells) + ")"); } @@ -1591,7 +1588,8 @@ std::string BasisSet::get_summary() const { } oss << "\n"; } - oss << "Contains auxiliary basis: " << (has_aux_basis() ? "Yes" : "No") << "\n"; + oss << "Contains auxiliary basis: " << (has_aux_basis() ? "Yes" : "No") + << "\n"; if (has_aux_basis()) { oss << "Auxiliary basis name: " << _aux_name << "\n"; oss << "Number of auxiliary shells: " << get_num_aux_shells() << "\n"; @@ -2302,8 +2300,7 @@ std::shared_ptr BasisSet::from_hdf5(H5::Group& group) { if (aux_shell_group.nameExists("exponents") && aux_shell_group.nameExists("coefficients")) { - H5::DataSet aux_exponents = - aux_shell_group.openDataSet("exponents"); + H5::DataSet aux_exponents = aux_shell_group.openDataSet("exponents"); H5::DataSet aux_coefficients = aux_shell_group.openDataSet("coefficients"); @@ -2332,8 +2329,7 @@ std::shared_ptr BasisSet::from_hdf5(H5::Group& group) { for (unsigned j = 0; j < num_prims; ++j) { if (aux_prim_offset + j < aux_all_exponents.size()) { shell_exponents(j) = aux_all_exponents[aux_prim_offset + j]; - shell_coefficients(j) = - aux_all_coefficients[aux_prim_offset + j]; + shell_coefficients(j) = aux_all_coefficients[aux_prim_offset + j]; } } aux_prim_offset += num_prims; diff --git a/cpp/tests/test_basis_set.cpp b/cpp/tests/test_basis_set.cpp index 748af4341..51f5d1000 100644 --- a/cpp/tests/test_basis_set.cpp +++ b/cpp/tests/test_basis_set.cpp @@ -86,7 +86,8 @@ TEST_F(BasisSetTest, Constructors) { // // Constructor with name and structure should throw (empty basis invalid) // EXPECT_THROW(BasisSet basis2("6-31G", structure), std::invalid_argument); - // // Constructor with name, structure and basis type should throw (empty basis + // // Constructor with name, structure and basis type should throw (empty + // basis // // invalid) // EXPECT_THROW(BasisSet basis3("6-31G", structure, AOType::Cartesian), // std::invalid_argument); @@ -396,7 +397,8 @@ TEST_F(BasisSetTest, Validation) { Structure structure(coords, symbols); // // Empty basis is invalid - // EXPECT_THROW(BasisSet empty_basis("test", structure), std::invalid_argument); + // EXPECT_THROW(BasisSet empty_basis("test", structure), + // std::invalid_argument); // Add a shell std::vector shells; @@ -974,7 +976,6 @@ TEST_F(BasisSetTest, ECPShellValidation) { EXPECT_FALSE(regular_shell.has_radial_powers()); } - TEST_F(BasisSetTest, ECPHDF5Serialization) { // Test HDF5 serialization with ECP data std::vector coords = {{0.0, 0.0, 0.0}, {1.0, 0.0, 0.0}}; @@ -1894,8 +1895,7 @@ TEST_F(BasisSetTest, AuxiliaryBasisFromShellsConstructor) { aux_shells.emplace_back( Shell(2, OrbitalType::S, std::vector{3.0}, std::vector{1.5})); - BasisSet basis("test-with-aux-shells", shells, aux_shells, - *structure); + BasisSet basis("test-with-aux-shells", shells, aux_shells, *structure); EXPECT_EQ("test-with-aux-shells", basis.get_name()); EXPECT_EQ(3u, basis.get_num_shells()); @@ -2022,8 +2022,7 @@ TEST_F(BasisSetTest, AuxiliaryBasisCopyConstructorAndAssignment) { assigned = original; EXPECT_TRUE(assigned.has_aux_basis()); - EXPECT_EQ(original.get_num_aux_shells(), - assigned.get_num_aux_shells()); + EXPECT_EQ(original.get_num_aux_shells(), assigned.get_num_aux_shells()); } TEST_F(BasisSetTest, AuxiliaryBasisJSONSerialization) { @@ -2151,7 +2150,6 @@ TEST_F(BasisSetTest, FromBasisNameWithAuxSCFComparison) { EXPECT_TRUE(basis_with_aux->has_aux_basis()); } - TEST_F(BasisSetTest, AuxiliaryShellDataAccessors) { // Construct a 2-atom structure with known aux shells and verify every // accessor that returns shell data. @@ -2222,7 +2220,6 @@ TEST_F(BasisSetTest, AuxiliaryShellDataAccessors) { EXPECT_EQ(1u, s2.atom_index); } - TEST_F(BasisSetTest, AuxShellDataIntegrityJSONRoundTrip) { std::vector coords = {{0.0, 0.0, 0.0}, {1.4, 0.0, 0.0}}; std::vector symbols = {"H", "H"}; @@ -2410,7 +2407,8 @@ TEST_F(BasisSetTest, SharedPtrStructureConstructors) { EXPECT_TRUE(b2.has_ecp_shells()); EXPECT_TRUE(b2.has_structure()); - // Constructor: (name, shells, ecp_name, ecp_shells, ecp_electrons, shared_ptr) + // Constructor: (name, shells, ecp_name, ecp_shells, ecp_electrons, + // shared_ptr) BasisSet b3("sp3", shells, "ecp", ecp_shells, ecp_electrons, structure); EXPECT_EQ("ecp", b3.get_ecp_name()); EXPECT_TRUE(b3.has_structure()); @@ -2437,7 +2435,6 @@ TEST_F(BasisSetTest, SharedPtrStructureConstructors) { EXPECT_TRUE(b6.has_structure()); } - TEST_F(BasisSetTest, MoveConstructorAndAssignment) { std::vector coords = {{0.0, 0.0, 0.0}, {1.4, 0.0, 0.0}}; std::vector symbols = {"H", "H"}; @@ -2473,7 +2470,6 @@ TEST_F(BasisSetTest, MoveConstructorAndAssignment) { EXPECT_EQ("aux2", target.get_aux_name()); } - TEST_F(BasisSetTest, AuxiliaryShellErrorPaths) { std::vector coords = {{0.0, 0.0, 0.0}}; std::vector symbols = {"H"}; @@ -2497,7 +2493,6 @@ TEST_F(BasisSetTest, AuxiliaryShellErrorPaths) { EXPECT_TRUE(no_aux.get_aux_shells().empty()); } - TEST_F(BasisSetTest, AuxiliaryBasisCartesianAOType) { std::vector coords = {{0.0, 0.0, 0.0}}; std::vector symbols = {"H"}; diff --git a/python/src/pybind11/data/basis_set.cpp b/python/src/pybind11/data/basis_set.cpp index ee7d91e30..c7bb090c9 100644 --- a/python/src/pybind11/data/basis_set.cpp +++ b/python/src/pybind11/data/basis_set.cpp @@ -266,8 +266,8 @@ A shell represents a group of atomic orbitals with the same atom, angular moment // pulling recognised kwargs into the positional vector when the // corresponding slot is not yet filled. // Expected positional order: name, shells, ... - if (a.size() >= 1 && py::isinstance(a[0]) && - a.size() < 2 && kwargs.contains("shells")) { + if (a.size() >= 1 && py::isinstance(a[0]) && a.size() < 2 && + kwargs.contains("shells")) { a.push_back(kwargs["shells"].cast()); } @@ -293,8 +293,7 @@ A shell represents a group of atomic orbitals with the same atom, angular moment auto shells = to_shell_vec(a[1].cast()); // (name, shells) - if (n == 2) - return BasisSet(name, shells, ao); + if (n == 2) return BasisSet(name, shells, ao); // (name, shells, structure) if (n == 3 && py::isinstance(a[2])) @@ -312,8 +311,7 @@ A shell represents a group of atomic orbitals with the same atom, angular moment if (list_has_ecp_shells(extra)) { // ECP shells without explicit ecp_electrons → supply empty vec return BasisSet(name, shells, to_shell_vec(extra), - std::vector(), - a[3].cast(), ao); + std::vector(), a[3].cast(), ao); } // Auxiliary shells return BasisSet(name, shells, to_shell_vec(extra), @@ -328,8 +326,7 @@ A shell represents a group of atomic orbitals with the same atom, angular moment a[4].cast(), ao); } // (name, shells, ecp_shells, ecp_electrons, structure) - return BasisSet(name, shells, - to_shell_vec(a[2].cast()), + return BasisSet(name, shells, to_shell_vec(a[2].cast()), a[3].cast>(), a[4].cast(), ao); } @@ -344,12 +341,11 @@ A shell represents a group of atomic orbitals with the same atom, angular moment // (name, shells, ecp_name, ecp_shells, ecp_electrons, // aux_name, aux_shells, structure) if (n == 8 && py::isinstance(a[2])) - return BasisSet(name, shells, a[2].cast(), - to_shell_vec(a[3].cast()), - a[4].cast>(), - a[5].cast(), - to_shell_vec(a[6].cast()), - a[7].cast(), ao); + return BasisSet( + name, shells, a[2].cast(), + to_shell_vec(a[3].cast()), + a[4].cast>(), a[5].cast(), + to_shell_vec(a[6].cast()), a[7].cast(), ao); throw py::type_error( "No matching BasisSet constructor for the given arguments"); @@ -1151,8 +1147,7 @@ Loads a standard basis set (e.g., "sto-3g", "cc-pvdz") for all atoms in the stru basis_set.def_static( "from_basis_name", py::overload_cast( - &BasisSet::from_basis_name), + const Structure&, AOType>(&BasisSet::from_basis_name), R"( Create a basis set by name with an auxiliary basis for a molecular structure. @@ -1172,8 +1167,7 @@ Loads a standard basis set and auxiliary basis set for all atoms in the structur >>> basis = BasisSet.from_basis_name("def2-svp", "def2-universal-jfit", structure) >>> print(f"Aux shells: {basis.get_num_aux_shells()}") )", - py::arg("basis_name"), py::arg("aux_basis_name"), - py::arg("structure"), + py::arg("basis_name"), py::arg("aux_basis_name"), py::arg("structure"), py::arg("atomic_orbital_type") = AOType::Spherical); basis_set.def_static( "from_element_map", @@ -1230,8 +1224,7 @@ Create a basis set with different basis sets and auxiliary basis sets per elemen >>> basis = BasisSet.from_element_map(basis_map, aux_map, structure) )", py::arg("element_to_basis_map"), py::arg("element_to_aux_basis_map"), - py::arg("structure"), - py::arg("atomic_orbital_type") = AOType::Spherical); + py::arg("structure"), py::arg("atomic_orbital_type") = AOType::Spherical); basis_set.def_static( "from_index_map", py::overload_cast&, const Structure&, @@ -1266,8 +1259,8 @@ Allows specifying different basis sets for individual atoms by their index. basis_set.def_static( "from_index_map", py::overload_cast&, - const std::map&, - const Structure&, AOType>(&BasisSet::from_index_map), + const std::map&, const Structure&, + AOType>(&BasisSet::from_index_map), R"( Create a basis set with different basis sets and auxiliary basis sets per atom index. @@ -1287,8 +1280,7 @@ Create a basis set with different basis sets and auxiliary basis sets per atom i >>> basis = BasisSet.from_index_map(basis_map, aux_map, structure) )", py::arg("index_to_basis_map"), py::arg("index_to_aux_basis_map"), - py::arg("structure"), - py::arg("atomic_orbital_type") = AOType::Spherical); + py::arg("structure"), py::arg("atomic_orbital_type") = AOType::Spherical); // Utility functions (static methods); basis_set.def_static("orbital_type_to_string", diff --git a/python/tests/test_basis_set.py b/python/tests/test_basis_set.py index 58625f7cf..2800f061b 100644 --- a/python/tests/test_basis_set.py +++ b/python/tests/test_basis_set.py @@ -331,7 +331,6 @@ def test_summary(): assert "Auxiliary" in summary_with_aux - def test_json_serialization(): """Test JSON serialization and deserialization.""" # Create a basis set with data @@ -343,10 +342,7 @@ def test_json_serialization(): Shell(0, OrbitalType.S, [1.0], [1.0]), Shell(0, OrbitalType.P, [0.5], [1.0]), ] - aux_shells = [ - Shell(0, OrbitalType.S, [2.0], [1.0]), - Shell(0, OrbitalType.D, [0.3], [0.5]) - ] + aux_shells = [Shell(0, OrbitalType.S, [2.0], [1.0]), Shell(0, OrbitalType.D, [0.3], [0.5])] basis_out = BasisSet("STO-3G", shells, "aux-fit", aux_shells, structure) # Test direct JSON conversion @@ -383,7 +379,6 @@ def test_json_serialization(): Path(filename).unlink() - def test_hdf5_serialization(): """Test HDF5 serialization and deserialization.""" # Create a basis set with data @@ -395,9 +390,7 @@ def test_hdf5_serialization(): Shell(0, OrbitalType.S, [1.0], [1.0]), Shell(0, OrbitalType.P, [0.5], [1.0]), ] - aux_shells = [ - Shell(0, OrbitalType.S, [2.0], [1.0]), - Shell(0, OrbitalType.P, [0.8], [0.7])] + aux_shells = [Shell(0, OrbitalType.S, [2.0], [1.0]), Shell(0, OrbitalType.P, [0.8], [0.7])] basis_out = BasisSet("cc-pVDZ", shells, "aux-fit", aux_shells, structure, AOType.Spherical) try: @@ -415,7 +408,6 @@ def test_hdf5_serialization(): assert basis_in.get_aux_name() == "aux-fit" assert basis_in.get_num_aux_shells() == 2 - except RuntimeError as e: pytest.skip(f"HDF5 test skipped - {e!s}") finally: @@ -424,12 +416,6 @@ def test_hdf5_serialization(): Path(filename).unlink() - -def test_error_handling(): - """Test error handling for invalid operations.""" - # Create a minimal shell for testing empty functions - shell = Shell(0, OrbitalType.S, [1.0], [1.0]) - def test_error_handling(): """Test error handling for invalid operations.""" # Create a minimal shell for testing empty functions @@ -1343,6 +1329,7 @@ def test_auxiliary_basis_set_serialization(): assert loaded_basis.has_aux_basis() assert loaded_basis.get_aux_name() == "aux_basis" + def test_basis_set_ecp_shells_copy(): """Test that ECP shells are properly copied.""" # Create structure and shells @@ -1556,7 +1543,6 @@ def test_basis_set_data_type_name(): assert BasisSet._data_type_name == "basis_set" - def test_auxiliary_basis_set_accessors(): """Test has/get auxiliary basis set methods.""" positions = np.array([[0.0, 0.0, 0.0]]) From 0b9b3c2ec889c69bfe5fa08505dc0094a596f67d Mon Sep 17 00:00:00 2001 From: rainli323 Date: Wed, 15 Apr 2026 22:36:30 +0000 Subject: [PATCH 06/33] fix documentation error --- python/src/pybind11/data/basis_set.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/src/pybind11/data/basis_set.cpp b/python/src/pybind11/data/basis_set.cpp index c7bb090c9..1b7d4dfae 100644 --- a/python/src/pybind11/data/basis_set.cpp +++ b/python/src/pybind11/data/basis_set.cpp @@ -353,7 +353,8 @@ A shell represents a group of atomic orbitals with the same atom, angular moment R"( BasisSet constructor. -Supported signatures (atomic_orbital_type is always optional, default Spherical): +Supported signatures (atomic_orbital_type is always optional, default Spherical):: + BasisSet(other: BasisSet) BasisSet(name, shells) BasisSet(name, shells, structure) From 176bbd3e4a7dffe7e5c5ec80c8fc86f058aeb3b9 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Mon, 20 Apr 2026 17:52:05 +0000 Subject: [PATCH 07/33] aux_basis default name --- cpp/include/qdk/chemistry/data/basis_set.hpp | 9 +++++++++ cpp/src/qdk/chemistry/data/basis_set.cpp | 14 +++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/cpp/include/qdk/chemistry/data/basis_set.hpp b/cpp/include/qdk/chemistry/data/basis_set.hpp index 754d245c5..39205b942 100644 --- a/cpp/include/qdk/chemistry/data/basis_set.hpp +++ b/cpp/include/qdk/chemistry/data/basis_set.hpp @@ -439,6 +439,9 @@ class BasisSet : public DataClass, /** @brief Name for custom ecps */ static constexpr std::string_view custom_ecp_name = "custom_ecp"; + /** @brief Name for custom auxiliary basis sets */ + static constexpr std::string_view custom_aux_name = "custom_aux"; + /** * @brief Get the data type name for this class * @return "basis_set" @@ -755,6 +758,12 @@ class BasisSet : public DataClass, */ size_t get_num_atomic_orbitals() const; + /** + * @brief Get number of auxiliary orbitals (total from all shells) + * @return Total number of auxiliary orbitals + */ + size_t get_num_auxiliary_orbitals() const; + /** * @brief Get the atom index for a atomic orbital * @param contracted_atomic_orbital_index Index of the atomic orbital diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index 8a12c3ac8..a245ab82b 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -1030,7 +1030,7 @@ std::shared_ptr BasisSet::from_index_map( return std::make_shared( std::string(BasisSet::custom_name), all_basis_shells, std::string(BasisSet::custom_ecp_name), all_ecp_shells, all_ecp_electrons, - std::string(BasisSet::custom_name), all_aux_shells, structure, + std::string(BasisSet::custom_aux_name), all_aux_shells, structure, atomic_orbital_type); } @@ -1620,6 +1620,18 @@ std::string BasisSet::get_summary() const { return oss.str(); } +size_t BasisSet::get_num_auxiliary_orbitals() const { + QDK_LOG_TRACE_ENTERING(); + + size_t num_aux_orbitals = 0; + for (const auto& sh : _aux_shells_per_atom) { + for (const auto& shell : sh) { + num_aux_orbitals += shell.get_num_atomic_orbitals(_atomic_orbital_type); + } + } + return num_aux_orbitals; +} + void BasisSet::to_file(const std::string& filename, const std::string& type) const { QDK_LOG_TRACE_ENTERING(); From fde8d55e2d3b2633261d3b1722d4764c823b6895 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Tue, 28 Apr 2026 22:22:19 +0000 Subject: [PATCH 08/33] AI suggestions --- cpp/include/qdk/chemistry/data/basis_set.hpp | 21 -------- cpp/src/qdk/chemistry/data/basis_set.cpp | 20 ++++++-- cpp/tests/test_basis_set.cpp | 50 ++++++++++++-------- 3 files changed, 48 insertions(+), 43 deletions(-) diff --git a/cpp/include/qdk/chemistry/data/basis_set.hpp b/cpp/include/qdk/chemistry/data/basis_set.hpp index 39205b942..d6a6b1d09 100644 --- a/cpp/include/qdk/chemistry/data/basis_set.hpp +++ b/cpp/include/qdk/chemistry/data/basis_set.hpp @@ -187,16 +187,6 @@ struct Shell { class BasisSet : public DataClass, public std::enable_shared_from_this { public: - // /** - // * @brief Constructor with basis set name and structure - // * @param name Name of the basis set (e.g., "6-31G", "cc-pVDZ") - // * @param structure The molecular structure - // * @param atomic_orbital_type Whether to use spherical or cartesian - // atomic - // * orbitals - // */ - // BasisSet(const std::string& name, const Structure& structure, - // AOType atomic_orbital_type = AOType::Spherical); /** * @brief Constructor with shells @@ -220,17 +210,6 @@ class BasisSet : public DataClass, const Structure& structure, AOType atomic_orbital_type = AOType::Spherical); - // /** - // * @brief Constructor with basis set name and structure shared pointer - // * @param name Name of the basis set (e.g., "6-31G", "cc-pVDZ") - // * @param structure Shared pointer to the molecular structure - // * @param atomic_orbital_type Whether to use spherical or cartesian - // atomic - // * orbitals - // */ - // BasisSet(const std::string& name, std::shared_ptr structure, - // AOType atomic_orbital_type = AOType::Spherical); - /** * @brief Constructor with shells and structure shared pointer * @param name Name of the basis set diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index a245ab82b..4b3aec641 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -327,6 +327,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, _structure(structure), _ecp_name("none") { QDK_LOG_TRACE_ENTERING(); + if (_name.empty()) { + throw std::invalid_argument("BasisSet name cannot be empty"); + } + if (!structure) { throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); } @@ -535,7 +539,9 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, : _name(name), _atomic_orbital_type(atomic_orbital_type), _structure(structure), - _aux_name(aux_name) { + _aux_name(aux_name), + _ecp_name("none"), + _ecp_electrons(std::vector(structure->get_num_atoms(), 0)) { QDK_LOG_TRACE_ENTERING(); if (!structure) { throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); @@ -604,7 +610,7 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); } - if ((!ecp_shells.empty() || !ecp_electrons.empty() || !ecp_name.empty()) && + if ((!ecp_shells.empty() || !ecp_electrons.empty() || ecp_name != "none") && ecp_electrons.size() != structure->get_num_atoms()) { throw std::invalid_argument( "ECP electrons vector size must match number of atoms"); @@ -1538,6 +1544,15 @@ bool BasisSet::_is_consistent_with_structure() const { } } + // Check if any aux shell references an atom beyond the structure's atom count + for (size_t atom_idx = 0; atom_idx < _aux_shells_per_atom.size(); + ++atom_idx) { + if (!_aux_shells_per_atom[atom_idx].empty() && + atom_idx >= _structure->get_num_atoms()) { + return false; + } + } + return true; } @@ -2325,7 +2340,6 @@ std::shared_ptr BasisSet::from_hdf5(H5::Group& group) { aux_exponents.read(aux_all_exponents.data(), H5::PredType::NATIVE_DOUBLE); - aux_all_coefficients.resize(aux_exp_dims[0]); aux_coefficients.read(aux_all_coefficients.data(), H5::PredType::NATIVE_DOUBLE); } diff --git a/cpp/tests/test_basis_set.cpp b/cpp/tests/test_basis_set.cpp index 51f5d1000..142e484e9 100644 --- a/cpp/tests/test_basis_set.cpp +++ b/cpp/tests/test_basis_set.cpp @@ -80,17 +80,12 @@ TEST_F(BasisSetTest, Constructors) { std::vector symbols = {"H"}; Structure structure(coords, symbols); - // // Constructor with empty name and structure - // EXPECT_THROW(BasisSet basis1("", structure), std::invalid_argument); - - // // Constructor with name and structure should throw (empty basis invalid) - // EXPECT_THROW(BasisSet basis2("6-31G", structure), std::invalid_argument); - - // // Constructor with name, structure and basis type should throw (empty - // basis - // // invalid) - // EXPECT_THROW(BasisSet basis3("6-31G", structure, AOType::Cartesian), - // std::invalid_argument); + // Constructor with empty name should throw + std::vector empty_name_shells; + empty_name_shells.emplace_back( + Shell(0, OrbitalType::S, std::vector{1.0}, std::vector{2.0})); + EXPECT_THROW(BasisSet basis1("", empty_name_shells, structure), + std::invalid_argument); // Constructor with shells should work std::vector shells; @@ -233,10 +228,6 @@ TEST_F(BasisSetTest, AOTypeManagement) { std::vector symbols = {"H"}; Structure structure(coords, symbols); - // // Test default basis type (spherical) - empty basis sets are invalid - // EXPECT_THROW(BasisSet basis_spherical("test", structure), - // std::invalid_argument); - // Create cartesian basis set std::vector shells; shells.emplace_back( @@ -396,10 +387,6 @@ TEST_F(BasisSetTest, Validation) { std::vector symbols = {"H"}; Structure structure(coords, symbols); - // // Empty basis is invalid - // EXPECT_THROW(BasisSet empty_basis("test", structure), - // std::invalid_argument); - // Add a shell std::vector shells; shells.emplace_back( @@ -1871,6 +1858,7 @@ TEST_F(BasisSetTest, AuxiliaryBasisFromSharedPtrConstructor) { EXPECT_EQ(3u, basis.get_num_shells()); EXPECT_TRUE(basis.has_aux_basis()); EXPECT_EQ(4u, basis.get_num_aux_shells()); + EXPECT_EQ(6u, basis.get_num_auxiliary_orbitals()); } TEST_F(BasisSetTest, AuxiliaryBasisFromShellsConstructor) { @@ -2493,6 +2481,30 @@ TEST_F(BasisSetTest, AuxiliaryShellErrorPaths) { EXPECT_TRUE(no_aux.get_aux_shells().empty()); } +TEST_F(BasisSetTest, AuxiliaryBasisMismatchedAtomIndices) { + // Structure with only 1 atom (index 0 is valid, index 1+ is invalid) + std::vector coords = {{0.0, 0.0, 0.0}}; + std::vector symbols = {"H"}; + Structure structure(coords, symbols); + + // Primary shell on atom 0 (valid) + std::vector shells; + shells.emplace_back(0, OrbitalType::S, std::vector{1.0}, std::vector{1.0}); + + // Aux shell referencing atom index 5, which does not exist in the structure + std::vector aux_shells; + aux_shells.emplace_back(5, OrbitalType::S, std::vector{2.0}, + std::vector{1.0}); + + // Construction should throw because aux shell references non-existent atom + EXPECT_THROW(BasisSet("test", shells, "aux-bad", aux_shells, structure), + std::invalid_argument); + + // Also test with unnamed aux constructor + EXPECT_THROW(BasisSet("test", shells, aux_shells, structure), + std::invalid_argument); +} + TEST_F(BasisSetTest, AuxiliaryBasisCartesianAOType) { std::vector coords = {{0.0, 0.0, 0.0}}; std::vector symbols = {"H"}; From 227fcaaf1ccc3c0e9a603f08c722df578eb9fe4e Mon Sep 17 00:00:00 2001 From: rainli323 Date: Wed, 29 Apr 2026 03:38:14 +0000 Subject: [PATCH 09/33] pre-commit --- cpp/include/qdk/chemistry/data/basis_set.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/include/qdk/chemistry/data/basis_set.hpp b/cpp/include/qdk/chemistry/data/basis_set.hpp index d6a6b1d09..2bb88a5db 100644 --- a/cpp/include/qdk/chemistry/data/basis_set.hpp +++ b/cpp/include/qdk/chemistry/data/basis_set.hpp @@ -187,7 +187,6 @@ struct Shell { class BasisSet : public DataClass, public std::enable_shared_from_this { public: - /** * @brief Constructor with shells * @param name Name of the basis set From 5938118c905e093658d3a42fd371c91c0998013d Mon Sep 17 00:00:00 2001 From: rainli323 Date: Wed, 29 Apr 2026 21:29:31 +0000 Subject: [PATCH 10/33] merge eith DF_SCF for basis set related AI comments --- cpp/src/qdk/chemistry/data/basis_set.cpp | 3 +- python/src/pybind11/data/basis_set.cpp | 85 +++++++--- python/tests/test_basis_set.py | 192 +++++++++++++++++++++++ 3 files changed, 253 insertions(+), 27 deletions(-) diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index 4b3aec641..523434378 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -540,8 +540,7 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, _atomic_orbital_type(atomic_orbital_type), _structure(structure), _aux_name(aux_name), - _ecp_name("none"), - _ecp_electrons(std::vector(structure->get_num_atoms(), 0)) { + _ecp_name("none") { QDK_LOG_TRACE_ENTERING(); if (!structure) { throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); diff --git a/python/src/pybind11/data/basis_set.cpp b/python/src/pybind11/data/basis_set.cpp index 1b7d4dfae..05eb8c712 100644 --- a/python/src/pybind11/data/basis_set.cpp +++ b/python/src/pybind11/data/basis_set.cpp @@ -262,27 +262,64 @@ A shell represents a group of atomic orbitals with the same atom, angular moment ao = kwargs["atomic_orbital_type"].cast(); // --- Merge keyword args into positional vector -------------------- - // Supports mixed calls like BasisSet("name", shells=shells) by - // pulling recognised kwargs into the positional vector when the - // corresponding slot is not yet filled. - // Expected positional order: name, shells, ... - if (a.size() >= 1 && py::isinstance(a[0]) && a.size() < 2 && - kwargs.contains("shells")) { - a.push_back(kwargs["shells"].cast()); - } - - const size_t n = a.size(); - + auto throw_multiple_values = [](const char* arg_name) { + throw py::type_error(std::string("BasisSet() got multiple values for " + "argument '") + + arg_name + "'"); + }; + auto reject_unexpected_kwargs = + [&kwargs](std::initializer_list allowed) { + for (auto item : kwargs) { + auto key = py::cast(item.first); + bool recognised = false; + for (auto allowed_key : allowed) { + if (key == allowed_key) { + recognised = true; + break; + } + } + if (!recognised) { + throw py::type_error( + "BasisSet() got an unexpected keyword " + "argument '" + + key + "'"); + } + } + }; + const size_t initial_n = a.size(); // Copy constructor: BasisSet(other) - if (n == 1 && py::isinstance(a[0])) + if (initial_n == 1 && py::isinstance(a[0])) { + if (!kwargs.empty()) { + throw py::type_error( + "BasisSet() copy constructor does not accept keyword " + "arguments"); + } return BasisSet(a[0].cast()); - - // --- Fully-keyword calls ------------------------------------------ - if (n == 0 && kwargs.contains("name") && kwargs.contains("shells")) { - auto name = kwargs["name"].cast(); - auto shells = to_shell_vec(kwargs["shells"].cast()); - return BasisSet(name, shells, ao); } + // Supports mixed and fully-keyword calls by pulling recognised kwargs + // into the positional vector when the corresponding slot is not yet + // filled. Expected positional order: name, shells, structure, ... + reject_unexpected_kwargs( + {"name", "shells", "structure", "atomic_orbital_type"}); + if (kwargs.contains("name")) { + if (a.size() > 0) throw_multiple_values("name"); + a.push_back(kwargs["name"].cast()); + } + if (kwargs.contains("shells")) { + if (a.size() > 1) throw_multiple_values("shells"); + if (a.size() == 1) { + a.push_back(kwargs["shells"].cast()); + } + } + if (kwargs.contains("structure")) { + if (a.size() > 2) throw_multiple_values("structure"); + if (a.size() < 2) + throw py::type_error( + "BasisSet() 'structure' keyword requires 'name' and " + "'shells' to be provided"); + a.push_back(kwargs["structure"].cast()); + } + const size_t n = a.size(); if (n < 2 || !py::isinstance(a[0]) || !py::isinstance(a[1])) @@ -302,18 +339,16 @@ A shell represents a group of atomic orbitals with the same atom, angular moment // (name, shells, , structure) -- n==4 // Disambiguate: if shells in a[2] have rpowers → ECP path (needs // ecp_electrons); if not → auxiliary path. - // C++ has no (name, shells, ecp_shells, structure) constructor, so - // ECP shells at n==4 use an empty ecp_electrons vector via the - // 5-arg ECP constructor. if (n == 4 && py::isinstance(a[2]) && py::isinstance(a[3])) { auto extra = a[2].cast(); if (list_has_ecp_shells(extra)) { - // ECP shells without explicit ecp_electrons → supply empty vec - return BasisSet(name, shells, to_shell_vec(extra), - std::vector(), a[3].cast(), ao); + throw py::type_error( + "BasisSet() with ECP shells requires explicit " + "ecp_electrons: use (name, shells, ecp_shells, " + "ecp_electrons, structure)"); } - // Auxiliary shells + // Auxiliary shells path return BasisSet(name, shells, to_shell_vec(extra), a[3].cast(), ao); } diff --git a/python/tests/test_basis_set.py b/python/tests/test_basis_set.py index 2800f061b..dec59ddf4 100644 --- a/python/tests/test_basis_set.py +++ b/python/tests/test_basis_set.py @@ -1728,3 +1728,195 @@ def test_auxiliary_basis_set_from_basis_name_database(): assert loaded.get_aux_name() == "def2-universal-jfit" assert loaded.get_num_shells() == basis.get_num_shells() assert loaded.get_num_aux_shells() == basis.get_num_aux_shells() + + +# --------------------------------------------------------------------------- +# Tests for __init__ dispatcher: positional, keyword, mixed, and error paths +# --------------------------------------------------------------------------- + + +class TestBasisSetConstructorDispatch: + """Tests for the BasisSet __init__ dispatcher. + + Covers all constructor signatures with positional, keyword, and mixed + calling conventions. + """ + + @pytest.fixture + def shell(self): + return Shell(0, OrbitalType.S, [1.0], [1.0]) + + @pytest.fixture + def shells(self, shell): + return [shell] + + @pytest.fixture + def structure(self): + positions = np.array([[0.0, 0.0, 0.0]]) + return Structure(["H"], positions) + + @pytest.fixture + def aux_shells(self): + return [Shell(0, OrbitalType.S, [2.0], [1.0])] + + @pytest.fixture + def ecp_shells(self): + return [Shell(0, OrbitalType.S, [5.0], [10.0], [0])] + + # --- Copy constructor: BasisSet(other) --- + + def test_copy_positional(self, shells): + original = BasisSet("orig", shells) + copy = BasisSet(original) + assert copy.get_name() == "orig" + assert copy.get_num_shells() == 1 + + def test_copy_rejects_kwargs(self, shells): + original = BasisSet("orig", shells) + with pytest.raises(TypeError, match="copy constructor does not accept keyword"): + BasisSet(original, atomic_orbital_type=AOType.Cartesian) + + # --- (name, shells) --- + + def test_name_shells_positional(self, shells): + b = BasisSet("test", shells) + assert b.get_name() == "test" + assert b.get_num_shells() == 1 + assert b.get_atomic_orbital_type() == AOType.Spherical + + def test_name_shells_all_kwargs(self, shells): + b = BasisSet(name="test", shells=shells) + assert b.get_name() == "test" + assert b.get_num_shells() == 1 + + def test_name_shells_mixed(self, shells): + b = BasisSet("test", shells=shells) + assert b.get_name() == "test" + assert b.get_num_shells() == 1 + + def test_name_shells_with_ao_kwarg(self, shells): + b = BasisSet("test", shells, atomic_orbital_type=AOType.Cartesian) + assert b.get_atomic_orbital_type() == AOType.Cartesian + + def test_name_shells_with_ao_positional(self, shells): + b = BasisSet("test", shells, AOType.Cartesian) + assert b.get_atomic_orbital_type() == AOType.Cartesian + + def test_name_shells_fully_kwarg_with_ao(self, shells): + b = BasisSet(name="test", shells=shells, atomic_orbital_type=AOType.Cartesian) + assert b.get_name() == "test" + assert b.get_atomic_orbital_type() == AOType.Cartesian + + # --- (name, shells, structure) --- + + def test_name_shells_structure_positional(self, shells, structure): + b = BasisSet("test", shells, structure) + assert b.has_structure() + assert b.get_structure().get_num_atoms() == 1 + + def test_name_shells_structure_kwarg(self, shells, structure): + b = BasisSet("test", shells, structure=structure) + assert b.has_structure() + + def test_name_shells_structure_all_kwargs(self, shells, structure): + b = BasisSet(name="test", shells=shells, structure=structure) + assert b.has_structure() + + def test_name_shells_structure_with_ao(self, shells, structure): + b = BasisSet("test", shells, structure, AOType.Cartesian) + assert b.has_structure() + assert b.get_atomic_orbital_type() == AOType.Cartesian + + def test_name_shells_structure_ao_kwarg(self, shells, structure): + b = BasisSet("test", shells, structure=structure, atomic_orbital_type=AOType.Cartesian) + assert b.has_structure() + assert b.get_atomic_orbital_type() == AOType.Cartesian + + # --- (name, shells, aux_shells, structure) --- n==4 path + + def test_name_shells_aux_structure_positional(self, shells, aux_shells, structure): + b = BasisSet("test", shells, aux_shells, structure) + assert b.has_aux_basis() + assert b.get_num_aux_shells() == 1 + assert b.has_structure() + + def test_name_shells_aux_structure_with_ao(self, shells, aux_shells, structure): + b = BasisSet("test", shells, aux_shells, structure, AOType.Cartesian) + assert b.has_aux_basis() + assert b.get_atomic_orbital_type() == AOType.Cartesian + + # --- (name, shells, aux_name, aux_shells, structure) --- n==5 str path + + def test_name_shells_auxname_aux_structure_positional(self, shells, aux_shells, structure): + b = BasisSet("test", shells, "my-aux", aux_shells, structure) + assert b.has_aux_basis() + assert b.get_aux_name() == "my-aux" + assert b.has_structure() + + # --- (name, shells, ecp_shells, ecp_electrons, structure) --- n==5 list path + + def test_name_shells_ecp_ecpelec_structure_positional(self, shells, ecp_shells, structure): + b = BasisSet("test", shells, ecp_shells, [2], structure) + assert b.has_ecp_shells() + assert b.get_num_ecp_shells() == 1 + assert list(b.get_ecp_electrons()) == [2] + + # --- (name, shells, ecp_name, ecp_shells, ecp_electrons, structure) --- n==6 list path + + def test_name_shells_ecpname_ecp_ecpelec_structure(self, shells, ecp_shells, structure): + b = BasisSet("test", shells, "my-ecp", ecp_shells, [2], structure) + assert b.has_ecp_shells() + assert b.get_ecp_name() == "my-ecp" + assert list(b.get_ecp_electrons()) == [2] + + # --- (name, shells, ecp_name, ecp_shells, ecp_electrons, aux_name, aux_shells, structure) --- n==8 path + + def test_full_8arg_constructor(self, shells, ecp_shells, aux_shells, structure): + b = BasisSet("test", shells, "my-ecp", ecp_shells, [2], "my-aux", aux_shells, structure) + assert b.has_ecp_shells() + assert b.get_ecp_name() == "my-ecp" + assert b.has_aux_basis() + assert b.get_aux_name() == "my-aux" + assert b.has_structure() + + # --- Error cases: unexpected kwargs --- + + def test_rejects_unexpected_kwarg(self, shells): + with pytest.raises(TypeError, match="unexpected keyword argument 'bogus'"): + BasisSet("test", shells, bogus=42) + + def test_rejects_unexpected_kwarg_typo(self, shells): + with pytest.raises(TypeError, match="unexpected keyword argument 'struture'"): + BasisSet("test", shells, struture="oops") + + # --- Error cases: multiple values --- + + def test_rejects_name_multiple_values(self, shells): + with pytest.raises(TypeError, match="multiple values for argument 'name'"): + BasisSet("test", name="other", shells=shells) + + def test_rejects_shells_multiple_values(self, shells): + with pytest.raises(TypeError, match="multiple values for argument 'shells'"): + BasisSet("test", shells, shells=shells) + + # --- Error cases: structure without shells --- + + def test_rejects_structure_without_shells(self, structure): + with pytest.raises(TypeError, match="'structure' keyword requires 'name' and 'shells'"): + BasisSet(name="test", structure=structure) + + # --- Error cases: no matching constructor --- + + def test_rejects_wrong_types(self): + with pytest.raises(TypeError): + BasisSet(123, 456) + + def test_rejects_no_args(self): + with pytest.raises(TypeError): + BasisSet() + + # --- Error cases: ECP shells at n==4 should raise --- + + def test_ecp_at_n4_raises(self, shells, ecp_shells, structure): + with pytest.raises(TypeError, match="ECP shells requires explicit ecp_electrons"): + BasisSet("test", shells, ecp_shells, structure) From 93f1cf7cef3e7238ad5259b0a989efcfad7f09c0 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Wed, 29 Apr 2026 21:34:46 +0000 Subject: [PATCH 11/33] more from DF_SCF branch, clean up pytest --- .../qdk_chemistry/plugins/pyscf/conversion.py | 6 +- python/tests/test_basis_set.py | 120 ------------------ python/tests/test_pyscf_plugin.py | 2 +- 3 files changed, 4 insertions(+), 124 deletions(-) diff --git a/python/src/qdk_chemistry/plugins/pyscf/conversion.py b/python/src/qdk_chemistry/plugins/pyscf/conversion.py index da78ece81..5ee2bbb58 100644 --- a/python/src/qdk_chemistry/plugins/pyscf/conversion.py +++ b/python/src/qdk_chemistry/plugins/pyscf/conversion.py @@ -287,6 +287,9 @@ def pyscf_mol_to_qdk_basis( # Extract ECP shells if present ecp_shells = [] + ecp_name = "none" + ecp_electrons = [0] * pyscf_mol.natm + if hasattr(pyscf_mol, "_ecp") and pyscf_mol._ecp: # noqa: SLF001 for iatm in range(pyscf_mol.natm): atom_symbol = atom_symbols[iatm] @@ -316,9 +319,6 @@ def pyscf_mol_to_qdk_basis( # Extract ECP name and electron counts if present if hasattr(pyscf_mol, "ecp") and pyscf_mol.ecp: - ecp_name = "none" - ecp_electrons = [0] * pyscf_mol.natm - if isinstance(pyscf_mol.ecp, str): # Simple case: ECP specified as a uniform string name ecp_name = pyscf_mol.ecp diff --git a/python/tests/test_basis_set.py b/python/tests/test_basis_set.py index dec59ddf4..21f0b083d 100644 --- a/python/tests/test_basis_set.py +++ b/python/tests/test_basis_set.py @@ -1579,126 +1579,6 @@ def test_auxiliary_basis_set_accessors(): assert len(aux_for_atom0) == 1 assert aux_for_atom0[0].orbital_type == OrbitalType.S - -def test_auxiliary_basis_set_constructors(): - """Test constructors that include auxiliary basis set parameters.""" - # Set up structure - positions = np.array([[0.0, 0.0, 0.0], [1.4, 0.0, 0.0]]) - elements = ["H", "H"] - structure = Structure(elements, positions) - - # --- Constructor: BasisSet(name, shells, aux_shells, structure) --- - shells = [ - Shell(0, OrbitalType.S, [1.0], [1.0]), - Shell(1, OrbitalType.S, [1.0], [1.0]), - ] - aux_shells = [ - Shell(0, OrbitalType.S, [3.0], [1.0]), - Shell(0, OrbitalType.P, [1.5], [0.8]), - Shell(1, OrbitalType.S, [3.0], [1.0]), - Shell(1, OrbitalType.P, [1.5], [0.8]), - ] - - basis_with_aux = BasisSet("test-basis", shells, aux_shells, structure) - assert basis_with_aux.get_name() == "test-basis" - assert basis_with_aux.get_num_shells() == 2 - assert basis_with_aux.has_aux_basis() - assert basis_with_aux.get_num_aux_shells() == 4 - - # --- Constructor: BasisSet(name, shells, aux_name, aux_shells, structure) --- - basis_named = BasisSet("test-basis", shells, "aux-named", aux_shells, structure) - assert basis_named.has_aux_basis() - assert basis_named.get_aux_name() == "aux-named" - assert basis_named.get_num_aux_shells() == 4 - - # Test with AOType.Cartesian - basis_cart = BasisSet("test-basis", shells, aux_shells, structure, AOType.Cartesian) - assert basis_cart.get_atomic_orbital_type() == AOType.Cartesian - assert basis_cart.has_aux_basis() - - -def test_ecp_with_combined_constructors(): - """Test ECP functionality of constructors that accept both ECP and auxiliary parameters.""" - # Set up structure - positions = np.array([[0.0, 0.0, 0.0], [1.4, 0.0, 0.0]]) - elements = ["H", "H"] - structure = Structure(elements, positions) - - shells = [ - Shell(0, OrbitalType.S, [1.0], [1.0]), - Shell(1, OrbitalType.S, [1.0], [1.0]), - ] - ecp_shells = [Shell(0, OrbitalType.S, [5.0], [10.0], [0])] - - # --- Constructor: BasisSet(name, shells, ecp_shells, ecp_electrons, structure) --- - basis = BasisSet("test-ecp", shells, ecp_shells, [2, 0], structure) - assert basis.get_name() == "test-ecp" - assert basis.get_num_shells() == 2 - assert basis.has_ecp_shells() - assert basis.get_num_ecp_shells() == 1 - - # --- Constructor: BasisSet(name, shells, ecp_name, ecp_shells, ecp_electrons, aux_name, aux_shells, structure) --- - basis_full = BasisSet( - "full-ecp", - shells, - "my-ecp", - ecp_shells, - [2, 0], - structure, - ) - assert basis_full.get_name() == "full-ecp" - assert basis_full.has_ecp_shells() - assert basis_full.get_ecp_name() == "my-ecp" - assert list(basis_full.get_ecp_electrons()) == [2, 0] - assert basis_full.get_num_ecp_shells() == 1 - - -def test_auxiliary_with_combined_constructors(): - """Test auxiliary basis functionality using the aux-only constructor.""" - # Set up structure - positions = np.array([[0.0, 0.0, 0.0], [1.4, 0.0, 0.0]]) - elements = ["H", "H"] - structure = Structure(elements, positions) - - shells = [ - Shell(0, OrbitalType.S, [1.0], [1.0]), - Shell(1, OrbitalType.S, [1.0], [1.0]), - ] - aux_shells = [ - Shell(0, OrbitalType.S, [2.0], [1.0]), - Shell(1, OrbitalType.S, [2.0], [1.0]), - ] - - # --- Constructor: BasisSet(name, shells, aux_name, aux_shells, structure) --- - basis = BasisSet("test-aux", shells, "my-aux", aux_shells, structure) - assert basis.has_aux_basis() - assert not basis.has_ecp_shells() - assert basis.get_aux_name() == "my-aux" - assert basis.get_num_aux_shells() == 2 - - # Test with Cartesian - basis_cart = BasisSet("test-aux-cart", shells, aux_shells, structure, AOType.Cartesian) - assert basis_cart.get_atomic_orbital_type() == AOType.Cartesian - assert basis_cart.has_aux_basis() - assert not basis_cart.has_ecp_shells() - - # Test 8-arg constructor with empty ECP to create aux-only basis - basis_full = BasisSet( - "aux-only", - shells, - "none", - [], - [0, 0], - "my-aux", - aux_shells, - structure, - ) - assert basis_full.has_aux_basis() - assert not basis_full.has_ecp_shells() - assert basis_full.get_aux_name() == "my-aux" - assert basis_full.get_num_aux_shells() == 2 - - def test_auxiliary_basis_set_from_basis_name_database(): """Test from_basis_name with auxiliary using actual basis set database.""" positions = np.array([[0.0, 0.0, 0.0], [1.4, 0.0, 0.0]]) diff --git a/python/tests/test_pyscf_plugin.py b/python/tests/test_pyscf_plugin.py index e9625e2e4..fcad80c4a 100644 --- a/python/tests/test_pyscf_plugin.py +++ b/python/tests/test_pyscf_plugin.py @@ -2596,7 +2596,7 @@ def test_ecp_edge_cases(self): assert qdk_basis_no_meta.has_ecp_shells() assert qdk_basis_no_meta.get_num_ecp_shells() == 1 - assert qdk_basis_no_meta.has_ecp_electrons() # No metadata set + assert qdk_basis_no_meta.has_ecp_electrons() assert qdk_basis_no_meta.get_ecp_name() == "none" # Edge case 2: Full ECP structure format roundtrip From 4773b5ca0cabb1607379cb036fdaa40b5baef2cd Mon Sep 17 00:00:00 2001 From: rainli323 Date: Wed, 29 Apr 2026 21:37:53 +0000 Subject: [PATCH 12/33] pre-commit --- python/tests/test_basis_set.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/tests/test_basis_set.py b/python/tests/test_basis_set.py index 21f0b083d..81fcc306d 100644 --- a/python/tests/test_basis_set.py +++ b/python/tests/test_basis_set.py @@ -1579,6 +1579,7 @@ def test_auxiliary_basis_set_accessors(): assert len(aux_for_atom0) == 1 assert aux_for_atom0[0].orbital_type == OrbitalType.S + def test_auxiliary_basis_set_from_basis_name_database(): """Test from_basis_name with auxiliary using actual basis set database.""" positions = np.array([[0.0, 0.0, 0.0], [1.4, 0.0, 0.0]]) From 6baaa8ab246978a012851bfad8d84cc2d93ad9e5 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Thu, 30 Apr 2026 00:01:25 +0000 Subject: [PATCH 13/33] AI comments, the pybind is heavy, I don't know if I like it --- cpp/src/qdk/chemistry/data/basis_set.cpp | 14 +++ .../user/comprehensive/data/basis_set.rst | 110 ++++++++--------- python/src/pybind11/data/basis_set.cpp | 112 +++++++++++++++++- python/tests/test_basis_set.py | 71 +++++++++++ 4 files changed, 249 insertions(+), 58 deletions(-) diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index 523434378..e9328d926 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -1535,6 +1535,11 @@ bool BasisSet::_is_consistent_with_structure() const { return false; } + if (has_ecp_electrons() && + (_ecp_electrons.size() != _structure->get_num_atoms())) { + return false; + } + // Check if any atom has shells but is beyond the structure's atom count for (size_t atom_idx = 0; atom_idx < _shells_per_atom.size(); ++atom_idx) { if (!_shells_per_atom[atom_idx].empty() && @@ -1552,6 +1557,15 @@ bool BasisSet::_is_consistent_with_structure() const { } } + // Check if any ECP shell references an atom beyond the structure's atom count + for (size_t atom_idx = 0; atom_idx < _ecp_shells_per_atom.size(); + ++atom_idx) { + if (!_ecp_shells_per_atom[atom_idx].empty() && + atom_idx >= _structure->get_num_atoms()) { + return false; + } + } + return true; } diff --git a/docs/source/user/comprehensive/data/basis_set.rst b/docs/source/user/comprehensive/data/basis_set.rst index 8e46cb249..625e70763 100644 --- a/docs/source/user/comprehensive/data/basis_set.rst +++ b/docs/source/user/comprehensive/data/basis_set.rst @@ -252,51 +252,42 @@ JSON representation of a :class:`~qdk_chemistry.data.BasisSet` has the following .. code-block:: json { + "version": "0.1.0", + "name": "6-31G", + "atomic_orbital_type": "spherical", + "num_atomic_orbitals": 9, + "num_shells": 3, + "num_atoms": 2, "atoms": [ { "atom_index": 0, "shells": [ { - "coefficients": [0.1543289673, 0.5353281423, 0.4446345422], + "orbital_type": "s", "exponents": [3.425250914, 0.6239137298, 0.168855404], - "orbital_type": "s" + "coefficients": [0.1543289673, 0.5353281423, 0.4446345422] } - ] - } - ], - "basis_type": "spherical", - "name": "6-31G", - "num_atoms": 2, - "num_basis_functions": 9, - "num_shells": 3, - "ecp_name": "my-ecp", - "ecp_electrons": [28, 0], - "ecp_atoms": [ - { - "atom_index": 0, - "shells": [ + ], + "ecp_shells": [ { - "coefficients": [50.0, 20.0], - "exponents": [10.0, 5.0], "orbital_type": "s", - "rpowers": [0, 2] + "exponents": [10.0, 5.0], + "coefficients": [50.0, 20.0], + "rpowers": [2, 2] } - ] - } - ], - "aux_name": "my-aux-fit", - "aux_atoms": [ - { - "atom_index": 0, - "shells": [ + ], + "aux_shells": [ { - "coefficients": [2.0], + "orbital_type": "s", "exponents": [5.0], - "orbital_type": "s" + "coefficients": [2.0] } ] } - ] + ], + "ecp_name": "my-ecp", + "ecp_electrons": [28, 0], + "aux_name": "my-aux-fit" } HDF5 format @@ -306,31 +297,40 @@ HDF5 representation of a :class:`~qdk_chemistry.data.BasisSet` has the following .. code-block:: text - / - ├── shells/ # Group - │ ├── atom_indices # Dataset: uint32, 1D Array of atom indices - │ ├── coefficients # Dataset: float64, 1D Array of orbital coefficients - │ ├── exponents # Dataset: float64, 1D Array of orbital exponents - │ ├── num_primitives # Dataset: uint32, 1D Array of number of primitives per orbital - │ └── orbital_types # Dataset: int32, 1D Array of orbital type per orbital - ├── ecp_shells/ # Group (present when ECP is defined) - │ ├── atom_indices # Dataset: uint32 - │ ├── coefficients # Dataset: float64 - │ ├── exponents # Dataset: float64 - │ ├── rpowers # Dataset: int32, 1D Array of radial powers per primitive - │ ├── num_primitives # Dataset: uint32 - │ └── orbital_types # Dataset: int32 - ├── aux_shells/ # Group (present when auxiliary basis is defined) - │ ├── atom_indices # Dataset: uint32 - │ ├── coefficients # Dataset: float64 - │ ├── exponents # Dataset: float64 - │ ├── num_primitives # Dataset: uint32 - │ └── orbital_types # Dataset: int32 - └── metadata/ # Group - ├── name # Attribute: string value of the basis set name - ├── ecp_name # Attribute: string (optional) - ├── ecp_electrons # Dataset: uint64 (optional) - └── aux_name # Attribute: string (optional) + /basis_set (Group - top-level) + ├── @version = "0.1.0" (Attribute, variable-length string) + ├── @ecp_name = "lanl2dz" (Attribute, variable-length string, optional) + ├── @aux_name = "cc-pVDZ-RI" (Attribute, variable-length string, optional) + │ + ├── metadata/ (Group) + │ ├── @name = "cc-pVDZ" (Attribute, variable-length string) + │ └── @atomic_orbital_type = "spherical" (Attribute, variable-length string) + │ + ├── shells/ (Group, present if num_shells > 0) + │ ├── atom_indices (Dataset: uint32, 1D, one per shell) + │ ├── orbital_types (Dataset: int32, 1D, one per shell) + │ ├── num_primitives (Dataset: uint32, 1D, one per shell) + │ ├── exponents (Dataset: float64, 1D, flattened across shells) + │ └── coefficients (Dataset: float64, 1D, flattened across shells) + │ + ├── ecp_shells/ (Group, optional - present if ECP shells exist) + │ ├── atom_indices (Dataset: uint32, 1D, one per shell) + │ ├── orbital_types (Dataset: int32, 1D, one per shell) + │ ├── num_primitives (Dataset: uint32, 1D, one per shell) + │ ├── exponents (Dataset: float64, 1D, flattened across shells) + │ ├── coefficients (Dataset: float64, 1D, flattened across shells) + │ └── rpowers (Dataset: int32, 1D, flattened across shells) + │ + ├── ecp_electrons (Dataset: uint64, 1D per atom, optional) + │ + ├── aux_shells/ (Group, optional - present if auxiliary basis exists) + │ ├── atom_indices (Dataset: uint32, 1D, one per shell) + │ ├── orbital_types (Dataset: int32, 1D, one per shell) + │ ├── num_primitives (Dataset: uint32, 1D, one per shell) + │ ├── exponents (Dataset: float64, 1D, flattened across shells) + │ └── coefficients (Dataset: float64, 1D, flattened across shells) + │ + └── structure/ (Group, optional - nested Structure object) .. tab:: C++ API diff --git a/python/src/pybind11/data/basis_set.cpp b/python/src/pybind11/data/basis_set.cpp index 05eb8c712..d33ce3b80 100644 --- a/python/src/pybind11/data/basis_set.cpp +++ b/python/src/pybind11/data/basis_set.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -299,8 +300,9 @@ A shell represents a group of atomic orbitals with the same atom, angular moment // Supports mixed and fully-keyword calls by pulling recognised kwargs // into the positional vector when the corresponding slot is not yet // filled. Expected positional order: name, shells, structure, ... - reject_unexpected_kwargs( - {"name", "shells", "structure", "atomic_orbital_type"}); + reject_unexpected_kwargs({"name", "shells", "ecp_name", "ecp_shells", + "ecp_electrons", "aux_name", "aux_shells", + "structure", "atomic_orbital_type"}); if (kwargs.contains("name")) { if (a.size() > 0) throw_multiple_values("name"); a.push_back(kwargs["name"].cast()); @@ -317,8 +319,112 @@ A shell represents a group of atomic orbitals with the same atom, angular moment throw py::type_error( "BasisSet() 'structure' keyword requires 'name' and " "'shells' to be provided"); - a.push_back(kwargs["structure"].cast()); + // structure is appended later for keyword-driven ECP/aux paths; + // only append here when no ECP/aux kwargs are present. + if (!kwargs.contains("ecp_shells") && !kwargs.contains("ecp_name") && + !kwargs.contains("ecp_electrons") && + !kwargs.contains("aux_shells") && !kwargs.contains("aux_name")) { + a.push_back(kwargs["structure"].cast()); + } } + // --- Keyword-driven ECP / auxiliary path -------------------------- + // When any of the ECP or auxiliary kwargs are provided we dispatch + // directly to the appropriate constructor instead of relying on the + // positional-index logic below. + bool has_ecp_kw = kwargs.contains("ecp_shells") || + kwargs.contains("ecp_name") || + kwargs.contains("ecp_electrons"); + bool has_aux_kw = + kwargs.contains("aux_shells") || kwargs.contains("aux_name"); + + if (has_ecp_kw || has_aux_kw) { + // Need at least name and shells + if (a.size() < 2 || !py::isinstance(a[0]) || + !py::isinstance(a[1])) + throw py::type_error( + "BasisSet() expects (name: str, shells: list[Shell], ...)"); + + auto kw_name = a[0].cast(); + auto kw_shells = to_shell_vec(a[1].cast()); + + // Extract optional structure + std::optional kw_structure; + if (kwargs.contains("structure")) + kw_structure.emplace(kwargs["structure"].cast()); + + // ECP + Aux path + if (has_ecp_kw && has_aux_kw) { + if (!kwargs.contains("ecp_shells") || + !kwargs.contains("ecp_electrons")) + throw py::type_error( + "BasisSet() ECP path requires both 'ecp_shells' and " + "'ecp_electrons'"); + if (!kwargs.contains("aux_shells")) + throw py::type_error( + "BasisSet() auxiliary path requires 'aux_shells'"); + auto ecp_shells_vec = + to_shell_vec(kwargs["ecp_shells"].cast()); + auto ecp_electrons_vec = + kwargs["ecp_electrons"].cast>(); + auto aux_shells_vec = + to_shell_vec(kwargs["aux_shells"].cast()); + std::string ecp_name_str = + kwargs.contains("ecp_name") + ? kwargs["ecp_name"].cast() + : ""; + std::string aux_name_str = + kwargs.contains("aux_name") + ? kwargs["aux_name"].cast() + : ""; + if (!kw_structure.has_value()) + throw py::type_error( + "BasisSet() with ECP+aux requires 'structure'"); + return BasisSet(kw_name, kw_shells, ecp_name_str, ecp_shells_vec, + ecp_electrons_vec, aux_name_str, aux_shells_vec, + kw_structure.value(), ao); + } + + // ECP-only path + if (has_ecp_kw) { + if (!kwargs.contains("ecp_shells") || + !kwargs.contains("ecp_electrons")) + throw py::type_error( + "BasisSet() ECP path requires both 'ecp_shells' and " + "'ecp_electrons'"); + auto ecp_shells_vec = + to_shell_vec(kwargs["ecp_shells"].cast()); + auto ecp_electrons_vec = + kwargs["ecp_electrons"].cast>(); + if (!kw_structure.has_value()) + throw py::type_error("BasisSet() with ECP requires 'structure'"); + if (kwargs.contains("ecp_name")) { + return BasisSet( + kw_name, kw_shells, kwargs["ecp_name"].cast(), + ecp_shells_vec, ecp_electrons_vec, kw_structure.value(), ao); + } + return BasisSet(kw_name, kw_shells, ecp_shells_vec, + ecp_electrons_vec, kw_structure.value(), ao); + } + + // Aux-only path + if (has_aux_kw) { + if (!kwargs.contains("aux_shells")) + throw py::type_error( + "BasisSet() auxiliary path requires 'aux_shells'"); + auto aux_shells_vec = + to_shell_vec(kwargs["aux_shells"].cast()); + if (!kw_structure.has_value()) + throw py::type_error("BasisSet() with aux requires 'structure'"); + if (kwargs.contains("aux_name")) { + return BasisSet(kw_name, kw_shells, + kwargs["aux_name"].cast(), + aux_shells_vec, kw_structure.value(), ao); + } + return BasisSet(kw_name, kw_shells, aux_shells_vec, + kw_structure.value(), ao); + } + } + const size_t n = a.size(); if (n < 2 || !py::isinstance(a[0]) || diff --git a/python/tests/test_basis_set.py b/python/tests/test_basis_set.py index 81fcc306d..116888df6 100644 --- a/python/tests/test_basis_set.py +++ b/python/tests/test_basis_set.py @@ -1726,6 +1726,17 @@ def test_name_shells_aux_structure_with_ao(self, shells, aux_shells, structure): assert b.has_aux_basis() assert b.get_atomic_orbital_type() == AOType.Cartesian + def test_name_shells_aux_structure_kwargs(self, shells, aux_shells, structure): + b = BasisSet("test", shells, aux_shells=aux_shells, structure=structure) + assert b.has_aux_basis() + assert b.get_num_aux_shells() == 1 + assert b.has_structure() + + def test_name_shells_aux_structure_kwargs_with_ao(self, shells, aux_shells, structure): + b = BasisSet("test", shells, aux_shells=aux_shells, structure=structure, atomic_orbital_type=AOType.Cartesian) + assert b.has_aux_basis() + assert b.get_atomic_orbital_type() == AOType.Cartesian + # --- (name, shells, aux_name, aux_shells, structure) --- n==5 str path def test_name_shells_auxname_aux_structure_positional(self, shells, aux_shells, structure): @@ -1734,6 +1745,12 @@ def test_name_shells_auxname_aux_structure_positional(self, shells, aux_shells, assert b.get_aux_name() == "my-aux" assert b.has_structure() + def test_name_shells_auxname_aux_structure_kwargs(self, shells, aux_shells, structure): + b = BasisSet("test", shells, aux_shells=aux_shells, aux_name="my-aux", structure=structure) + assert b.has_aux_basis() + assert b.get_aux_name() == "my-aux" + assert b.has_structure() + # --- (name, shells, ecp_shells, ecp_electrons, structure) --- n==5 list path def test_name_shells_ecp_ecpelec_structure_positional(self, shells, ecp_shells, structure): @@ -1742,6 +1759,24 @@ def test_name_shells_ecp_ecpelec_structure_positional(self, shells, ecp_shells, assert b.get_num_ecp_shells() == 1 assert list(b.get_ecp_electrons()) == [2] + def test_name_shells_ecp_ecpelec_structure_kwargs(self, shells, ecp_shells, structure): + b = BasisSet("test", shells, ecp_shells=ecp_shells, ecp_electrons=[2], structure=structure) + assert b.has_ecp_shells() + assert b.get_num_ecp_shells() == 1 + assert list(b.get_ecp_electrons()) == [2] + + def test_name_shells_ecp_ecpelec_structure_kwargs_with_ao(self, shells, ecp_shells, structure): + b = BasisSet( + "test", + shells, + ecp_shells=ecp_shells, + ecp_electrons=[2], + structure=structure, + atomic_orbital_type=AOType.Cartesian, + ) + assert b.has_ecp_shells() + assert b.get_atomic_orbital_type() == AOType.Cartesian + # --- (name, shells, ecp_name, ecp_shells, ecp_electrons, structure) --- n==6 list path def test_name_shells_ecpname_ecp_ecpelec_structure(self, shells, ecp_shells, structure): @@ -1750,6 +1785,12 @@ def test_name_shells_ecpname_ecp_ecpelec_structure(self, shells, ecp_shells, str assert b.get_ecp_name() == "my-ecp" assert list(b.get_ecp_electrons()) == [2] + def test_name_shells_ecpname_ecp_ecpelec_structure_kwargs(self, shells, ecp_shells, structure): + b = BasisSet("test", shells, ecp_shells=ecp_shells, ecp_name="my-ecp", ecp_electrons=[2], structure=structure) + assert b.has_ecp_shells() + assert b.get_ecp_name() == "my-ecp" + assert list(b.get_ecp_electrons()) == [2] + # --- (name, shells, ecp_name, ecp_shells, ecp_electrons, aux_name, aux_shells, structure) --- n==8 path def test_full_8arg_constructor(self, shells, ecp_shells, aux_shells, structure): @@ -1760,6 +1801,36 @@ def test_full_8arg_constructor(self, shells, ecp_shells, aux_shells, structure): assert b.get_aux_name() == "my-aux" assert b.has_structure() + def test_full_8arg_constructor_kwargs(self, shells, ecp_shells, aux_shells, structure): + b = BasisSet( + "test", + shells, + ecp_name="my-ecp", + ecp_shells=ecp_shells, + ecp_electrons=[2], + aux_name="my-aux", + aux_shells=aux_shells, + structure=structure, + ) + assert b.has_ecp_shells() + assert b.get_ecp_name() == "my-ecp" + assert b.has_aux_basis() + assert b.get_aux_name() == "my-aux" + assert b.has_structure() + + def test_full_8arg_constructor_kwargs_without_names(self, shells, ecp_shells, aux_shells, structure): + b = BasisSet( + "test", + shells, + ecp_shells=ecp_shells, + ecp_electrons=[2], + aux_shells=aux_shells, + structure=structure, + ) + assert b.has_ecp_shells() + assert b.has_aux_basis() + assert b.has_structure() + # --- Error cases: unexpected kwargs --- def test_rejects_unexpected_kwarg(self, shells): From 097b7462b6652185f706f40dc2038dfc7817fc9b Mon Sep 17 00:00:00 2001 From: rainli323 Date: Fri, 1 May 2026 22:51:46 +0000 Subject: [PATCH 14/33] update pybind11 treatment of BasisSet. Finally decent --- cpp/include/qdk/chemistry/data/basis_set.hpp | 2 +- cpp/src/qdk/chemistry/data/basis_set.cpp | 20 +- python/src/pybind11/data/basis_set.cpp | 479 +++++++------------ python/tests/test_basis_set.py | 87 +--- 4 files changed, 211 insertions(+), 377 deletions(-) diff --git a/cpp/include/qdk/chemistry/data/basis_set.hpp b/cpp/include/qdk/chemistry/data/basis_set.hpp index 2bb88a5db..1afd5344c 100644 --- a/cpp/include/qdk/chemistry/data/basis_set.hpp +++ b/cpp/include/qdk/chemistry/data/basis_set.hpp @@ -139,7 +139,7 @@ struct Shell { /** * @brief Check if this shell has radial powers (i.e., is an ECP shell) */ - bool has_radial_powers() const { return rpowers.size() > 0; } + bool has_radial_powers() const { return rpowers.size() > 0 && rpowers.any(); } /** * @brief Get number of atomic orbitals in this shell diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index e9328d926..8b8975226 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -441,8 +441,14 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, _shells_per_atom[atom_index].push_back(shell); } - // Organize ECP shells by atom index + // Organize auxiliary shells by atom index for (const auto& aux_shell : aux_shells) { + if (aux_shell.has_radial_powers()) { + throw std::invalid_argument( + "Auxiliary shells contains a shell with radial powers; did you pass " + "ECP shells by mistake? ECP basis must be constructed with both ECP " + "shells and ECP electrons."); + } size_t atom_index = aux_shell.atom_index; // Ensure we have enough space for this atom @@ -560,6 +566,12 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, // Organize auxiliary shells by atom index for (const auto& aux_shell : aux_shells) { + if (aux_shell.has_radial_powers()) { + throw std::invalid_argument( + "aux_shells contains a shell with radial powers; did you pass " + "ECP shells by mistake? ECP basis must be constructed with both ECP " + "shells and ECP electrons."); + } size_t atom_index = aux_shell.atom_index; // Ensure we have enough space for this atom @@ -635,6 +647,12 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, // Organize auxiliary shells by atom index for (const auto& aux_shell : aux_shells) { + if (aux_shell.has_radial_powers()) { + throw std::invalid_argument( + "Auxiliary shells contains a shell with radial powers; did you pass " + "ECP shells by mistake? ECP basis must be constructed with both ECP " + "shells and ECP electrons."); + } size_t atom_index = aux_shell.atom_index; if (atom_index >= _aux_shells_per_atom.size()) { _aux_shells_per_atom.resize(atom_index + 1); diff --git a/python/src/pybind11/data/basis_set.cpp b/python/src/pybind11/data/basis_set.cpp index d33ce3b80..ae9969d37 100644 --- a/python/src/pybind11/data/basis_set.cpp +++ b/python/src/pybind11/data/basis_set.cpp @@ -8,7 +8,6 @@ #include #include -#include #include #include #include @@ -56,18 +55,28 @@ std::shared_ptr basis_set_from_json_file_wrapper( qdk::chemistry::python::utils::to_string_path(filename)); } -} // namespace - -// Helper: convert a Python list of Shell objects to std::vector. -// This bypasses pybind11's list_caster which crashes under py::smart_holder -// when it tries to weakref a list_iterator during overload probing. -static std::vector to_shell_vec(const py::list& lst) { +// Convert a Python list of Shell objects to std::vector. +// Used by lambda-based init overloads to bypass pybind11's list_caster, which +// can create a list_iterator that crashes under py::smart_holder during +// overload probing of constructors with multiple std::vector params. +// +// Uses index-based access (PyList_GetItem) instead of iteration so that no +// Python list_iterator object is ever created (list_iterator does not support +// weak references, which smart_holder tries to install). +std::vector to_shell_vec(const py::list& lst) { + const ssize_t n = py::len(lst); std::vector result; - result.reserve(lst.size()); - for (auto& item : lst) result.push_back(item.cast()); + result.reserve(static_cast(n)); + for (ssize_t i = 0; i < n; ++i) { + result.push_back(py::reinterpret_borrow( + PyList_GET_ITEM(lst.ptr(), i)) + .cast()); + } return result; } +} // namespace + void bind_basis_set(py::module& m) { using namespace qdk::chemistry::data; using qdk::chemistry::python::utils::bind_getter_as_property; @@ -213,298 +222,176 @@ A shell represents a group of atomic orbitals with the same atom, angular moment Examples: Create a simple basis set: - >>> from qdk_chemistry.data import BasisSet, OrbitalType - >>> basis = BasisSet("STO-3G") - >>> basis.add_shell(0, OrbitalType.S, 1.0, 1.0) # s orbital on atom 0 + >>> from qdk_chemistry.data import BasisSet, OrbitalType, Shell + >>> shell = Shell(0, OrbitalType.S, [1.0], [1.0]) + >>> basis = BasisSet("STO-3G", [shell]) >>> print(f"Number of atomic orbitals: {basis.get_num_atomic_orbitals()}") )"); - // ----------------------------------------------------------------------- - // Single __init__ dispatcher. Multiple py::init overloads crash under - // py::smart_holder because pybind11 probes each overload and the - // list_caster> internally creates a list_iterator that - // smart_holder cannot weak-reference. A single py::init with runtime - // type checks avoids overload probing entirely. - // - // Helper: if the last positional arg is an AOType, pop it off and use it; - // otherwise look in kwargs; otherwise default to Spherical. - // Also support fully-keyword calls: BasisSet(name=..., shells=..., ...). - // ----------------------------------------------------------------------- + // Copy constructor + basis_set.def(py::init(), py::arg("other"), + "Copy constructor. Creates a deep copy of the basis set."); + + // BasisSet(name, shells [, atomic_orbital_type]) + basis_set.def( + py::init([](const std::string& name, const py::list& shells, AOType ao) { + return BasisSet(name, to_shell_vec(shells), ao); + }), + py::arg("name"), py::arg("shells"), + py::arg("atomic_orbital_type") = AOType::Spherical, + R"( +Create a basis set from a name and shells. + +Args: + name (str): Name of the basis set (e.g., "6-31G", "cc-pVDZ") + shells (list[Shell]): List of Shell objects + atomic_orbital_type (AOType): Spherical or Cartesian (default Spherical) +)"); + + // BasisSet(name, shells, structure [, atomic_orbital_type]) + basis_set.def( + py::init([](const std::string& name, const py::list& shells, + const Structure& structure, AOType ao) { + return BasisSet(name, to_shell_vec(shells), structure, ao); + }), + py::arg("name"), py::arg("shells"), py::arg("structure"), + py::arg("atomic_orbital_type") = AOType::Spherical, + R"( +Create a basis set from a name, shells, and molecular structure. + +Args: + name (str): Name of the basis set + shells (list[Shell]): List of Shell objects + structure (Structure): Molecular structure + atomic_orbital_type (AOType): Spherical or Cartesian (default Spherical) +)"); + + // BasisSet(name, shells, aux_shells, structure [, atomic_orbital_type]) + basis_set.def( + py::init([](const std::string& name, const py::list& shells, + const py::list& aux_shells, const Structure& structure, + AOType ao) { + return BasisSet(name, to_shell_vec(shells), to_shell_vec(aux_shells), + structure, ao); + }), + py::arg("name"), py::arg("shells"), py::arg("aux_shells"), + py::arg("structure"), py::arg("atomic_orbital_type") = AOType::Spherical, + R"( +Create a basis set with auxiliary shells. + +Args: + name (str): Name of the basis set + shells (list[Shell]): List of Shell objects + aux_shells (list[Shell]): List of auxiliary Shell objects (e.g., for density fitting) + structure (Structure): Molecular structure + atomic_orbital_type (AOType): Spherical or Cartesian (default Spherical) +)"); + + // BasisSet(name, shells, aux_name, aux_shells, structure + // [, atomic_orbital_type]) basis_set.def( - py::init([](py::args args, py::kwargs kwargs) -> BasisSet { - // --- Helper: check whether a Python type name equals a target ----- - auto type_name = [](const py::object& obj) -> std::string { - return py::str(obj.get_type().attr("__name__")).cast(); - }; - - // --- Helper: true when any Shell in a py::list has rpowers -------- - // ECP shells carry rpowers; auxiliary shells do not. - auto list_has_ecp_shells = [](const py::list& lst) -> bool { - for (auto& item : lst) { - if (item.cast().has_radial_powers()) return true; - } - return false; - }; - - // --- Collect positional args into a mutable vector ---------------- - std::vector a; - a.reserve(args.size()); - for (size_t i = 0; i < args.size(); ++i) - a.push_back(args[i].cast()); - - // If last positional arg is AOType, pop it. - AOType ao = AOType::Spherical; - if (!a.empty() && type_name(a.back()) == "AOType") { - ao = a.back().cast(); - a.pop_back(); - } - // Keyword override - if (kwargs.contains("atomic_orbital_type")) - ao = kwargs["atomic_orbital_type"].cast(); - - // --- Merge keyword args into positional vector -------------------- - auto throw_multiple_values = [](const char* arg_name) { - throw py::type_error(std::string("BasisSet() got multiple values for " - "argument '") + - arg_name + "'"); - }; - auto reject_unexpected_kwargs = - [&kwargs](std::initializer_list allowed) { - for (auto item : kwargs) { - auto key = py::cast(item.first); - bool recognised = false; - for (auto allowed_key : allowed) { - if (key == allowed_key) { - recognised = true; - break; - } - } - if (!recognised) { - throw py::type_error( - "BasisSet() got an unexpected keyword " - "argument '" + - key + "'"); - } - } - }; - const size_t initial_n = a.size(); - // Copy constructor: BasisSet(other) - if (initial_n == 1 && py::isinstance(a[0])) { - if (!kwargs.empty()) { - throw py::type_error( - "BasisSet() copy constructor does not accept keyword " - "arguments"); - } - return BasisSet(a[0].cast()); - } - // Supports mixed and fully-keyword calls by pulling recognised kwargs - // into the positional vector when the corresponding slot is not yet - // filled. Expected positional order: name, shells, structure, ... - reject_unexpected_kwargs({"name", "shells", "ecp_name", "ecp_shells", - "ecp_electrons", "aux_name", "aux_shells", - "structure", "atomic_orbital_type"}); - if (kwargs.contains("name")) { - if (a.size() > 0) throw_multiple_values("name"); - a.push_back(kwargs["name"].cast()); - } - if (kwargs.contains("shells")) { - if (a.size() > 1) throw_multiple_values("shells"); - if (a.size() == 1) { - a.push_back(kwargs["shells"].cast()); - } - } - if (kwargs.contains("structure")) { - if (a.size() > 2) throw_multiple_values("structure"); - if (a.size() < 2) - throw py::type_error( - "BasisSet() 'structure' keyword requires 'name' and " - "'shells' to be provided"); - // structure is appended later for keyword-driven ECP/aux paths; - // only append here when no ECP/aux kwargs are present. - if (!kwargs.contains("ecp_shells") && !kwargs.contains("ecp_name") && - !kwargs.contains("ecp_electrons") && - !kwargs.contains("aux_shells") && !kwargs.contains("aux_name")) { - a.push_back(kwargs["structure"].cast()); - } - } - // --- Keyword-driven ECP / auxiliary path -------------------------- - // When any of the ECP or auxiliary kwargs are provided we dispatch - // directly to the appropriate constructor instead of relying on the - // positional-index logic below. - bool has_ecp_kw = kwargs.contains("ecp_shells") || - kwargs.contains("ecp_name") || - kwargs.contains("ecp_electrons"); - bool has_aux_kw = - kwargs.contains("aux_shells") || kwargs.contains("aux_name"); - - if (has_ecp_kw || has_aux_kw) { - // Need at least name and shells - if (a.size() < 2 || !py::isinstance(a[0]) || - !py::isinstance(a[1])) - throw py::type_error( - "BasisSet() expects (name: str, shells: list[Shell], ...)"); - - auto kw_name = a[0].cast(); - auto kw_shells = to_shell_vec(a[1].cast()); - - // Extract optional structure - std::optional kw_structure; - if (kwargs.contains("structure")) - kw_structure.emplace(kwargs["structure"].cast()); - - // ECP + Aux path - if (has_ecp_kw && has_aux_kw) { - if (!kwargs.contains("ecp_shells") || - !kwargs.contains("ecp_electrons")) - throw py::type_error( - "BasisSet() ECP path requires both 'ecp_shells' and " - "'ecp_electrons'"); - if (!kwargs.contains("aux_shells")) - throw py::type_error( - "BasisSet() auxiliary path requires 'aux_shells'"); - auto ecp_shells_vec = - to_shell_vec(kwargs["ecp_shells"].cast()); - auto ecp_electrons_vec = - kwargs["ecp_electrons"].cast>(); - auto aux_shells_vec = - to_shell_vec(kwargs["aux_shells"].cast()); - std::string ecp_name_str = - kwargs.contains("ecp_name") - ? kwargs["ecp_name"].cast() - : ""; - std::string aux_name_str = - kwargs.contains("aux_name") - ? kwargs["aux_name"].cast() - : ""; - if (!kw_structure.has_value()) - throw py::type_error( - "BasisSet() with ECP+aux requires 'structure'"); - return BasisSet(kw_name, kw_shells, ecp_name_str, ecp_shells_vec, - ecp_electrons_vec, aux_name_str, aux_shells_vec, - kw_structure.value(), ao); - } - - // ECP-only path - if (has_ecp_kw) { - if (!kwargs.contains("ecp_shells") || - !kwargs.contains("ecp_electrons")) - throw py::type_error( - "BasisSet() ECP path requires both 'ecp_shells' and " - "'ecp_electrons'"); - auto ecp_shells_vec = - to_shell_vec(kwargs["ecp_shells"].cast()); - auto ecp_electrons_vec = - kwargs["ecp_electrons"].cast>(); - if (!kw_structure.has_value()) - throw py::type_error("BasisSet() with ECP requires 'structure'"); - if (kwargs.contains("ecp_name")) { - return BasisSet( - kw_name, kw_shells, kwargs["ecp_name"].cast(), - ecp_shells_vec, ecp_electrons_vec, kw_structure.value(), ao); - } - return BasisSet(kw_name, kw_shells, ecp_shells_vec, - ecp_electrons_vec, kw_structure.value(), ao); - } - - // Aux-only path - if (has_aux_kw) { - if (!kwargs.contains("aux_shells")) - throw py::type_error( - "BasisSet() auxiliary path requires 'aux_shells'"); - auto aux_shells_vec = - to_shell_vec(kwargs["aux_shells"].cast()); - if (!kw_structure.has_value()) - throw py::type_error("BasisSet() with aux requires 'structure'"); - if (kwargs.contains("aux_name")) { - return BasisSet(kw_name, kw_shells, - kwargs["aux_name"].cast(), - aux_shells_vec, kw_structure.value(), ao); - } - return BasisSet(kw_name, kw_shells, aux_shells_vec, - kw_structure.value(), ao); - } - } - - const size_t n = a.size(); - - if (n < 2 || !py::isinstance(a[0]) || - !py::isinstance(a[1])) - throw py::type_error( - "BasisSet() expects (name: str, shells: list[Shell], ...)"); - - auto name = a[0].cast(); - auto shells = to_shell_vec(a[1].cast()); - - // (name, shells) - if (n == 2) return BasisSet(name, shells, ao); - - // (name, shells, structure) - if (n == 3 && py::isinstance(a[2])) - return BasisSet(name, shells, a[2].cast(), ao); - - // (name, shells, , structure) -- n==4 - // Disambiguate: if shells in a[2] have rpowers → ECP path (needs - // ecp_electrons); if not → auxiliary path. - if (n == 4 && py::isinstance(a[2]) && - py::isinstance(a[3])) { - auto extra = a[2].cast(); - if (list_has_ecp_shells(extra)) { - throw py::type_error( - "BasisSet() with ECP shells requires explicit " - "ecp_electrons: use (name, shells, ecp_shells, " - "ecp_electrons, structure)"); - } - // Auxiliary shells path - return BasisSet(name, shells, to_shell_vec(extra), - a[3].cast(), ao); - } - - if (n == 5) { - if (py::isinstance(a[2])) { - // (name, shells, aux_name, aux_shells, structure) - return BasisSet(name, shells, a[2].cast(), - to_shell_vec(a[3].cast()), - a[4].cast(), ao); - } - // (name, shells, ecp_shells, ecp_electrons, structure) - return BasisSet(name, shells, to_shell_vec(a[2].cast()), - a[3].cast>(), - a[4].cast(), ao); - } - - // (name, shells, ecp_name, ecp_shells, ecp_electrons, structure) - if (n == 6 && py::isinstance(a[2])) - return BasisSet(name, shells, a[2].cast(), - to_shell_vec(a[3].cast()), - a[4].cast>(), - a[5].cast(), ao); - - // (name, shells, ecp_name, ecp_shells, ecp_electrons, - // aux_name, aux_shells, structure) - if (n == 8 && py::isinstance(a[2])) - return BasisSet( - name, shells, a[2].cast(), - to_shell_vec(a[3].cast()), - a[4].cast>(), a[5].cast(), - to_shell_vec(a[6].cast()), a[7].cast(), ao); - - throw py::type_error( - "No matching BasisSet constructor for the given arguments"); + py::init([](const std::string& name, const py::list& shells, + const std::string& aux_name, const py::list& aux_shells, + const Structure& structure, AOType ao) { + return BasisSet(name, to_shell_vec(shells), aux_name, + to_shell_vec(aux_shells), structure, ao); }), + py::arg("name"), py::arg("shells"), py::arg("aux_name"), + py::arg("aux_shells"), py::arg("structure"), + py::arg("atomic_orbital_type") = AOType::Spherical, R"( -BasisSet constructor. - -Supported signatures (atomic_orbital_type is always optional, default Spherical):: - - BasisSet(other: BasisSet) - BasisSet(name, shells) - BasisSet(name, shells, structure) - BasisSet(name, shells, aux_shells, structure) - BasisSet(name, shells, aux_name, aux_shells, structure) - BasisSet(name, shells, ecp_shells, ecp_electrons, structure) - BasisSet(name, shells, ecp_name, ecp_shells, ecp_electrons, structure) - BasisSet(name, shells, ecp_name, ecp_shells, ecp_electrons, - aux_name, aux_shells, structure) +Create a basis set with a named auxiliary basis. + +Args: + name (str): Name of the basis set + shells (list[Shell]): List of Shell objects + aux_name (str): Name of the auxiliary basis set + aux_shells (list[Shell]): List of auxiliary Shell objects + structure (Structure): Molecular structure + atomic_orbital_type (AOType): Spherical or Cartesian (default Spherical) +)"); + + // BasisSet(name, shells, ecp_shells, ecp_electrons, structure + // [, atomic_orbital_type]) + basis_set.def( + py::init([](const std::string& name, const py::list& shells, + const py::list& ecp_shells, + const std::vector& ecp_electrons, + const Structure& structure, AOType ao) { + return BasisSet(name, to_shell_vec(shells), to_shell_vec(ecp_shells), + ecp_electrons, structure, ao); + }), + py::arg("name"), py::arg("shells"), py::arg("ecp_shells"), + py::arg("ecp_electrons"), py::arg("structure"), + py::arg("atomic_orbital_type") = AOType::Spherical, + R"( +Create a basis set with ECP shells. + +Args: + name (str): Name of the basis set + shells (list[Shell]): List of Shell objects + ecp_shells (list[Shell]): List of ECP Shell objects (with radial powers) + ecp_electrons (list[int]): Number of ECP electrons per atom + structure (Structure): Molecular structure + atomic_orbital_type (AOType): Spherical or Cartesian (default Spherical) +)"); + + // BasisSet(name, shells, ecp_name, ecp_shells, ecp_electrons, structure + // [, atomic_orbital_type]) + basis_set.def( + py::init([](const std::string& name, const py::list& shells, + const std::string& ecp_name, const py::list& ecp_shells, + const std::vector& ecp_electrons, + const Structure& structure, AOType ao) { + return BasisSet(name, to_shell_vec(shells), ecp_name, + to_shell_vec(ecp_shells), ecp_electrons, structure, + ao); + }), + py::arg("name"), py::arg("shells"), py::arg("ecp_name"), + py::arg("ecp_shells"), py::arg("ecp_electrons"), py::arg("structure"), + py::arg("atomic_orbital_type") = AOType::Spherical, + R"( +Create a basis set with a named ECP. + +Args: + name (str): Name of the basis set + shells (list[Shell]): List of Shell objects + ecp_name (str): Name of the ECP basis set + ecp_shells (list[Shell]): List of ECP Shell objects + ecp_electrons (list[int]): Number of ECP electrons per atom + structure (Structure): Molecular structure + atomic_orbital_type (AOType): Spherical or Cartesian (default Spherical) +)"); + + // BasisSet(name, shells, ecp_name, ecp_shells, ecp_electrons, + // aux_name, aux_shells, structure [, atomic_orbital_type]) + basis_set.def( + py::init([](const std::string& name, const py::list& shells, + const std::string& ecp_name, const py::list& ecp_shells, + const std::vector& ecp_electrons, + const std::string& aux_name, const py::list& aux_shells, + const Structure& structure, AOType ao) { + return BasisSet(name, to_shell_vec(shells), ecp_name, + to_shell_vec(ecp_shells), ecp_electrons, aux_name, + to_shell_vec(aux_shells), structure, ao); + }), + py::arg("name"), py::arg("shells"), py::arg("ecp_name"), + py::arg("ecp_shells"), py::arg("ecp_electrons"), py::arg("aux_name"), + py::arg("aux_shells"), py::arg("structure"), + py::arg("atomic_orbital_type") = AOType::Spherical, + R"( +Create a basis set with ECP and auxiliary basis. + +Args: + name (str): Name of the basis set + shells (list[Shell]): List of Shell objects + ecp_name (str): Name of the ECP basis set + ecp_shells (list[Shell]): List of ECP Shell objects + ecp_electrons (list[int]): Number of ECP electrons per atom + aux_name (str): Name of the auxiliary basis set + aux_shells (list[Shell]): List of auxiliary Shell objects + structure (Structure): Molecular structure + atomic_orbital_type (AOType): Spherical or Cartesian (default Spherical) )"); // Basis type management @@ -1597,7 +1484,7 @@ Name used for custom ECP basis sets. Type: str )"); - + // Data type name class attribute basis_set.attr("_data_type_name") = DATACLASS_TO_SNAKE_CASE(BasisSet); } diff --git a/python/tests/test_basis_set.py b/python/tests/test_basis_set.py index 116888df6..a145e4c8f 100644 --- a/python/tests/test_basis_set.py +++ b/python/tests/test_basis_set.py @@ -1642,7 +1642,7 @@ def aux_shells(self): @pytest.fixture def ecp_shells(self): - return [Shell(0, OrbitalType.S, [5.0], [10.0], [0])] + return [Shell(0, OrbitalType.S, [5.0], [10.0], [1])] # --- Copy constructor: BasisSet(other) --- @@ -1654,7 +1654,7 @@ def test_copy_positional(self, shells): def test_copy_rejects_kwargs(self, shells): original = BasisSet("orig", shells) - with pytest.raises(TypeError, match="copy constructor does not accept keyword"): + with pytest.raises(TypeError, match="incompatible constructor arguments"): BasisSet(original, atomic_orbital_type=AOType.Cartesian) # --- (name, shells) --- @@ -1726,17 +1726,6 @@ def test_name_shells_aux_structure_with_ao(self, shells, aux_shells, structure): assert b.has_aux_basis() assert b.get_atomic_orbital_type() == AOType.Cartesian - def test_name_shells_aux_structure_kwargs(self, shells, aux_shells, structure): - b = BasisSet("test", shells, aux_shells=aux_shells, structure=structure) - assert b.has_aux_basis() - assert b.get_num_aux_shells() == 1 - assert b.has_structure() - - def test_name_shells_aux_structure_kwargs_with_ao(self, shells, aux_shells, structure): - b = BasisSet("test", shells, aux_shells=aux_shells, structure=structure, atomic_orbital_type=AOType.Cartesian) - assert b.has_aux_basis() - assert b.get_atomic_orbital_type() == AOType.Cartesian - # --- (name, shells, aux_name, aux_shells, structure) --- n==5 str path def test_name_shells_auxname_aux_structure_positional(self, shells, aux_shells, structure): @@ -1745,12 +1734,6 @@ def test_name_shells_auxname_aux_structure_positional(self, shells, aux_shells, assert b.get_aux_name() == "my-aux" assert b.has_structure() - def test_name_shells_auxname_aux_structure_kwargs(self, shells, aux_shells, structure): - b = BasisSet("test", shells, aux_shells=aux_shells, aux_name="my-aux", structure=structure) - assert b.has_aux_basis() - assert b.get_aux_name() == "my-aux" - assert b.has_structure() - # --- (name, shells, ecp_shells, ecp_electrons, structure) --- n==5 list path def test_name_shells_ecp_ecpelec_structure_positional(self, shells, ecp_shells, structure): @@ -1759,24 +1742,6 @@ def test_name_shells_ecp_ecpelec_structure_positional(self, shells, ecp_shells, assert b.get_num_ecp_shells() == 1 assert list(b.get_ecp_electrons()) == [2] - def test_name_shells_ecp_ecpelec_structure_kwargs(self, shells, ecp_shells, structure): - b = BasisSet("test", shells, ecp_shells=ecp_shells, ecp_electrons=[2], structure=structure) - assert b.has_ecp_shells() - assert b.get_num_ecp_shells() == 1 - assert list(b.get_ecp_electrons()) == [2] - - def test_name_shells_ecp_ecpelec_structure_kwargs_with_ao(self, shells, ecp_shells, structure): - b = BasisSet( - "test", - shells, - ecp_shells=ecp_shells, - ecp_electrons=[2], - structure=structure, - atomic_orbital_type=AOType.Cartesian, - ) - assert b.has_ecp_shells() - assert b.get_atomic_orbital_type() == AOType.Cartesian - # --- (name, shells, ecp_name, ecp_shells, ecp_electrons, structure) --- n==6 list path def test_name_shells_ecpname_ecp_ecpelec_structure(self, shells, ecp_shells, structure): @@ -1785,12 +1750,6 @@ def test_name_shells_ecpname_ecp_ecpelec_structure(self, shells, ecp_shells, str assert b.get_ecp_name() == "my-ecp" assert list(b.get_ecp_electrons()) == [2] - def test_name_shells_ecpname_ecp_ecpelec_structure_kwargs(self, shells, ecp_shells, structure): - b = BasisSet("test", shells, ecp_shells=ecp_shells, ecp_name="my-ecp", ecp_electrons=[2], structure=structure) - assert b.has_ecp_shells() - assert b.get_ecp_name() == "my-ecp" - assert list(b.get_ecp_electrons()) == [2] - # --- (name, shells, ecp_name, ecp_shells, ecp_electrons, aux_name, aux_shells, structure) --- n==8 path def test_full_8arg_constructor(self, shells, ecp_shells, aux_shells, structure): @@ -1801,60 +1760,30 @@ def test_full_8arg_constructor(self, shells, ecp_shells, aux_shells, structure): assert b.get_aux_name() == "my-aux" assert b.has_structure() - def test_full_8arg_constructor_kwargs(self, shells, ecp_shells, aux_shells, structure): - b = BasisSet( - "test", - shells, - ecp_name="my-ecp", - ecp_shells=ecp_shells, - ecp_electrons=[2], - aux_name="my-aux", - aux_shells=aux_shells, - structure=structure, - ) - assert b.has_ecp_shells() - assert b.get_ecp_name() == "my-ecp" - assert b.has_aux_basis() - assert b.get_aux_name() == "my-aux" - assert b.has_structure() - - def test_full_8arg_constructor_kwargs_without_names(self, shells, ecp_shells, aux_shells, structure): - b = BasisSet( - "test", - shells, - ecp_shells=ecp_shells, - ecp_electrons=[2], - aux_shells=aux_shells, - structure=structure, - ) - assert b.has_ecp_shells() - assert b.has_aux_basis() - assert b.has_structure() - # --- Error cases: unexpected kwargs --- def test_rejects_unexpected_kwarg(self, shells): - with pytest.raises(TypeError, match="unexpected keyword argument 'bogus'"): + with pytest.raises(TypeError, match="incompatible constructor arguments"): BasisSet("test", shells, bogus=42) def test_rejects_unexpected_kwarg_typo(self, shells): - with pytest.raises(TypeError, match="unexpected keyword argument 'struture'"): + with pytest.raises(TypeError, match="incompatible constructor arguments"): BasisSet("test", shells, struture="oops") # --- Error cases: multiple values --- def test_rejects_name_multiple_values(self, shells): - with pytest.raises(TypeError, match="multiple values for argument 'name'"): + with pytest.raises(TypeError, match="incompatible constructor arguments"): BasisSet("test", name="other", shells=shells) def test_rejects_shells_multiple_values(self, shells): - with pytest.raises(TypeError, match="multiple values for argument 'shells'"): + with pytest.raises(TypeError, match="incompatible constructor arguments"): BasisSet("test", shells, shells=shells) # --- Error cases: structure without shells --- def test_rejects_structure_without_shells(self, structure): - with pytest.raises(TypeError, match="'structure' keyword requires 'name' and 'shells'"): + with pytest.raises(TypeError, match="incompatible constructor arguments"): BasisSet(name="test", structure=structure) # --- Error cases: no matching constructor --- @@ -1870,5 +1799,5 @@ def test_rejects_no_args(self): # --- Error cases: ECP shells at n==4 should raise --- def test_ecp_at_n4_raises(self, shells, ecp_shells, structure): - with pytest.raises(TypeError, match="ECP shells requires explicit ecp_electrons"): + with pytest.raises(ValueError, match="Auxiliary shells contains a shell with radial powers"): BasisSet("test", shells, ecp_shells, structure) From 1463c401e3a2d17c674621cce16e1414cf082dfa Mon Sep 17 00:00:00 2001 From: rainli323 Date: Fri, 1 May 2026 23:02:28 +0000 Subject: [PATCH 15/33] pre-commit --- python/src/pybind11/data/basis_set.cpp | 70 +++++++++++++------------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/python/src/pybind11/data/basis_set.cpp b/python/src/pybind11/data/basis_set.cpp index ae9969d37..3f62b6937 100644 --- a/python/src/pybind11/data/basis_set.cpp +++ b/python/src/pybind11/data/basis_set.cpp @@ -68,9 +68,9 @@ std::vector to_shell_vec(const py::list& lst) { std::vector result; result.reserve(static_cast(n)); for (ssize_t i = 0; i < n; ++i) { - result.push_back(py::reinterpret_borrow( - PyList_GET_ITEM(lst.ptr(), i)) - .cast()); + result.push_back( + py::reinterpret_borrow(PyList_GET_ITEM(lst.ptr(), i)) + .cast()); } return result; } @@ -249,14 +249,13 @@ Create a basis set from a name and shells. )"); // BasisSet(name, shells, structure [, atomic_orbital_type]) - basis_set.def( - py::init([](const std::string& name, const py::list& shells, - const Structure& structure, AOType ao) { - return BasisSet(name, to_shell_vec(shells), structure, ao); - }), - py::arg("name"), py::arg("shells"), py::arg("structure"), - py::arg("atomic_orbital_type") = AOType::Spherical, - R"( + basis_set.def(py::init([](const std::string& name, const py::list& shells, + const Structure& structure, AOType ao) { + return BasisSet(name, to_shell_vec(shells), structure, ao); + }), + py::arg("name"), py::arg("shells"), py::arg("structure"), + py::arg("atomic_orbital_type") = AOType::Spherical, + R"( Create a basis set from a name, shells, and molecular structure. Args: @@ -267,16 +266,16 @@ Create a basis set from a name, shells, and molecular structure. )"); // BasisSet(name, shells, aux_shells, structure [, atomic_orbital_type]) - basis_set.def( - py::init([](const std::string& name, const py::list& shells, - const py::list& aux_shells, const Structure& structure, - AOType ao) { - return BasisSet(name, to_shell_vec(shells), to_shell_vec(aux_shells), - structure, ao); - }), - py::arg("name"), py::arg("shells"), py::arg("aux_shells"), - py::arg("structure"), py::arg("atomic_orbital_type") = AOType::Spherical, - R"( + basis_set.def(py::init([](const std::string& name, const py::list& shells, + const py::list& aux_shells, + const Structure& structure, AOType ao) { + return BasisSet(name, to_shell_vec(shells), + to_shell_vec(aux_shells), structure, ao); + }), + py::arg("name"), py::arg("shells"), py::arg("aux_shells"), + py::arg("structure"), + py::arg("atomic_orbital_type") = AOType::Spherical, + R"( Create a basis set with auxiliary shells. Args: @@ -313,18 +312,18 @@ Create a basis set with a named auxiliary basis. // BasisSet(name, shells, ecp_shells, ecp_electrons, structure // [, atomic_orbital_type]) - basis_set.def( - py::init([](const std::string& name, const py::list& shells, - const py::list& ecp_shells, - const std::vector& ecp_electrons, - const Structure& structure, AOType ao) { - return BasisSet(name, to_shell_vec(shells), to_shell_vec(ecp_shells), - ecp_electrons, structure, ao); - }), - py::arg("name"), py::arg("shells"), py::arg("ecp_shells"), - py::arg("ecp_electrons"), py::arg("structure"), - py::arg("atomic_orbital_type") = AOType::Spherical, - R"( + basis_set.def(py::init([](const std::string& name, const py::list& shells, + const py::list& ecp_shells, + const std::vector& ecp_electrons, + const Structure& structure, AOType ao) { + return BasisSet(name, to_shell_vec(shells), + to_shell_vec(ecp_shells), ecp_electrons, + structure, ao); + }), + py::arg("name"), py::arg("shells"), py::arg("ecp_shells"), + py::arg("ecp_electrons"), py::arg("structure"), + py::arg("atomic_orbital_type") = AOType::Spherical, + R"( Create a basis set with ECP shells. Args: @@ -344,8 +343,7 @@ Create a basis set with ECP shells. const std::vector& ecp_electrons, const Structure& structure, AOType ao) { return BasisSet(name, to_shell_vec(shells), ecp_name, - to_shell_vec(ecp_shells), ecp_electrons, structure, - ao); + to_shell_vec(ecp_shells), ecp_electrons, structure, ao); }), py::arg("name"), py::arg("shells"), py::arg("ecp_name"), py::arg("ecp_shells"), py::arg("ecp_electrons"), py::arg("structure"), @@ -1484,7 +1482,7 @@ Name used for custom ECP basis sets. Type: str )"); - + // Data type name class attribute basis_set.attr("_data_type_name") = DATACLASS_TO_SNAKE_CASE(BasisSet); } From ab16ac80ac00b80703078c4c3e076f96086d80a1 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Fri, 1 May 2026 23:43:08 +0000 Subject: [PATCH 16/33] AI commments Co-authored-by: Copilot --- cpp/src/qdk/chemistry/data/basis_set.cpp | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index 8b8975226..6c470eaac 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -1973,12 +1973,13 @@ void BasisSet::to_hdf5(H5::Group& group) const { } // Save ECP name and electrons if present - if (has_ecp_electrons() || !_ecp_name.empty()) { + if (!_ecp_name.empty()) { // Save ECP name as attribute H5::Attribute ecp_name_attr = group.createAttribute("ecp_name", string_type, scalar_space); ecp_name_attr.write(string_type, _ecp_name); - + } + if (has_ecp_electrons()) { // Save ECP electrons array as dataset if (!_ecp_electrons.empty()) { hsize_t ecp_dims[1] = {_ecp_electrons.size()}; @@ -2416,10 +2417,12 @@ std::shared_ptr BasisSet::from_hdf5(H5::Group& group) { H5::Group structure_group = group.openGroup("structure"); auto structure = Structure::from_hdf5(structure_group); if (!aux_shells.empty()) { - // Aux exists: use full constructor; ecp params may be empty + // Aux exists: use full constructor; treat empty ecp_name as absent. + const std::string normalized_ecp_name = + ecp_name.empty() ? "none" : ecp_name; basis_set = std::make_shared( - name, shells, ecp_name, ecp_shells, ecp_electrons, aux_name, - aux_shells, *structure, atomic_orbital_type); + name, shells, normalized_ecp_name, ecp_shells, ecp_electrons, + aux_name, aux_shells, *structure, atomic_orbital_type); } else if (!ecp_shells.empty()) { if (!ecp_name.empty() && !ecp_electrons.empty()) { basis_set = std::make_shared( @@ -2557,10 +2560,10 @@ nlohmann::json BasisSet::to_json() const { } } - if (has_ecp_electrons() || !_ecp_name.empty()) { - j["ecp_name"] = _ecp_name; - j["ecp_electrons"] = _ecp_electrons; - } + if (has_ecp_electrons() || !_ecp_name != "none")) { + j["ecp_name"] = _ecp_name; + j["ecp_electrons"] = _ecp_electrons; + } if (has_aux_basis() || !_aux_name.empty()) { j["aux_name"] = _aux_name; From 14748611ba9cc7f050f9c0bba3a0cd093aed81e0 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Mon, 4 May 2026 04:35:01 +0000 Subject: [PATCH 17/33] update how to decide if shell is ecp or aux Co-authored-by: Copilot --- cpp/include/qdk/chemistry/data/basis_set.hpp | 2 +- cpp/src/qdk/chemistry/data/basis_set.cpp | 23 +++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/cpp/include/qdk/chemistry/data/basis_set.hpp b/cpp/include/qdk/chemistry/data/basis_set.hpp index 1afd5344c..2bb88a5db 100644 --- a/cpp/include/qdk/chemistry/data/basis_set.hpp +++ b/cpp/include/qdk/chemistry/data/basis_set.hpp @@ -139,7 +139,7 @@ struct Shell { /** * @brief Check if this shell has radial powers (i.e., is an ECP shell) */ - bool has_radial_powers() const { return rpowers.size() > 0 && rpowers.any(); } + bool has_radial_powers() const { return rpowers.size() > 0; } /** * @brief Get number of atomic orbitals in this shell diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index 6c470eaac..f988e183a 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -189,23 +189,19 @@ get_basis_for_nuclear_charge(const double nuclear_charge, size_t am_size = shell["angular_momentum"].size(); size_t momentum = shell["angular_momentum"][am_size > 1 ? i : 0]; - // fill exponents and coefficients + // fill exponents and coefficients (regular shells have no radial powers) std::vector exponents; std::vector coefficients; - std::vector rpowers; - int power = 0; for (size_t k = 0; k < shell["exponents"].size(); k++) { exponents.push_back( std::stod(shell["exponents"][k].get())); coefficients.push_back( std::stod(shell["coefficients"][i][k].get())); - rpowers.push_back(0); - power++; } // create shell and add to list Shell sh{atom_index, static_cast(momentum), exponents, - coefficients, rpowers}; + coefficients}; shells.push_back(sh); } } @@ -493,7 +489,7 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); } - if ((!ecp_shells.empty() || !ecp_electrons.empty() || !ecp_name.empty()) && + if ((!ecp_shells.empty() || !ecp_electrons.empty() || ecp_name != "none") && ecp_electrons.size() != structure->get_num_atoms()) { throw std::invalid_argument( "ECP electrons vector size must match number of atoms"); @@ -2560,7 +2556,7 @@ nlohmann::json BasisSet::to_json() const { } } - if (has_ecp_electrons() || !_ecp_name != "none")) { + if (has_ecp_electrons() || _ecp_name != "none") { j["ecp_name"] = _ecp_name; j["ecp_electrons"] = _ecp_electrons; } @@ -2832,11 +2828,18 @@ std::shared_ptr BasisSet::from_json(const nlohmann::json& j) { std::shared_ptr basis_set; if (j.contains("structure")) { auto structure = Structure::from_json(j["structure"]); - if (!aux_shells.empty()) { - // Aux exists: use full constructor; ecp params may be empty + bool has_ecp = !ecp_shells.empty() || !ecp_electrons.empty() || + !ecp_name.empty(); + if (!aux_shells.empty() && has_ecp) { + // Both aux and ECP present: use full 8-arg constructor basis_set = std::make_shared( name, shells, ecp_name, ecp_shells, ecp_electrons, aux_name, aux_shells, *structure, atomic_orbital_type); + } else if (!aux_shells.empty()) { + // Aux only + basis_set = std::make_shared( + name, shells, aux_name, aux_shells, *structure, + atomic_orbital_type); } else if (!ecp_shells.empty()) { if (!ecp_name.empty() && !ecp_electrons.empty()) { basis_set = std::make_shared( From c0e0eff43bd63e8929114938fde6602ed3ed39ab Mon Sep 17 00:00:00 2001 From: rainli323 Date: Mon, 4 May 2026 04:35:48 +0000 Subject: [PATCH 18/33] pre-commit --- cpp/src/qdk/chemistry/data/basis_set.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index f988e183a..03ab31df3 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -2557,9 +2557,9 @@ nlohmann::json BasisSet::to_json() const { } if (has_ecp_electrons() || _ecp_name != "none") { - j["ecp_name"] = _ecp_name; - j["ecp_electrons"] = _ecp_electrons; - } + j["ecp_name"] = _ecp_name; + j["ecp_electrons"] = _ecp_electrons; + } if (has_aux_basis() || !_aux_name.empty()) { j["aux_name"] = _aux_name; @@ -2828,8 +2828,8 @@ std::shared_ptr BasisSet::from_json(const nlohmann::json& j) { std::shared_ptr basis_set; if (j.contains("structure")) { auto structure = Structure::from_json(j["structure"]); - bool has_ecp = !ecp_shells.empty() || !ecp_electrons.empty() || - !ecp_name.empty(); + bool has_ecp = + !ecp_shells.empty() || !ecp_electrons.empty() || !ecp_name.empty(); if (!aux_shells.empty() && has_ecp) { // Both aux and ECP present: use full 8-arg constructor basis_set = std::make_shared( @@ -2837,9 +2837,9 @@ std::shared_ptr BasisSet::from_json(const nlohmann::json& j) { aux_shells, *structure, atomic_orbital_type); } else if (!aux_shells.empty()) { // Aux only - basis_set = std::make_shared( - name, shells, aux_name, aux_shells, *structure, - atomic_orbital_type); + basis_set = + std::make_shared(name, shells, aux_name, aux_shells, + *structure, atomic_orbital_type); } else if (!ecp_shells.empty()) { if (!ecp_name.empty() && !ecp_electrons.empty()) { basis_set = std::make_shared( From dbc2755f18823257880e0df4e02bf368cbfe60f0 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Mon, 4 May 2026 05:08:57 +0000 Subject: [PATCH 19/33] ai comment --- cpp/src/qdk/chemistry/data/basis_set.cpp | 84 ++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index 03ab31df3..2092b704d 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -331,9 +331,17 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); } + const size_t num_atoms = structure->get_num_atoms(); + // Organize shells by atom index for (const auto& shell : shells) { size_t atom_index = shell.atom_index; + if (atom_index >= num_atoms) { + throw std::invalid_argument("Shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); + } // Ensure we have enough space for this atom if (atom_index >= _shells_per_atom.size()) { @@ -375,9 +383,17 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); } + const size_t num_atoms = structure->get_num_atoms(); + // Organize shells by atom index for (const auto& shell : shells) { size_t atom_index = shell.atom_index; + if (atom_index >= num_atoms) { + throw std::invalid_argument("Shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); + } // Ensure we have enough space for this atom if (atom_index >= _shells_per_atom.size()) { @@ -390,6 +406,12 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, // Organize ECP shells by atom index for (const auto& ecp_shell : ecp_shells) { size_t atom_index = ecp_shell.atom_index; + if (atom_index >= num_atoms) { + throw std::invalid_argument("ECP shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); + } // Ensure we have enough space for this atom if (atom_index >= _ecp_shells_per_atom.size()) { @@ -425,9 +447,17 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); } + const size_t num_atoms = structure->get_num_atoms(); + // Organize shells by atom index for (const auto& shell : shells) { size_t atom_index = shell.atom_index; + if (atom_index >= num_atoms) { + throw std::invalid_argument("Shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); + } // Ensure we have enough space for this atom if (atom_index >= _shells_per_atom.size()) { @@ -446,6 +476,12 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, "shells and ECP electrons."); } size_t atom_index = aux_shell.atom_index; + if (atom_index >= num_atoms) { + throw std::invalid_argument("Auxiliary shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); + } // Ensure we have enough space for this atom if (atom_index >= _aux_shells_per_atom.size()) { @@ -495,9 +531,17 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, "ECP electrons vector size must match number of atoms"); } + const size_t num_atoms = structure->get_num_atoms(); + // Organize shells by atom index for (const auto& shell : shells) { size_t atom_index = shell.atom_index; + if (atom_index >= num_atoms) { + throw std::invalid_argument("Shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); + } // Ensure we have enough space for this atom if (atom_index >= _shells_per_atom.size()) { @@ -510,6 +554,12 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, // Organize ECP shells by atom index for (const auto& ecp_shell : ecp_shells) { size_t atom_index = ecp_shell.atom_index; + if (atom_index >= num_atoms) { + throw std::invalid_argument("ECP shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); + } // Ensure we have enough space for this atom if (atom_index >= _ecp_shells_per_atom.size()) { @@ -548,9 +598,17 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); } + const size_t num_atoms = structure->get_num_atoms(); + // Organize shells by atom index for (const auto& shell : shells) { size_t atom_index = shell.atom_index; + if (atom_index >= num_atoms) { + throw std::invalid_argument("Shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); + } // Ensure we have enough space for this atom if (atom_index >= _shells_per_atom.size()) { @@ -569,6 +627,12 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, "shells and ECP electrons."); } size_t atom_index = aux_shell.atom_index; + if (atom_index >= num_atoms) { + throw std::invalid_argument("Auxiliary shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); + } // Ensure we have enough space for this atom if (atom_index >= _aux_shells_per_atom.size()) { @@ -623,9 +687,17 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, "ECP electrons vector size must match number of atoms"); } + const size_t num_atoms = structure->get_num_atoms(); + // Organize shells by atom index for (const auto& shell : shells) { size_t atom_index = shell.atom_index; + if (atom_index >= num_atoms) { + throw std::invalid_argument("Shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); + } if (atom_index >= _shells_per_atom.size()) { _shells_per_atom.resize(atom_index + 1); } @@ -635,6 +707,12 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, // Organize ECP shells by atom index for (const auto& ecp_shell : ecp_shells) { size_t atom_index = ecp_shell.atom_index; + if (atom_index >= num_atoms) { + throw std::invalid_argument("ECP shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); + } if (atom_index >= _ecp_shells_per_atom.size()) { _ecp_shells_per_atom.resize(atom_index + 1); } @@ -650,6 +728,12 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, "shells and ECP electrons."); } size_t atom_index = aux_shell.atom_index; + if (atom_index >= num_atoms) { + throw std::invalid_argument("Auxiliary shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); + } if (atom_index >= _aux_shells_per_atom.size()) { _aux_shells_per_atom.resize(atom_index + 1); } From f0cb296888e33d7e751bba1b8ab56be63795be67 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Mon, 4 May 2026 06:12:59 +0000 Subject: [PATCH 20/33] ai comments Co-authored-by: Copilot --- cpp/src/qdk/chemistry/data/basis_set.cpp | 102 ++++++++++++----------- python/tests/test_basis_set.py | 18 ++++ 2 files changed, 71 insertions(+), 49 deletions(-) diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index 2092b704d..e70fea780 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -337,10 +337,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, for (const auto& shell : shells) { size_t atom_index = shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument("Shell atom_index (" + - std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument( + "Shell atom_index (" + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } // Ensure we have enough space for this atom @@ -389,10 +389,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, for (const auto& shell : shells) { size_t atom_index = shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument("Shell atom_index (" + - std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument( + "Shell atom_index (" + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } // Ensure we have enough space for this atom @@ -407,10 +407,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, for (const auto& ecp_shell : ecp_shells) { size_t atom_index = ecp_shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument("ECP shell atom_index (" + - std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument( + "ECP shell atom_index (" + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } // Ensure we have enough space for this atom @@ -453,10 +453,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, for (const auto& shell : shells) { size_t atom_index = shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument("Shell atom_index (" + - std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument( + "Shell atom_index (" + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } // Ensure we have enough space for this atom @@ -477,10 +477,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, } size_t atom_index = aux_shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument("Auxiliary shell atom_index (" + - std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument( + "Auxiliary shell atom_index (" + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } // Ensure we have enough space for this atom @@ -537,10 +537,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, for (const auto& shell : shells) { size_t atom_index = shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument("Shell atom_index (" + - std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument( + "Shell atom_index (" + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } // Ensure we have enough space for this atom @@ -555,10 +555,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, for (const auto& ecp_shell : ecp_shells) { size_t atom_index = ecp_shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument("ECP shell atom_index (" + - std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument( + "ECP shell atom_index (" + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } // Ensure we have enough space for this atom @@ -604,10 +604,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, for (const auto& shell : shells) { size_t atom_index = shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument("Shell atom_index (" + - std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument( + "Shell atom_index (" + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } // Ensure we have enough space for this atom @@ -628,10 +628,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, } size_t atom_index = aux_shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument("Auxiliary shell atom_index (" + - std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument( + "Auxiliary shell atom_index (" + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } // Ensure we have enough space for this atom @@ -693,10 +693,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, for (const auto& shell : shells) { size_t atom_index = shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument("Shell atom_index (" + - std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument( + "Shell atom_index (" + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } if (atom_index >= _shells_per_atom.size()) { _shells_per_atom.resize(atom_index + 1); @@ -708,10 +708,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, for (const auto& ecp_shell : ecp_shells) { size_t atom_index = ecp_shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument("ECP shell atom_index (" + - std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument( + "ECP shell atom_index (" + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } if (atom_index >= _ecp_shells_per_atom.size()) { _ecp_shells_per_atom.resize(atom_index + 1); @@ -729,10 +729,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, } size_t atom_index = aux_shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument("Auxiliary shell atom_index (" + - std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument( + "Auxiliary shell atom_index (" + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } if (atom_index >= _aux_shells_per_atom.size()) { _aux_shells_per_atom.resize(atom_index + 1); @@ -1313,7 +1313,11 @@ std::vector BasisSet::get_aux_shells() const { const std::vector& BasisSet::get_aux_shells_for_atom( size_t atom_index) const { QDK_LOG_TRACE_ENTERING(); - _validate_atom_index(atom_index); + if (atom_index >= _aux_shells_per_atom.size()) { + throw std::out_of_range("Atom index " + std::to_string(atom_index) + + " is out of range. Maximum index: " + + std::to_string(_aux_shells_per_atom.size() - 1)); + } if (atom_index >= _aux_shells_per_atom.size()) { static const std::vector empty_vector; return empty_vector; diff --git a/python/tests/test_basis_set.py b/python/tests/test_basis_set.py index a145e4c8f..9e26747ec 100644 --- a/python/tests/test_basis_set.py +++ b/python/tests/test_basis_set.py @@ -1469,6 +1469,13 @@ def test_basis_set_from_element_map(): num_orbitals = determinant.get_orbitals().get_num_molecular_orbitals() assert num_orbitals == 15 + # Test from_element_map with auxiliary basis set + element_aux_map = {"H": "def2-universal-jfit", "O": "def2-universal-jfit"} + basis_with_aux = BasisSet.from_element_map(element_basis_map, element_aux_map, structure) + assert basis_with_aux.get_name() == "custom_basis_set" + assert basis_with_aux.has_aux_basis() + assert basis_with_aux.get_num_aux_shells() > 0 + def test_basis_set_from_index_map(): """Test creating basis set using from_index_map static method.""" @@ -1495,6 +1502,17 @@ def test_basis_set_from_index_map(): num_orbitals = determinant.get_orbitals().get_num_molecular_orbitals() assert num_orbitals == 24 + # Test from_index_map with auxiliary basis set + index_aux_map = { + 0: "def2-universal-jfit", + 1: "def2-universal-jfit", + 2: "def2-universal-jfit", + } + basis_with_aux = BasisSet.from_index_map(index_basis_map, index_aux_map, structure) + assert basis_with_aux.get_name() == "custom_basis_set" + assert basis_with_aux.has_aux_basis() + assert basis_with_aux.get_num_aux_shells() > 0 + def test_basis_set_static_constants(): """Test that static constant variables are accessible.""" From 9fdc16a3921e27a35be80b50fa7c2a82f0d25cb8 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Mon, 4 May 2026 06:14:04 +0000 Subject: [PATCH 21/33] pre-commit --- cpp/src/qdk/chemistry/data/basis_set.cpp | 98 ++++++++++++------------ 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index e70fea780..c3b8da924 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -337,10 +337,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, for (const auto& shell : shells) { size_t atom_index = shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument( - "Shell atom_index (" + std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument("Shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } // Ensure we have enough space for this atom @@ -389,10 +389,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, for (const auto& shell : shells) { size_t atom_index = shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument( - "Shell atom_index (" + std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument("Shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } // Ensure we have enough space for this atom @@ -407,10 +407,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, for (const auto& ecp_shell : ecp_shells) { size_t atom_index = ecp_shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument( - "ECP shell atom_index (" + std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument("ECP shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } // Ensure we have enough space for this atom @@ -453,10 +453,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, for (const auto& shell : shells) { size_t atom_index = shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument( - "Shell atom_index (" + std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument("Shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } // Ensure we have enough space for this atom @@ -477,10 +477,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, } size_t atom_index = aux_shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument( - "Auxiliary shell atom_index (" + std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument("Auxiliary shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } // Ensure we have enough space for this atom @@ -537,10 +537,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, for (const auto& shell : shells) { size_t atom_index = shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument( - "Shell atom_index (" + std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument("Shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } // Ensure we have enough space for this atom @@ -555,10 +555,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, for (const auto& ecp_shell : ecp_shells) { size_t atom_index = ecp_shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument( - "ECP shell atom_index (" + std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument("ECP shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } // Ensure we have enough space for this atom @@ -604,10 +604,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, for (const auto& shell : shells) { size_t atom_index = shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument( - "Shell atom_index (" + std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument("Shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } // Ensure we have enough space for this atom @@ -628,10 +628,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, } size_t atom_index = aux_shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument( - "Auxiliary shell atom_index (" + std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument("Auxiliary shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } // Ensure we have enough space for this atom @@ -693,10 +693,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, for (const auto& shell : shells) { size_t atom_index = shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument( - "Shell atom_index (" + std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument("Shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } if (atom_index >= _shells_per_atom.size()) { _shells_per_atom.resize(atom_index + 1); @@ -708,10 +708,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, for (const auto& ecp_shell : ecp_shells) { size_t atom_index = ecp_shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument( - "ECP shell atom_index (" + std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument("ECP shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } if (atom_index >= _ecp_shells_per_atom.size()) { _ecp_shells_per_atom.resize(atom_index + 1); @@ -729,10 +729,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, } size_t atom_index = aux_shell.atom_index; if (atom_index >= num_atoms) { - throw std::invalid_argument( - "Auxiliary shell atom_index (" + std::to_string(atom_index) + - ") is out of range for structure with " + - std::to_string(num_atoms) + " atoms"); + throw std::invalid_argument("Auxiliary shell atom_index (" + + std::to_string(atom_index) + + ") is out of range for structure with " + + std::to_string(num_atoms) + " atoms"); } if (atom_index >= _aux_shells_per_atom.size()) { _aux_shells_per_atom.resize(atom_index + 1); @@ -1313,7 +1313,7 @@ std::vector BasisSet::get_aux_shells() const { const std::vector& BasisSet::get_aux_shells_for_atom( size_t atom_index) const { QDK_LOG_TRACE_ENTERING(); - if (atom_index >= _aux_shells_per_atom.size()) { + if (atom_index >= _aux_shells_per_atom.size()) { throw std::out_of_range("Atom index " + std::to_string(atom_index) + " is out of range. Maximum index: " + std::to_string(_aux_shells_per_atom.size() - 1)); From dcb8d97b876b27e13c13868def391bc41a5df5b1 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Mon, 4 May 2026 21:24:31 +0000 Subject: [PATCH 22/33] change default of ecp name Co-authored-by: Copilot --- cpp/src/qdk/chemistry/data/basis_set.cpp | 54 ++++++++++++++---------- cpp/tests/test_basis_set.cpp | 4 +- python/tests/test_basis_set.py | 2 +- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index c3b8da924..f3d16fd51 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -320,8 +320,7 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, AOType atomic_orbital_type) : _name(name), _atomic_orbital_type(atomic_orbital_type), - _structure(structure), - _ecp_name("none") { + _structure(structure) { QDK_LOG_TRACE_ENTERING(); if (_name.empty()) { throw std::invalid_argument("BasisSet name cannot be empty"); @@ -376,7 +375,6 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, : _name(name), _atomic_orbital_type(atomic_orbital_type), _structure(structure), - _ecp_name("none"), _ecp_electrons(ecp_electrons) { QDK_LOG_TRACE_ENTERING(); if (!structure) { @@ -385,6 +383,11 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, const size_t num_atoms = structure->get_num_atoms(); + if (ecp_electrons.size() != num_atoms) { + throw std::invalid_argument( + "ECP electrons vector size must match number of atoms"); + } + // Organize shells by atom index for (const auto& shell : shells) { size_t atom_index = shell.atom_index; @@ -440,8 +443,7 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, AOType atomic_orbital_type) : _name(name), _atomic_orbital_type(atomic_orbital_type), - _structure(structure), - _ecp_name("none") { + _structure(structure) { QDK_LOG_TRACE_ENTERING(); if (!structure) { throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); @@ -525,12 +527,16 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); } - if ((!ecp_shells.empty() || !ecp_electrons.empty() || ecp_name != "none") && + if ((!ecp_shells.empty() || !ecp_electrons.empty() || !ecp_name.empty()) && ecp_electrons.size() != structure->get_num_atoms()) { throw std::invalid_argument( "ECP electrons vector size must match number of atoms"); } + if (_ecp_electrons.empty()) { + _ecp_electrons.resize(structure->get_num_atoms(), 0); + } + const size_t num_atoms = structure->get_num_atoms(); // Organize shells by atom index @@ -591,8 +597,7 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, : _name(name), _atomic_orbital_type(atomic_orbital_type), _structure(structure), - _aux_name(aux_name), - _ecp_name("none") { + _aux_name(aux_name) { QDK_LOG_TRACE_ENTERING(); if (!structure) { throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); @@ -681,12 +686,16 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); } - if ((!ecp_shells.empty() || !ecp_electrons.empty() || ecp_name != "none") && + if ((!ecp_shells.empty() || !ecp_electrons.empty() || !ecp_name.empty()) && ecp_electrons.size() != structure->get_num_atoms()) { throw std::invalid_argument( "ECP electrons vector size must match number of atoms"); } + if (_ecp_electrons.empty()) { + _ecp_electrons.resize(structure->get_num_atoms(), 0); + } + const size_t num_atoms = structure->get_num_atoms(); // Organize shells by atom index @@ -2492,29 +2501,28 @@ std::shared_ptr BasisSet::from_hdf5(H5::Group& group) { aux_name_attr.read(string_type, aux_name); } - // Construct BasisSet handling all combinations: - // structure: present or absent - // ecp: present (with or without metadata) or absent - // aux: present or absent std::shared_ptr basis_set; if (group.nameExists("structure")) { H5::Group structure_group = group.openGroup("structure"); auto structure = Structure::from_hdf5(structure_group); if (!aux_shells.empty()) { // Aux exists: use full constructor; treat empty ecp_name as absent. - const std::string normalized_ecp_name = - ecp_name.empty() ? "none" : ecp_name; basis_set = std::make_shared( - name, shells, normalized_ecp_name, ecp_shells, ecp_electrons, - aux_name, aux_shells, *structure, atomic_orbital_type); + name, shells, ecp_name, ecp_shells, ecp_electrons, aux_name, + aux_shells, *structure, atomic_orbital_type); } else if (!ecp_shells.empty()) { - if (!ecp_name.empty() && !ecp_electrons.empty()) { + if (ecp_electrons.empty()) { + throw std::runtime_error( + "ECP electrons data is missing but ECP shells are present"); + } + if (!ecp_name.empty()) { basis_set = std::make_shared( name, shells, ecp_name, ecp_shells, ecp_electrons, *structure, atomic_orbital_type); } else { - basis_set = std::make_shared( - name, shells, ecp_shells, *structure, atomic_orbital_type); + basis_set = std::make_shared(name, shells, ecp_shells, + ecp_electrons, *structure, + atomic_orbital_type); } } else { basis_set = std::make_shared(name, shells, *structure, @@ -2644,7 +2652,7 @@ nlohmann::json BasisSet::to_json() const { } } - if (has_ecp_electrons() || _ecp_name != "none") { + if (has_ecp_electrons() || !_ecp_name.empty()) { j["ecp_name"] = _ecp_name; j["ecp_electrons"] = _ecp_electrons; } @@ -2898,8 +2906,10 @@ std::shared_ptr BasisSet::from_json(const nlohmann::json& j) { // Load ECP name and electrons if present std::string ecp_name; std::vector ecp_electrons; - if (j.contains("ecp_name") && j.contains("ecp_electrons")) { + if (j.contains("ecp_name")) { ecp_name = j["ecp_name"]; + } + if (j.contains("ecp_electrons")) { ecp_electrons = j["ecp_electrons"].get>(); } diff --git a/cpp/tests/test_basis_set.cpp b/cpp/tests/test_basis_set.cpp index 142e484e9..a28d0e5af 100644 --- a/cpp/tests/test_basis_set.cpp +++ b/cpp/tests/test_basis_set.cpp @@ -545,7 +545,7 @@ TEST_F(BasisSetTest, ECPDefaultInitialization) { // Check default ECP values EXPECT_FALSE(basis.has_ecp_electrons()); - EXPECT_EQ("none", basis.get_ecp_name()); + EXPECT_EQ("", basis.get_ecp_name()); EXPECT_EQ(2u, basis.get_ecp_electrons().size()); EXPECT_EQ(0u, basis.get_ecp_electrons()[0]); EXPECT_EQ(0u, basis.get_ecp_electrons()[1]); @@ -835,7 +835,7 @@ TEST_F(BasisSetTest, ECPJSONSerialization) { auto json_no_ecp = basis_without_ecp.to_json(); auto loaded_no_ecp = BasisSet::from_json(json_no_ecp); EXPECT_FALSE(loaded_no_ecp->has_ecp_electrons()); - EXPECT_EQ("none", loaded_no_ecp->get_ecp_name()); + EXPECT_EQ("", loaded_no_ecp->get_ecp_name()); EXPECT_FALSE(loaded_no_ecp->has_ecp_shells()); EXPECT_EQ(0u, loaded_no_ecp->get_num_ecp_shells()); } diff --git a/python/tests/test_basis_set.py b/python/tests/test_basis_set.py index 9e26747ec..d67035cfd 100644 --- a/python/tests/test_basis_set.py +++ b/python/tests/test_basis_set.py @@ -1149,7 +1149,7 @@ def test_basis_set_ecp_functionality(): # Test default ECP state assert not basis.has_ecp_electrons() - assert basis.get_ecp_name() == "none" + assert basis.get_ecp_name() == "" assert basis.get_ecp_electrons() == [0, 0, 0] # Test creating ECP with constructor From 5133c1b36dadb622c67587b42b297b98628b81dd Mon Sep 17 00:00:00 2001 From: rainli323 Date: Mon, 4 May 2026 21:31:38 +0000 Subject: [PATCH 23/33] AI comment --- cpp/src/qdk/chemistry/data/basis_set.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index f3d16fd51..0e1d2751d 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -1327,10 +1327,7 @@ const std::vector& BasisSet::get_aux_shells_for_atom( " is out of range. Maximum index: " + std::to_string(_aux_shells_per_atom.size() - 1)); } - if (atom_index >= _aux_shells_per_atom.size()) { - static const std::vector empty_vector; - return empty_vector; - } + return _aux_shells_per_atom[atom_index]; } From ede6dbf502ce4c494a6554f2d4ef627072d0c087 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Mon, 4 May 2026 21:46:56 +0000 Subject: [PATCH 24/33] AI comments Co-authored-by: Copilot --- cpp/src/qdk/chemistry/data/basis_set.cpp | 6 ++++++ python/tests/test_pyscf_plugin.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index 0e1d2751d..d0a8c83f4 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -2526,6 +2526,12 @@ std::shared_ptr BasisSet::from_hdf5(H5::Group& group) { atomic_orbital_type); } } else { + if (!aux_shells.empty() || !ecp_shells.empty() || + !ecp_electrons.empty() || !ecp_name.empty() || !aux_name.empty()) { + throw std::runtime_error( + "HDF5 BasisSet contains ECP or auxiliary data but no structure; " + "cannot reconstruct without losing information"); + } basis_set = std::make_shared(name, shells, atomic_orbital_type); } diff --git a/python/tests/test_pyscf_plugin.py b/python/tests/test_pyscf_plugin.py index fcad80c4a..7812da483 100644 --- a/python/tests/test_pyscf_plugin.py +++ b/python/tests/test_pyscf_plugin.py @@ -2597,7 +2597,7 @@ def test_ecp_edge_cases(self): assert qdk_basis_no_meta.has_ecp_shells() assert qdk_basis_no_meta.get_num_ecp_shells() == 1 assert qdk_basis_no_meta.has_ecp_electrons() - assert qdk_basis_no_meta.get_ecp_name() == "none" + assert qdk_basis_no_meta.get_ecp_name() == "" # Edge case 2: Full ECP structure format roundtrip pyscf_mol_orig = pyscf.gto.M(atom="Ag 0 0 0", spin=1, basis="lanl2dz", ecp="lanl2dz", verbose=0) From 83f9b91ba04f2b9478769fc01167b582cb054ed4 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Mon, 4 May 2026 22:20:28 +0000 Subject: [PATCH 25/33] bug fix and ai comments Co-authored-by: Copilot --- cpp/src/qdk/chemistry/data/basis_set.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index d0a8c83f4..0c988b961 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -1322,12 +1322,11 @@ std::vector BasisSet::get_aux_shells() const { const std::vector& BasisSet::get_aux_shells_for_atom( size_t atom_index) const { QDK_LOG_TRACE_ENTERING(); + _validate_atom_index(atom_index); if (atom_index >= _aux_shells_per_atom.size()) { - throw std::out_of_range("Atom index " + std::to_string(atom_index) + - " is out of range. Maximum index: " + - std::to_string(_aux_shells_per_atom.size() - 1)); + static const std::vector empty_vector; + return empty_vector; } - return _aux_shells_per_atom[atom_index]; } @@ -2526,8 +2525,11 @@ std::shared_ptr BasisSet::from_hdf5(H5::Group& group) { atomic_orbital_type); } } else { - if (!aux_shells.empty() || !ecp_shells.empty() || - !ecp_electrons.empty() || !ecp_name.empty() || !aux_name.empty()) { + const bool has_real_ecp_electrons = + std::any_of(ecp_electrons.begin(), ecp_electrons.end(), + [](size_t n) { return n != 0; }); + if (!aux_shells.empty() || !ecp_shells.empty() || !aux_name.empty() || + has_real_ecp_electrons) { throw std::runtime_error( "HDF5 BasisSet contains ECP or auxiliary data but no structure; " "cannot reconstruct without losing information"); From f9ee64060154b4444445f26b3d1f95669b8cd1fa Mon Sep 17 00:00:00 2001 From: rainli323 Date: Tue, 5 May 2026 00:03:43 +0000 Subject: [PATCH 26/33] AI commment Co-authored-by: Copilot --- cpp/src/qdk/chemistry/data/basis_set.cpp | 35 +++++++++++++++++++----- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index 0c988b961..7e160885a 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -867,7 +867,12 @@ std::shared_ptr BasisSet::from_basis_name( // sort ecp shells detail::sort_shells_inplace(all_ecp_shells); - return std::make_shared(basis_name, all_basis_shells, basis_name, + const bool has_ecp_data = + !all_ecp_shells.empty() || + std::any_of(all_ecp_electrons.begin(), all_ecp_electrons.end(), + [](size_t electron_count) { return electron_count > 0; }); + const std::string ecp_name = has_ecp_data ? basis_name : ""; + return std::make_shared(basis_name, all_basis_shells, ecp_name, all_ecp_shells, all_ecp_electrons, structure, atomic_orbital_type); } @@ -957,10 +962,15 @@ std::shared_ptr BasisSet::from_index_map( // sort ecp shells detail::sort_shells_inplace(all_ecp_shells); + const bool has_ecp_data = + !all_ecp_shells.empty() || + std::any_of(all_ecp_electrons.begin(), all_ecp_electrons.end(), + [](size_t electron_count) { return electron_count > 0; }); + const std::string ecp_name = + has_ecp_data ? std::string(BasisSet::custom_ecp_name) : std::string(); return std::make_shared( - std::string(BasisSet::custom_name), all_basis_shells, - std::string(BasisSet::custom_ecp_name), all_ecp_shells, all_ecp_electrons, - structure, atomic_orbital_type); + std::string(BasisSet::custom_name), all_basis_shells, ecp_name, + all_ecp_shells, all_ecp_electrons, structure, atomic_orbital_type); } std::shared_ptr BasisSet::from_basis_name( @@ -1019,7 +1029,12 @@ std::shared_ptr BasisSet::from_basis_name( detail::sort_shells_inplace(all_ecp_shells); detail::sort_shells_inplace(all_aux_shells); - return std::make_shared(basis_name, all_basis_shells, basis_name, + const bool has_ecp_data = + !all_ecp_shells.empty() || + std::any_of(all_ecp_electrons.begin(), all_ecp_electrons.end(), + [](size_t electron_count) { return electron_count > 0; }); + const std::string ecp_name = has_ecp_data ? basis_name : ""; + return std::make_shared(basis_name, all_basis_shells, ecp_name, all_ecp_shells, all_ecp_electrons, aux_name_lower, all_aux_shells, structure, atomic_orbital_type); @@ -1139,9 +1154,15 @@ std::shared_ptr BasisSet::from_index_map( detail::sort_shells_inplace(all_ecp_shells); detail::sort_shells_inplace(all_aux_shells); + const bool has_ecp_data = + !all_ecp_shells.empty() || + std::any_of(all_ecp_electrons.begin(), all_ecp_electrons.end(), + [](size_t electron_count) { return electron_count > 0; }); + const std::string ecp_name = + has_ecp_data ? std::string(BasisSet::custom_ecp_name) : std::string(); return std::make_shared( - std::string(BasisSet::custom_name), all_basis_shells, - std::string(BasisSet::custom_ecp_name), all_ecp_shells, all_ecp_electrons, + std::string(BasisSet::custom_name), all_basis_shells, ecp_name, + all_ecp_shells, all_ecp_electrons, std::string(BasisSet::custom_aux_name), all_aux_shells, structure, atomic_orbital_type); } From 34ab4b575ba179beecf01d032c8b337ae0075459 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Tue, 5 May 2026 00:09:32 +0000 Subject: [PATCH 27/33] pre-commit --- cpp/src/qdk/chemistry/data/basis_set.cpp | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index 7e160885a..77310fc1f 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -868,9 +868,9 @@ std::shared_ptr BasisSet::from_basis_name( detail::sort_shells_inplace(all_ecp_shells); const bool has_ecp_data = - !all_ecp_shells.empty() || - std::any_of(all_ecp_electrons.begin(), all_ecp_electrons.end(), - [](size_t electron_count) { return electron_count > 0; }); + !all_ecp_shells.empty() || + std::any_of(all_ecp_electrons.begin(), all_ecp_electrons.end(), + [](size_t electron_count) { return electron_count > 0; }); const std::string ecp_name = has_ecp_data ? basis_name : ""; return std::make_shared(basis_name, all_basis_shells, ecp_name, all_ecp_shells, all_ecp_electrons, @@ -1030,14 +1030,13 @@ std::shared_ptr BasisSet::from_basis_name( detail::sort_shells_inplace(all_aux_shells); const bool has_ecp_data = - !all_ecp_shells.empty() || - std::any_of(all_ecp_electrons.begin(), all_ecp_electrons.end(), - [](size_t electron_count) { return electron_count > 0; }); + !all_ecp_shells.empty() || + std::any_of(all_ecp_electrons.begin(), all_ecp_electrons.end(), + [](size_t electron_count) { return electron_count > 0; }); const std::string ecp_name = has_ecp_data ? basis_name : ""; - return std::make_shared(basis_name, all_basis_shells, ecp_name, - all_ecp_shells, all_ecp_electrons, - aux_name_lower, all_aux_shells, structure, - atomic_orbital_type); + return std::make_shared( + basis_name, all_basis_shells, ecp_name, all_ecp_shells, all_ecp_electrons, + aux_name_lower, all_aux_shells, structure, atomic_orbital_type); } std::shared_ptr BasisSet::from_element_map( @@ -1162,9 +1161,8 @@ std::shared_ptr BasisSet::from_index_map( has_ecp_data ? std::string(BasisSet::custom_ecp_name) : std::string(); return std::make_shared( std::string(BasisSet::custom_name), all_basis_shells, ecp_name, - all_ecp_shells, all_ecp_electrons, - std::string(BasisSet::custom_aux_name), all_aux_shells, structure, - atomic_orbital_type); + all_ecp_shells, all_ecp_electrons, std::string(BasisSet::custom_aux_name), + all_aux_shells, structure, atomic_orbital_type); } BasisSet::BasisSet(const BasisSet& other) From 62116f3cdb2a391a4d9f263b9f7d0de1b8e5ffea Mon Sep 17 00:00:00 2001 From: rainli323 Date: Tue, 5 May 2026 20:07:36 +0000 Subject: [PATCH 28/33] AI comments but regenerate constructors to make consistent. Co-authored-by: Copilot --- cpp/src/qdk/chemistry/data/basis_set.cpp | 145 ++++++++++++----------- 1 file changed, 75 insertions(+), 70 deletions(-) diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index 77310fc1f..f77116062 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -286,7 +286,7 @@ Shell::Shell(size_t atom_idx, OrbitalType orb_type, BasisSet::BasisSet(const std::string& name, const std::vector& shells, AOType atomic_orbital_type) - : _name(name), _atomic_orbital_type(atomic_orbital_type), _ecp_name(name) { + : _name(name), _atomic_orbital_type(atomic_orbital_type) { QDK_LOG_TRACE_ENTERING(); // Organize shells by atom index for (const auto& shell : shells) { @@ -377,6 +377,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, _structure(structure), _ecp_electrons(ecp_electrons) { QDK_LOG_TRACE_ENTERING(); + if (_name.empty()) { + throw std::invalid_argument("BasisSet name cannot be empty"); + } + if (!structure) { throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); } @@ -445,6 +449,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, _atomic_orbital_type(atomic_orbital_type), _structure(structure) { QDK_LOG_TRACE_ENTERING(); + if (_name.empty()) { + throw std::invalid_argument("BasisSet name cannot be empty"); + } + if (!structure) { throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); } @@ -523,6 +531,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, _ecp_name(ecp_name), _ecp_electrons(ecp_electrons) { QDK_LOG_TRACE_ENTERING(); + if (_name.empty()) { + throw std::invalid_argument("BasisSet name cannot be empty"); + } + if (!structure) { throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); } @@ -599,6 +611,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, _structure(structure), _aux_name(aux_name) { QDK_LOG_TRACE_ENTERING(); + if (_name.empty()) { + throw std::invalid_argument("BasisSet name cannot be empty"); + } + if (!structure) { throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); } @@ -682,6 +698,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, _ecp_electrons(ecp_electrons), _aux_name(aux_name) { QDK_LOG_TRACE_ENTERING(); + if (_name.empty()) { + throw std::invalid_argument("BasisSet name cannot be empty"); + } + if (!structure) { throw std::invalid_argument("Structure shared_ptr cannot be nullptr"); } @@ -696,6 +716,10 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, _ecp_electrons.resize(structure->get_num_atoms(), 0); } + if (!ecp_shells.empty() && _ecp_name.empty()) { + _ecp_name = _name; + } + const size_t num_atoms = structure->get_num_atoms(); // Organize shells by atom index @@ -2516,47 +2540,38 @@ std::shared_ptr BasisSet::from_hdf5(H5::Group& group) { aux_name_attr.read(string_type, aux_name); } - std::shared_ptr basis_set; - if (group.nameExists("structure")) { - H5::Group structure_group = group.openGroup("structure"); - auto structure = Structure::from_hdf5(structure_group); - if (!aux_shells.empty()) { - // Aux exists: use full constructor; treat empty ecp_name as absent. - basis_set = std::make_shared( - name, shells, ecp_name, ecp_shells, ecp_electrons, aux_name, - aux_shells, *structure, atomic_orbital_type); - } else if (!ecp_shells.empty()) { - if (ecp_electrons.empty()) { - throw std::runtime_error( - "ECP electrons data is missing but ECP shells are present"); - } - if (!ecp_name.empty()) { - basis_set = std::make_shared( - name, shells, ecp_name, ecp_shells, ecp_electrons, *structure, - atomic_orbital_type); - } else { - basis_set = std::make_shared(name, shells, ecp_shells, - ecp_electrons, *structure, - atomic_orbital_type); - } - } else { - basis_set = std::make_shared(name, shells, *structure, - atomic_orbital_type); - } - } else { + if (!group.nameExists("structure")) { const bool has_real_ecp_electrons = std::any_of(ecp_electrons.begin(), ecp_electrons.end(), [](size_t n) { return n != 0; }); - if (!aux_shells.empty() || !ecp_shells.empty() || !aux_name.empty() || - has_real_ecp_electrons) { + if (!aux_shells.empty() || !ecp_shells.empty() || !ecp_name.empty() || + !aux_name.empty() || has_real_ecp_electrons) { throw std::runtime_error( "HDF5 BasisSet contains ECP or auxiliary data but no structure; " "cannot reconstruct without losing information"); } - basis_set = std::make_shared(name, shells, atomic_orbital_type); + return std::make_shared(name, shells, atomic_orbital_type); } - return basis_set; + if (!ecp_shells.empty() && ecp_electrons.empty()) { + throw std::runtime_error( + "ECP electrons data is missing but ECP shells are present"); + } + + H5::Group structure_group = group.openGroup("structure"); + auto structure = Structure::from_hdf5(structure_group); + const bool has_real_ecp_electrons = + std::any_of(ecp_electrons.begin(), ecp_electrons.end(), + [](size_t n) { return n != 0; }); + const std::string effective_ecp_name = + (!ecp_shells.empty() || has_real_ecp_electrons) ? ecp_name + : std::string(); + const std::string effective_aux_name = + !aux_shells.empty() ? aux_name : std::string(); + + return std::make_shared( + name, shells, effective_ecp_name, ecp_shells, ecp_electrons, + effective_aux_name, aux_shells, *structure, atomic_orbital_type); } catch (const H5::Exception& e) { throw std::runtime_error("HDF5 error: " + std::string(e.getCDetailMsg())); @@ -2943,47 +2958,37 @@ std::shared_ptr BasisSet::from_json(const nlohmann::json& j) { aux_name = j["aux_name"]; } - // Construct BasisSet handling all combinations: - // structure: present or absent - // ecp: present (with or without metadata) or absent - // aux: present or absent - std::shared_ptr basis_set; - if (j.contains("structure")) { - auto structure = Structure::from_json(j["structure"]); - bool has_ecp = - !ecp_shells.empty() || !ecp_electrons.empty() || !ecp_name.empty(); - if (!aux_shells.empty() && has_ecp) { - // Both aux and ECP present: use full 8-arg constructor - basis_set = std::make_shared( - name, shells, ecp_name, ecp_shells, ecp_electrons, aux_name, - aux_shells, *structure, atomic_orbital_type); - } else if (!aux_shells.empty()) { - // Aux only - basis_set = - std::make_shared(name, shells, aux_name, aux_shells, - *structure, atomic_orbital_type); - } else if (!ecp_shells.empty()) { - if (!ecp_name.empty() && !ecp_electrons.empty()) { - basis_set = std::make_shared( - name, shells, ecp_name, ecp_shells, ecp_electrons, *structure, - atomic_orbital_type); - } else { - basis_set = std::make_shared( - name, shells, ecp_shells, *structure, atomic_orbital_type); - } - } else { - basis_set = std::make_shared(name, shells, *structure, - atomic_orbital_type); - } - } else { - if (!ecp_shells.empty()) { + if (!j.contains("structure")) { + const bool has_real_ecp_electrons = + std::any_of(ecp_electrons.begin(), ecp_electrons.end(), + [](size_t n) { return n != 0; }); + if (!aux_shells.empty() || !aux_name.empty() || !ecp_shells.empty() || + !ecp_name.empty() || has_real_ecp_electrons) { throw std::runtime_error( - "Cannot create BasisSet with ECP shells but without structure"); + "Cannot create BasisSet with ECP or auxiliary basis data but " + "without structure"); } - basis_set = std::make_shared(name, shells, atomic_orbital_type); + return std::make_shared(name, shells, atomic_orbital_type); } - return basis_set; + if (!ecp_shells.empty() && ecp_electrons.empty()) { + throw std::runtime_error( + "ECP electrons data is missing but ECP shells are present"); + } + + auto structure = Structure::from_json(j["structure"]); + const bool has_real_ecp_electrons = + std::any_of(ecp_electrons.begin(), ecp_electrons.end(), + [](size_t n) { return n != 0; }); + const std::string effective_ecp_name = + (!ecp_shells.empty() || has_real_ecp_electrons) ? ecp_name + : std::string(); + const std::string effective_aux_name = + !aux_shells.empty() ? aux_name : std::string(); + + return std::make_shared( + name, shells, effective_ecp_name, ecp_shells, ecp_electrons, + effective_aux_name, aux_shells, *structure, atomic_orbital_type); } catch (const std::exception& e) { throw std::runtime_error("Failed to parse BasisSet from JSON: " + From 056e90021d8dabfff632021e57096107f0630e34 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Tue, 5 May 2026 20:14:23 +0000 Subject: [PATCH 29/33] pre-commit --- cpp/src/qdk/chemistry/data/basis_set.cpp | 78 ++++++++++++------------ 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index f77116062..51b54d09f 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -2544,34 +2544,34 @@ std::shared_ptr BasisSet::from_hdf5(H5::Group& group) { const bool has_real_ecp_electrons = std::any_of(ecp_electrons.begin(), ecp_electrons.end(), [](size_t n) { return n != 0; }); - if (!aux_shells.empty() || !ecp_shells.empty() || !ecp_name.empty() || + if (!aux_shells.empty() || !ecp_shells.empty() || !ecp_name.empty() || !aux_name.empty() || has_real_ecp_electrons) { throw std::runtime_error( "HDF5 BasisSet contains ECP or auxiliary data but no structure; " "cannot reconstruct without losing information"); } - return std::make_shared(name, shells, atomic_orbital_type); + return std::make_shared(name, shells, atomic_orbital_type); } - if (!ecp_shells.empty() && ecp_electrons.empty()) { - throw std::runtime_error( - "ECP electrons data is missing but ECP shells are present"); - } + if (!ecp_shells.empty() && ecp_electrons.empty()) { + throw std::runtime_error( + "ECP electrons data is missing but ECP shells are present"); + } - H5::Group structure_group = group.openGroup("structure"); - auto structure = Structure::from_hdf5(structure_group); - const bool has_real_ecp_electrons = - std::any_of(ecp_electrons.begin(), ecp_electrons.end(), - [](size_t n) { return n != 0; }); - const std::string effective_ecp_name = - (!ecp_shells.empty() || has_real_ecp_electrons) ? ecp_name - : std::string(); - const std::string effective_aux_name = - !aux_shells.empty() ? aux_name : std::string(); + H5::Group structure_group = group.openGroup("structure"); + auto structure = Structure::from_hdf5(structure_group); + const bool has_real_ecp_electrons = + std::any_of(ecp_electrons.begin(), ecp_electrons.end(), + [](size_t n) { return n != 0; }); + const std::string effective_ecp_name = + (!ecp_shells.empty() || has_real_ecp_electrons) ? ecp_name + : std::string(); + const std::string effective_aux_name = + !aux_shells.empty() ? aux_name : std::string(); - return std::make_shared( - name, shells, effective_ecp_name, ecp_shells, ecp_electrons, - effective_aux_name, aux_shells, *structure, atomic_orbital_type); + return std::make_shared( + name, shells, effective_ecp_name, ecp_shells, ecp_electrons, + effective_aux_name, aux_shells, *structure, atomic_orbital_type); } catch (const H5::Exception& e) { throw std::runtime_error("HDF5 error: " + std::string(e.getCDetailMsg())); @@ -2962,33 +2962,33 @@ std::shared_ptr BasisSet::from_json(const nlohmann::json& j) { const bool has_real_ecp_electrons = std::any_of(ecp_electrons.begin(), ecp_electrons.end(), [](size_t n) { return n != 0; }); - if (!aux_shells.empty() || !aux_name.empty() || !ecp_shells.empty() || + if (!aux_shells.empty() || !aux_name.empty() || !ecp_shells.empty() || !ecp_name.empty() || has_real_ecp_electrons) { throw std::runtime_error( "Cannot create BasisSet with ECP or auxiliary basis data but " "without structure"); } - return std::make_shared(name, shells, atomic_orbital_type); + return std::make_shared(name, shells, atomic_orbital_type); } - if (!ecp_shells.empty() && ecp_electrons.empty()) { - throw std::runtime_error( - "ECP electrons data is missing but ECP shells are present"); - } - - auto structure = Structure::from_json(j["structure"]); - const bool has_real_ecp_electrons = - std::any_of(ecp_electrons.begin(), ecp_electrons.end(), - [](size_t n) { return n != 0; }); - const std::string effective_ecp_name = - (!ecp_shells.empty() || has_real_ecp_electrons) ? ecp_name - : std::string(); - const std::string effective_aux_name = - !aux_shells.empty() ? aux_name : std::string(); - - return std::make_shared( - name, shells, effective_ecp_name, ecp_shells, ecp_electrons, - effective_aux_name, aux_shells, *structure, atomic_orbital_type); + if (!ecp_shells.empty() && ecp_electrons.empty()) { + throw std::runtime_error( + "ECP electrons data is missing but ECP shells are present"); + } + + auto structure = Structure::from_json(j["structure"]); + const bool has_real_ecp_electrons = + std::any_of(ecp_electrons.begin(), ecp_electrons.end(), + [](size_t n) { return n != 0; }); + const std::string effective_ecp_name = + (!ecp_shells.empty() || has_real_ecp_electrons) ? ecp_name + : std::string(); + const std::string effective_aux_name = + !aux_shells.empty() ? aux_name : std::string(); + + return std::make_shared( + name, shells, effective_ecp_name, ecp_shells, ecp_electrons, + effective_aux_name, aux_shells, *structure, atomic_orbital_type); } catch (const std::exception& e) { throw std::runtime_error("Failed to parse BasisSet from JSON: " + From 766797b5031b854c2dba069cc953a405ac2d7e45 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Tue, 5 May 2026 22:04:00 +0000 Subject: [PATCH 30/33] AI comments Co-authored-by: Copilot --- cpp/src/qdk/chemistry/data/basis_set.cpp | 2 +- python/src/pybind11/data/basis_set.cpp | 103 ++++++++++++++--------- python/tests/test_basis_set.py | 5 +- 3 files changed, 68 insertions(+), 42 deletions(-) diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index 51b54d09f..4307f050e 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -481,7 +481,7 @@ BasisSet::BasisSet(const std::string& name, const std::vector& shells, for (const auto& aux_shell : aux_shells) { if (aux_shell.has_radial_powers()) { throw std::invalid_argument( - "Auxiliary shells contains a shell with radial powers; did you pass " + "Auxiliary shells contain a shell with radial powers; did you pass " "ECP shells by mistake? ECP basis must be constructed with both ECP " "shells and ECP electrons."); } diff --git a/python/src/pybind11/data/basis_set.cpp b/python/src/pybind11/data/basis_set.cpp index 3f62b6937..a99fbf053 100644 --- a/python/src/pybind11/data/basis_set.cpp +++ b/python/src/pybind11/data/basis_set.cpp @@ -55,15 +55,17 @@ std::shared_ptr basis_set_from_json_file_wrapper( qdk::chemistry::python::utils::to_string_path(filename)); } -// Convert a Python list of Shell objects to std::vector. +// Convert a Python iterable of Shell objects to std::vector. // Used by lambda-based init overloads to bypass pybind11's list_caster, which // can create a list_iterator that crashes under py::smart_holder during // overload probing of constructors with multiple std::vector params. // -// Uses index-based access (PyList_GetItem) instead of iteration so that no -// Python list_iterator object is ever created (list_iterator does not support -// weak references, which smart_holder tries to install). -std::vector to_shell_vec(const py::list& lst) { +// Materialize the iterable to a list first, then use index-based access so +// that no Python list_iterator object is ever created while extracting items +// (list_iterator does not support weak references, which smart_holder tries +// to install). +std::vector to_shell_vec(const py::iterable& items) { + py::list lst(items); const ssize_t n = py::len(lst); std::vector result; result.reserve(static_cast(n)); @@ -234,7 +236,8 @@ A shell represents a group of atomic orbitals with the same atom, angular moment // BasisSet(name, shells [, atomic_orbital_type]) basis_set.def( - py::init([](const std::string& name, const py::list& shells, AOType ao) { + py::init([](const std::string& name, const py::iterable& shells, + AOType ao) { return BasisSet(name, to_shell_vec(shells), ao); }), py::arg("name"), py::arg("shells"), @@ -244,31 +247,33 @@ Create a basis set from a name and shells. Args: name (str): Name of the basis set (e.g., "6-31G", "cc-pVDZ") - shells (list[Shell]): List of Shell objects + shells (Iterable[Shell]): Iterable of Shell objects atomic_orbital_type (AOType): Spherical or Cartesian (default Spherical) )"); // BasisSet(name, shells, structure [, atomic_orbital_type]) - basis_set.def(py::init([](const std::string& name, const py::list& shells, - const Structure& structure, AOType ao) { - return BasisSet(name, to_shell_vec(shells), structure, ao); - }), - py::arg("name"), py::arg("shells"), py::arg("structure"), - py::arg("atomic_orbital_type") = AOType::Spherical, - R"( + basis_set.def(py::init([](const std::string& name, + const py::iterable& shells, + const Structure& structure, AOType ao) { + return BasisSet(name, to_shell_vec(shells), structure, ao); + }), + py::arg("name"), py::arg("shells"), py::arg("structure"), + py::arg("atomic_orbital_type") = AOType::Spherical, + R"( Create a basis set from a name, shells, and molecular structure. Args: name (str): Name of the basis set - shells (list[Shell]): List of Shell objects + shells (Iterable[Shell]): Iterable of Shell objects structure (Structure): Molecular structure atomic_orbital_type (AOType): Spherical or Cartesian (default Spherical) )"); // BasisSet(name, shells, aux_shells, structure [, atomic_orbital_type]) - basis_set.def(py::init([](const std::string& name, const py::list& shells, - const py::list& aux_shells, - const Structure& structure, AOType ao) { + basis_set.def(py::init([](const std::string& name, + const py::iterable& shells, + const py::iterable& aux_shells, + const Structure& structure, AOType ao) { return BasisSet(name, to_shell_vec(shells), to_shell_vec(aux_shells), structure, ao); }), @@ -280,8 +285,8 @@ Create a basis set with auxiliary shells. Args: name (str): Name of the basis set - shells (list[Shell]): List of Shell objects - aux_shells (list[Shell]): List of auxiliary Shell objects (e.g., for density fitting) + shells (Iterable[Shell]): Iterable of Shell objects + aux_shells (Iterable[Shell]): Iterable of auxiliary Shell objects (e.g., for density fitting) structure (Structure): Molecular structure atomic_orbital_type (AOType): Spherical or Cartesian (default Spherical) )"); @@ -289,8 +294,9 @@ Create a basis set with auxiliary shells. // BasisSet(name, shells, aux_name, aux_shells, structure // [, atomic_orbital_type]) basis_set.def( - py::init([](const std::string& name, const py::list& shells, - const std::string& aux_name, const py::list& aux_shells, + py::init([](const std::string& name, const py::iterable& shells, + const std::string& aux_name, + const py::iterable& aux_shells, const Structure& structure, AOType ao) { return BasisSet(name, to_shell_vec(shells), aux_name, to_shell_vec(aux_shells), structure, ao); @@ -303,19 +309,20 @@ Create a basis set with a named auxiliary basis. Args: name (str): Name of the basis set - shells (list[Shell]): List of Shell objects + shells (Iterable[Shell]): Iterable of Shell objects aux_name (str): Name of the auxiliary basis set - aux_shells (list[Shell]): List of auxiliary Shell objects + aux_shells (Iterable[Shell]): Iterable of auxiliary Shell objects structure (Structure): Molecular structure atomic_orbital_type (AOType): Spherical or Cartesian (default Spherical) )"); // BasisSet(name, shells, ecp_shells, ecp_electrons, structure // [, atomic_orbital_type]) - basis_set.def(py::init([](const std::string& name, const py::list& shells, - const py::list& ecp_shells, - const std::vector& ecp_electrons, - const Structure& structure, AOType ao) { + basis_set.def(py::init([](const std::string& name, + const py::iterable& shells, + const py::iterable& ecp_shells, + const std::vector& ecp_electrons, + const Structure& structure, AOType ao) { return BasisSet(name, to_shell_vec(shells), to_shell_vec(ecp_shells), ecp_electrons, structure, ao); @@ -328,8 +335,8 @@ Create a basis set with ECP shells. Args: name (str): Name of the basis set - shells (list[Shell]): List of Shell objects - ecp_shells (list[Shell]): List of ECP Shell objects (with radial powers) + shells (Iterable[Shell]): Iterable of Shell objects + ecp_shells (Iterable[Shell]): Iterable of ECP Shell objects (with radial powers) ecp_electrons (list[int]): Number of ECP electrons per atom structure (Structure): Molecular structure atomic_orbital_type (AOType): Spherical or Cartesian (default Spherical) @@ -338,8 +345,9 @@ Create a basis set with ECP shells. // BasisSet(name, shells, ecp_name, ecp_shells, ecp_electrons, structure // [, atomic_orbital_type]) basis_set.def( - py::init([](const std::string& name, const py::list& shells, - const std::string& ecp_name, const py::list& ecp_shells, + py::init([](const std::string& name, const py::iterable& shells, + const std::string& ecp_name, + const py::iterable& ecp_shells, const std::vector& ecp_electrons, const Structure& structure, AOType ao) { return BasisSet(name, to_shell_vec(shells), ecp_name, @@ -353,9 +361,9 @@ Create a basis set with a named ECP. Args: name (str): Name of the basis set - shells (list[Shell]): List of Shell objects + shells (Iterable[Shell]): Iterable of Shell objects ecp_name (str): Name of the ECP basis set - ecp_shells (list[Shell]): List of ECP Shell objects + ecp_shells (Iterable[Shell]): Iterable of ECP Shell objects ecp_electrons (list[int]): Number of ECP electrons per atom structure (Structure): Molecular structure atomic_orbital_type (AOType): Spherical or Cartesian (default Spherical) @@ -364,10 +372,12 @@ Create a basis set with a named ECP. // BasisSet(name, shells, ecp_name, ecp_shells, ecp_electrons, // aux_name, aux_shells, structure [, atomic_orbital_type]) basis_set.def( - py::init([](const std::string& name, const py::list& shells, - const std::string& ecp_name, const py::list& ecp_shells, + py::init([](const std::string& name, const py::iterable& shells, + const std::string& ecp_name, + const py::iterable& ecp_shells, const std::vector& ecp_electrons, - const std::string& aux_name, const py::list& aux_shells, + const std::string& aux_name, + const py::iterable& aux_shells, const Structure& structure, AOType ao) { return BasisSet(name, to_shell_vec(shells), ecp_name, to_shell_vec(ecp_shells), ecp_electrons, aux_name, @@ -382,12 +392,12 @@ Create a basis set with ECP and auxiliary basis. Args: name (str): Name of the basis set - shells (list[Shell]): List of Shell objects + shells (Iterable[Shell]): Iterable of Shell objects ecp_name (str): Name of the ECP basis set - ecp_shells (list[Shell]): List of ECP Shell objects + ecp_shells (Iterable[Shell]): Iterable of ECP Shell objects ecp_electrons (list[int]): Number of ECP electrons per atom aux_name (str): Name of the auxiliary basis set - aux_shells (list[Shell]): List of auxiliary Shell objects + aux_shells (Iterable[Shell]): Iterable of auxiliary Shell objects structure (Structure): Molecular structure atomic_orbital_type (AOType): Spherical or Cartesian (default Spherical) )"); @@ -667,6 +677,19 @@ Get total number of atomic orbitals in the basis set. >>> print(f"Total atomic orbitals: {n_basis}") )"); + basis_set.def("get_num_auxiliary_orbitals", + &BasisSet::get_num_auxiliary_orbitals, + R"( +Get total number of auxiliary orbitals in the basis set. + +Returns: + int: Total number of auxiliary orbitals from all auxiliary shells + +Examples: + >>> n_aux = basis_set.get_num_auxiliary_orbitals() + >>> print(f"Total auxiliary orbitals: {n_aux}") +)"); + // Atom mapping basis_set.def("get_atom_index_for_atomic_orbital", &BasisSet::get_atom_index_for_atomic_orbital, diff --git a/python/tests/test_basis_set.py b/python/tests/test_basis_set.py index d67035cfd..a21fc51a7 100644 --- a/python/tests/test_basis_set.py +++ b/python/tests/test_basis_set.py @@ -359,6 +359,7 @@ def test_json_serialization(): assert basis_in.has_aux_basis() assert basis_in.get_aux_name() == "aux-fit" assert basis_in.get_num_aux_shells() == 2 + assert basis_in.get_num_auxiliary_orbitals() == 6 # Test file-based serialization with tempfile.NamedTemporaryFile(suffix=".basis_set.json", mode="w", delete=False) as tmp: @@ -375,6 +376,8 @@ def test_json_serialization(): assert basis_file.get_num_atomic_orbitals() == 4 assert basis_file.get_aux_name() == "aux-fit" assert basis_file.get_num_aux_shells() == 2 + assert basis_out.get_num_auxiliary_orbitals() == 6 + assert basis_file.get_num_auxiliary_orbitals() == 6 finally: Path(filename).unlink() @@ -1817,5 +1820,5 @@ def test_rejects_no_args(self): # --- Error cases: ECP shells at n==4 should raise --- def test_ecp_at_n4_raises(self, shells, ecp_shells, structure): - with pytest.raises(ValueError, match="Auxiliary shells contains a shell with radial powers"): + with pytest.raises(ValueError, match="Auxiliary shells contain a shell with radial powers"): BasisSet("test", shells, ecp_shells, structure) From 7e098d186eb2f94dc432ad70b08f585bcc160dd9 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Tue, 5 May 2026 22:04:28 +0000 Subject: [PATCH 31/33] pre-commit --- python/src/pybind11/data/basis_set.cpp | 60 +++++++++++--------------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/python/src/pybind11/data/basis_set.cpp b/python/src/pybind11/data/basis_set.cpp index a99fbf053..d2d91d161 100644 --- a/python/src/pybind11/data/basis_set.cpp +++ b/python/src/pybind11/data/basis_set.cpp @@ -65,7 +65,7 @@ std::shared_ptr basis_set_from_json_file_wrapper( // (list_iterator does not support weak references, which smart_holder tries // to install). std::vector to_shell_vec(const py::iterable& items) { - py::list lst(items); + py::list lst(items); const ssize_t n = py::len(lst); std::vector result; result.reserve(static_cast(n)); @@ -235,14 +235,13 @@ A shell represents a group of atomic orbitals with the same atom, angular moment "Copy constructor. Creates a deep copy of the basis set."); // BasisSet(name, shells [, atomic_orbital_type]) - basis_set.def( - py::init([](const std::string& name, const py::iterable& shells, - AOType ao) { - return BasisSet(name, to_shell_vec(shells), ao); - }), - py::arg("name"), py::arg("shells"), - py::arg("atomic_orbital_type") = AOType::Spherical, - R"( + basis_set.def(py::init([](const std::string& name, const py::iterable& shells, + AOType ao) { + return BasisSet(name, to_shell_vec(shells), ao); + }), + py::arg("name"), py::arg("shells"), + py::arg("atomic_orbital_type") = AOType::Spherical, + R"( Create a basis set from a name and shells. Args: @@ -252,14 +251,13 @@ Create a basis set from a name and shells. )"); // BasisSet(name, shells, structure [, atomic_orbital_type]) - basis_set.def(py::init([](const std::string& name, - const py::iterable& shells, - const Structure& structure, AOType ao) { - return BasisSet(name, to_shell_vec(shells), structure, ao); - }), - py::arg("name"), py::arg("shells"), py::arg("structure"), - py::arg("atomic_orbital_type") = AOType::Spherical, - R"( + basis_set.def(py::init([](const std::string& name, const py::iterable& shells, + const Structure& structure, AOType ao) { + return BasisSet(name, to_shell_vec(shells), structure, ao); + }), + py::arg("name"), py::arg("shells"), py::arg("structure"), + py::arg("atomic_orbital_type") = AOType::Spherical, + R"( Create a basis set from a name, shells, and molecular structure. Args: @@ -270,10 +268,9 @@ Create a basis set from a name, shells, and molecular structure. )"); // BasisSet(name, shells, aux_shells, structure [, atomic_orbital_type]) - basis_set.def(py::init([](const std::string& name, - const py::iterable& shells, - const py::iterable& aux_shells, - const Structure& structure, AOType ao) { + basis_set.def(py::init([](const std::string& name, const py::iterable& shells, + const py::iterable& aux_shells, + const Structure& structure, AOType ao) { return BasisSet(name, to_shell_vec(shells), to_shell_vec(aux_shells), structure, ao); }), @@ -295,8 +292,7 @@ Create a basis set with auxiliary shells. // [, atomic_orbital_type]) basis_set.def( py::init([](const std::string& name, const py::iterable& shells, - const std::string& aux_name, - const py::iterable& aux_shells, + const std::string& aux_name, const py::iterable& aux_shells, const Structure& structure, AOType ao) { return BasisSet(name, to_shell_vec(shells), aux_name, to_shell_vec(aux_shells), structure, ao); @@ -318,11 +314,10 @@ Create a basis set with a named auxiliary basis. // BasisSet(name, shells, ecp_shells, ecp_electrons, structure // [, atomic_orbital_type]) - basis_set.def(py::init([](const std::string& name, - const py::iterable& shells, - const py::iterable& ecp_shells, - const std::vector& ecp_electrons, - const Structure& structure, AOType ao) { + basis_set.def(py::init([](const std::string& name, const py::iterable& shells, + const py::iterable& ecp_shells, + const std::vector& ecp_electrons, + const Structure& structure, AOType ao) { return BasisSet(name, to_shell_vec(shells), to_shell_vec(ecp_shells), ecp_electrons, structure, ao); @@ -346,8 +341,7 @@ Create a basis set with ECP shells. // [, atomic_orbital_type]) basis_set.def( py::init([](const std::string& name, const py::iterable& shells, - const std::string& ecp_name, - const py::iterable& ecp_shells, + const std::string& ecp_name, const py::iterable& ecp_shells, const std::vector& ecp_electrons, const Structure& structure, AOType ao) { return BasisSet(name, to_shell_vec(shells), ecp_name, @@ -373,11 +367,9 @@ Create a basis set with a named ECP. // aux_name, aux_shells, structure [, atomic_orbital_type]) basis_set.def( py::init([](const std::string& name, const py::iterable& shells, - const std::string& ecp_name, - const py::iterable& ecp_shells, + const std::string& ecp_name, const py::iterable& ecp_shells, const std::vector& ecp_electrons, - const std::string& aux_name, - const py::iterable& aux_shells, + const std::string& aux_name, const py::iterable& aux_shells, const Structure& structure, AOType ao) { return BasisSet(name, to_shell_vec(shells), ecp_name, to_shell_vec(ecp_shells), ecp_electrons, aux_name, From 5fe70edf7d945ed2041c701aa688b7df05efb642 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Wed, 6 May 2026 00:07:44 +0000 Subject: [PATCH 32/33] remove dead code --- python/tests/test_basis_set.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/tests/test_basis_set.py b/python/tests/test_basis_set.py index a21fc51a7..a789f80a6 100644 --- a/python/tests/test_basis_set.py +++ b/python/tests/test_basis_set.py @@ -1048,7 +1048,6 @@ def test_basis_set_pickling_and_repr(): Shell(0, OrbitalType.S, [3.425251, 0.623914, 0.168855], [0.154329, 0.535328, 0.444635]), Shell(0, OrbitalType.P, [1.158, 0.325], [0.155916, 0.607684]), ] - original = BasisSet("STO-3G", shells) # Create a basis set with auxiliary shells positions = np.array([[0.0, 0.0, 0.0]]) From 0fbb39e510026fac63e090989048640c059fb4a9 Mon Sep 17 00:00:00 2001 From: rainli323 Date: Wed, 6 May 2026 17:07:57 +0000 Subject: [PATCH 33/33] AI comments on valid test and python export Co-authored-by: Copilot --- cpp/src/qdk/chemistry/data/basis_set.cpp | 32 ++++++++++++++++++++++++ python/src/pybind11/data/basis_set.cpp | 7 ++++++ 2 files changed, 39 insertions(+) diff --git a/cpp/src/qdk/chemistry/data/basis_set.cpp b/cpp/src/qdk/chemistry/data/basis_set.cpp index 4307f050e..e4bb67895 100644 --- a/cpp/src/qdk/chemistry/data/basis_set.cpp +++ b/cpp/src/qdk/chemistry/data/basis_set.cpp @@ -1739,6 +1739,38 @@ bool BasisSet::_is_valid() const { } } } + bool has_ecp = false; + for (const auto& atom_shells : _ecp_shells_per_atom) { + if (!atom_shells.empty()) { + has_ecp = true; + + // Check if all shells have valid data + for (const auto& shell : atom_shells) { + if (shell.exponents.size() == 0 || shell.coefficients.size() == 0) { + return false; + } + if (shell.exponents.size() != shell.coefficients.size()) { + return false; + } + } + } + } + bool has_aux_shells = false; + for (const auto& atom_shells : _aux_shells_per_atom) { + if (!atom_shells.empty()) { + has_aux_shells = true; + + // Check if all shells have valid data + for (const auto& shell : atom_shells) { + if (shell.exponents.size() == 0 || shell.coefficients.size() == 0) { + return false; + } + if (shell.exponents.size() != shell.coefficients.size()) { + return false; + } + } + } + } bool ecp_consistency = (has_ecp_electrons() == has_ecp_shells()); diff --git a/python/src/pybind11/data/basis_set.cpp b/python/src/pybind11/data/basis_set.cpp index d2d91d161..07f9576dc 100644 --- a/python/src/pybind11/data/basis_set.cpp +++ b/python/src/pybind11/data/basis_set.cpp @@ -1497,6 +1497,13 @@ Name used for custom ECP basis sets. Type: str )"); + basis_set.def_readonly_static("custom_aux_name", &BasisSet::custom_aux_name, + R"( +Name used for custom auxiliary basis sets. + +Type: + str +)"); // Data type name class attribute basis_set.attr("_data_type_name") = DATACLASS_TO_SNAKE_CASE(BasisSet);