Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

clease

Package Overview
Dependencies
Maintainers
2
Versions
38
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

clease - npm Package Compare versions

Comparing version
1.1.0
to
1.2.0
+979
cxx/src/ce_updater.cpp
#include "ce_updater.hpp"
#include <algorithm>
#include <cassert>
#include <iostream>
#include <iterator>
#include <sstream>
#include <stdexcept>
#include "additional_tools.hpp"
#include "atomic_numbers.hpp"
#include "basis_function.hpp"
#include "cluster_name.hpp"
#include "config.hpp"
using namespace std;
CEUpdater::CEUpdater(){};
CEUpdater::~CEUpdater(){};
void CEUpdater::init(PyObject *py_atoms, PyObject *settings, PyObject *corrFunc, PyObject *pyeci,
PyObject *cluster_list) {
atoms = py_atoms;
if (settings == nullptr) {
throw invalid_argument("Settings object is nullptr!");
}
#ifdef PRINT_DEBUG
cout << "Getting symbols from settings object\n";
#endif
PyObject *py_ignore_bck = get_attr(settings, "ignore_background_atoms");
ignore_background_indices = PyObject_IsTrue(py_ignore_bck);
Py_DECREF(py_ignore_bck);
unsigned int n_atoms = PyObject_Length(atoms);
if (n_atoms < 0) {
throw invalid_argument("Could not retrieve the length of the atoms object!");
}
// Read the atomic symbols
std::vector<std::string> symbols = get_symbols_from_atoms(py_atoms);
trans_symm_group.resize(n_atoms);
set<string> unique_symbols;
// Extract unique symbols from settings
PyObject *py_unique_symb = get_attr(settings, "unique_elements");
for (unsigned int i = 0; i < list_size(py_unique_symb); i++) {
unique_symbols.insert(py2string(PyList_GetItem(py_unique_symb, i)));
}
Py_DECREF(py_unique_symb);
insert_in_set(symbols, unique_symbols);
symbols_with_id = std::make_unique<Symbols>(symbols, unique_symbols);
// Build read the translational sites
PyObject *py_trans_symm_group = get_attr(settings, "index_by_sublattice");
if (py_trans_symm_group == nullptr) {
throw invalid_argument("index_by_sublattice is nullptr!");
}
#ifdef PRINT_DEBUG
cout << "Reading background indices\n";
#endif
// Read the backgound indices from settings
PyObject *bkg_indx = get_attr(settings, "background_indices");
read_background_indices(bkg_indx);
Py_DECREF(bkg_indx);
count_non_bkg_sites();
build_trans_symm_group(py_trans_symm_group);
Py_DECREF(py_trans_symm_group);
#ifdef PRINT_DEBUG
cout << "Getting cluster names from atoms object\n";
#endif
// Read cluster names
create_cname_with_dec(corrFunc);
#ifdef PRINT_DEBUG
cout << "Cluster names with decoration number created...\n";
#endif
PyObject *py_num_elements = get_attr(settings, "num_unique_elements");
if (py_num_elements == nullptr) {
throw invalid_argument("num_unique_elements is nullptr!");
}
int num_bfs = py2int(py_num_elements) - 1;
Py_DECREF(py_num_elements);
if (cluster_list == nullptr) {
throw invalid_argument("cluster_list is nullptr!");
}
// unsigned int num_trans_symm = list_size(cluster_info);
unsigned int num_clusters = PySequence_Size(cluster_list);
#ifdef PRINT_DEBUG
cout << "Parsing cluster list...\n";
#endif
PyObject *py_no_si = get_attr(cluster_list, "assume_no_self_interactions");
assume_no_self_interactions = PyObject_IsTrue(py_no_si);
Py_DECREF(py_no_si);
#ifdef PRINT_DEBUG
std::cout << "Assuming no self-interaction?: " << assume_no_self_interactions << std::endl;
#endif
for (unsigned int i = 0; i < num_clusters; i++) {
PyObject *py_cluster = PySequence_GetItem(cluster_list, i);
Cluster new_clst(py_cluster);
PyObject *py_cluster_name = get_attr(py_cluster, "name");
string cluster_name = py2string(py_cluster_name);
Py_DECREF(py_cluster_name);
Py_DECREF(py_cluster);
new_clst.construct_equivalent_deco(num_bfs);
clusters.append(new_clst);
int norm_fact = new_clst.get().size() * trans_symm_group_count[new_clst.symm_group];
if (normalisation_factor.find(cluster_name) == normalisation_factor.end()) {
normalisation_factor[cluster_name] = norm_fact;
} else {
normalisation_factor[cluster_name] += norm_fact;
}
}
#ifdef PRINT_DEBUG
cout << "Finished reading cluster_info\n";
#endif
#ifdef PRINT_DEBUG
cout << "Reading basis functions from settings object\n";
#endif
PyObject *bfs = get_attr(settings, "basis_functions");
if (bfs == NULL) {
status = Status_t::INIT_FAILED;
return;
}
// Reading basis functions from python object
PyObject *key;
PyObject *value;
unsigned int n_bfs = list_size(bfs);
bf_list basis_func_raw;
for (unsigned int i = 0; i < n_bfs; i++) {
Py_ssize_t pos = 0;
map<string, double> new_entry;
PyObject *bf_dict = PyList_GetItem(bfs, i);
while (PyDict_Next(bf_dict, &pos, &key, &value)) {
new_entry[py2string(key)] = PyFloat_AS_DOUBLE(value);
}
basis_func_raw.push_back(new_entry);
}
this->basis_functions = std::make_unique<BasisFunction>(basis_func_raw, *symbols_with_id);
#ifdef PRINT_DEBUG
cout << "Reading translation matrix from settings\n";
#endif
// Retrieve the TransMatrix object
PyObject *trans_mat_obj = get_attr(settings, "trans_matrix");
if (trans_mat_obj == NULL) {
status = Status_t::INIT_FAILED;
return;
}
// Get the internal trans_matrix object from within the TransMatrix object
PyObject *trans_mat_orig = get_attr(trans_mat_obj, "trans_matrix");
read_trans_matrix(trans_mat_orig);
Py_DECREF(trans_mat_obj);
Py_DECREF(trans_mat_orig);
// Read the ECIs, and parse the names.
this->set_eci(pyeci);
#ifdef PRINT_DEBUG
cout << "Parsing correlation function\n";
#endif
vector<string> flattened_cnames;
flattened_cf_names(flattened_cnames);
history = std::make_unique<CFHistoryTracker>(eci.get_names());
history->insert(corrFunc, nullptr);
// Store the singlets names
for (unsigned int i = 0; i < flattened_cnames.size(); i++) {
std::string name = flattened_cnames[i];
// Fetch the pre-parsed version of the name.
const ParsedName parsed = this->m_parsed_names[i];
if (parsed.size == 1) {
singlets.push_back(name);
}
}
status = Status_t::READY;
clear_history();
#ifdef PRINT_DEBUG
cout << "CEUpdater initialized sucessfully!\n";
#endif
// Verify that the ECIs given corresponds to a correlation function
if (!all_eci_corresponds_to_cf()) {
throw invalid_argument("All ECIs does not correspond to a correlation function!");
}
}
void CEUpdater::parse_eci_names() {
std::size_t num = eci.size();
this->m_parsed_names.clear();
this->m_parsed_names.reserve(num);
for (unsigned int i = 0; i < num; i++) {
std::string name = eci.name(i);
ClusterName c_name = ClusterName(name);
ParsedName parsed = c_name.get_parsed();
this->m_parsed_names.emplace_back(parsed);
}
}
double CEUpdater::get_energy() {
double energy = 0.0;
cf &corr_func = history->get_current();
energy = eci.dot(corr_func);
return energy * symbols_with_id->size();
}
double CEUpdater::spin_product_one_atom(int ref_indx, const Cluster &cluster,
const vector<int> &dec, int ref_id) const {
double sp = 0.0;
// Note: No duplication factor is included here, since this method is used for calculating the
// CF from scratch (which will account for self-interactions with no issue), and not updating
// (which must account for self-interactions via the duplication factor).
// i.e. spin_product_one_atom_delta must accounts for the self-interaction.
const vector<vector<int>> &indx_list = cluster.get();
unsigned int num_indx = indx_list.size();
unsigned int n_memb = indx_list[0].size();
// Cache the relevant row from the trans matrix.
int *trans_matrix_row = trans_matrix.get_row(ref_indx);
for (unsigned int i = 0; i < num_indx; i++) {
double sp_temp = 1.0;
// Use pointer arithmetics in the inner most loop
const int *indices = &indx_list[i][0];
for (unsigned int j = 0; j < n_memb; j++) {
int trans_index = trans_matrix.lookup_in_row(trans_matrix_row, indices[j]);
int id = (trans_index == ref_indx) ? ref_id : symbols_with_id->id(trans_index);
sp_temp *= basis_functions->get(dec[j], id);
}
sp += sp_temp;
}
return sp;
}
int CEUpdater::get_original_index(int ref_indx) const {
int *trans_matrix_row = trans_matrix.get_row(ref_indx);
int *allowed_lu = trans_matrix.get_allowed_lookup_values();
for (unsigned int j = 0; j < trans_matrix.get_num_non_zero(); j++) {
int col = allowed_lu[j];
int indx = trans_matrix.lookup_in_row(trans_matrix_row, col);
if (indx == ref_indx) {
return col;
}
}
std::stringstream err;
err << "Did not locate original index for ref index: " << ref_indx;
throw std::runtime_error(err.str());
}
double CEUpdater::spin_product_one_atom_delta_no_si(const SpinProductCache &sp_cache,
const Cluster &cluster,
const deco_t &deco) const {
/* Note: This function assumes no self-interaction within a cluster. */
// Figure out how many times we need to iterate
unsigned int num_indx = cluster.get_num_figures(); // Outer loop count
// Assume 1 ref site in a figure, so we iterate 1 less
unsigned int n_non_ref = cluster.get_size() - 1; // Inner loop count
int *tm_row = sp_cache.trans_matrix_row;
/* There are n_non_ref sites per each ref site, so the non_ref_site_ptr
iterates faster than the ref_site_ptr. Figures are placed contiguously
in a 1D array.*/
const ClusterSite *non_ref_site_ptr = &cluster.get_non_ref_sites()[0];
const int *ref_site_ptr = &cluster.get_ref_cluster_sites()[0];
// Keep track of the change in spin-product
double sp_delta = 0.0;
// Iterate each figure in the cluster. 1 reference site is assumed per cluster
for (unsigned int i = 0; i < num_indx; i++, ++ref_site_ptr) {
/* Calculate the spin product for both new and the old (ref)
The constant term to the spin product from the sites which didn't change.*/
const int dec_ref = deco[*ref_site_ptr];
double new_bf = basis_functions->get(dec_ref, sp_cache.new_symb_id);
double old_bf = basis_functions->get(dec_ref, sp_cache.old_symb_id);
double sp_change = new_bf - old_bf;
/* Iterate the remaining non-reference sites, as we already took care of the reference
site (assuming no self-interaction)*/
for (unsigned int j = 0; j < n_non_ref; j++, ++non_ref_site_ptr) {
const ClusterSite &site = *non_ref_site_ptr;
const int dec_j = deco[site.cluster_index];
const int trans_index = trans_matrix.lookup_in_row(tm_row, site.lattice_index);
sp_change *= basis_functions->get(dec_j, symbols_with_id->id(trans_index));
}
sp_delta += sp_change;
}
return sp_delta;
}
double CEUpdater::spin_product_one_atom_delta(const SpinProductCache &sp_cache,
const Cluster &cluster, const deco_t &deco) const {
// Keep track of the change in spin-product
double sp_delta = 0.0;
// List of figures in the cluster
const vector<vector<int>> &indx_list = cluster.get();
// Account for the self-interaction, in case we updated 2 sites with 1 change
const std::vector<double> &dup_factors = cluster.get_duplication_factors();
unsigned int num_indx = indx_list.size();
unsigned int n_memb = indx_list[0].size();
int *tm_row = sp_cache.trans_matrix_row;
// Iterate each site in the cluster
for (unsigned int i = 0; i < num_indx; i++) {
// Use pointer arithmetics in the inner most loop
const int *indices = &indx_list[i][0];
// Calculate the spin product for both new and the old (ref)
double sp_temp_new = 1.0, sp_temp_ref = 1.0;
// The constant term to the spin product from the sites which didn't change.
double sp_const = dup_factors[i];
for (unsigned int j = 0; j < n_memb; j++) {
const int site_index = indices[j];
const int dec_j = deco[j];
if (site_index == sp_cache.original_index) {
// This site is the reference index.
// Look up the BF values directly for the new and old symbols
sp_temp_new *= basis_functions->get(dec_j, sp_cache.new_symb_id);
sp_temp_ref *= basis_functions->get(dec_j, sp_cache.old_symb_id);
} else {
// Look up the symbol of the non-reference site, which hasn't changed.
const int trans_index = trans_matrix.lookup_in_row(tm_row, site_index);
sp_const *= basis_functions->get(dec_j, symbols_with_id->id(trans_index));
}
}
// The change in spin-product is the difference in SP between the site(s) which
// changed, multiplied by the constant SP from the other un-changed sites (since
// these contribute equally before and after the change).
sp_delta += (sp_temp_new - sp_temp_ref) * sp_const;
}
return sp_delta;
}
void CEUpdater::update_cf(PyObject *single_change) {
SymbolChange symb_change = SymbolChange(single_change);
update_cf(symb_change);
}
void CEUpdater::py_changes2_symb_changes(PyObject *all_changes,
vector<SymbolChange> &symb_changes) {
unsigned int size = list_size(all_changes);
for (unsigned int i = 0; i < size; i++) {
SymbolChange symb_change = SymbolChange(PyList_GetItem(all_changes, i));
symb_changes.push_back(symb_change);
}
}
SpinProductCache CEUpdater::build_sp_cache(const SymbolChange &symb_change) const {
int ref_indx = symb_change.indx;
// Look up the untranslated index of the reference index.
int orig_indx = this->get_original_index(ref_indx);
// Cache the relevant row from the trans matrix.
int *trans_matrix_row = this->trans_matrix.get_row(ref_indx);
unsigned int old_symb_id = symbols_with_id->get_symbol_id(symb_change.old_symb);
unsigned int new_symb_id = symbols_with_id->get_symbol_id(symb_change.new_symb);
SpinProductCache sp_cache = {ref_indx, orig_indx, trans_matrix_row, new_symb_id, old_symb_id};
return sp_cache;
}
cf &CEUpdater::get_next_cf(SymbolChange &symb_change) {
SymbolChange *symb_change_track;
cf *next_cf_ptr = nullptr;
history->get_next(&next_cf_ptr, &symb_change_track);
cf &next_cf = *next_cf_ptr;
symb_change_track->indx = symb_change.indx;
symb_change_track->old_symb = symb_change.old_symb;
symb_change_track->new_symb = symb_change.new_symb;
symb_change_track->track_indx = symb_change.track_indx;
return next_cf;
}
void CEUpdater::update_cf(SymbolChange &symb_change) {
if (symb_change.old_symb == symb_change.new_symb) {
return;
}
if (is_background_index[symb_change.indx]) {
throw runtime_error("Attempting to move a background atom!");
}
cf &current_cf = history->get_current();
cf &next_cf = get_next_cf(symb_change);
symbols_with_id->set_symbol(symb_change.indx, symb_change.new_symb);
/* The following prepares a range of properties which will be
useful for all of the clusters, so we don't compute more
than we have to inside the main ECI loop */
SpinProductCache sp_cache = this->build_sp_cache(symb_change);
if (atoms != nullptr) {
set_symbol_in_atoms(atoms, symb_change.indx, symb_change.new_symb);
}
int symm = trans_symm_group[symb_change.indx];
const std::vector<ClusterCache> &clusters_cache = m_cluster_by_symm[symm];
// Loop over all ECIs
// As work load for different clusters are different due to a different
// multiplicity factor, we need to apply a dynamic schedule
#ifdef HAS_OMP
bool is_par = this->cf_update_num_threads > 1;
#pragma omp parallel for if (is_par) num_threads(this->cf_update_num_threads) schedule(dynamic)
#endif
for (unsigned int i = 0; i < eci.size(); i++) {
// The pre-parsed version of the cluster name.
const ParsedName &parsed = this->m_parsed_names[i];
// 0-body
if (parsed.size == 0) {
next_cf[i] = current_cf[i];
continue;
}
// Singlet
if (parsed.size == 1) {
unsigned int dec = parsed.dec_num;
double new_bf = basis_functions->get(dec, sp_cache.new_symb_id);
double old_bf = basis_functions->get(dec, sp_cache.old_symb_id);
next_cf[i] = current_cf[i] + (new_bf - old_bf) / num_non_bkg_sites;
continue;
}
// n-body
const ClusterCache &cluster_cache = clusters_cache[i];
const Cluster *cluster_ptr = cluster_cache.cluster_ptr;
if (cluster_ptr == nullptr) {
// This cluster was not present in the symmetry group.
next_cf[i] = current_cf[i];
continue;
}
// The cluster is in the symmetry group, so calculate the spin product
// change for this cluster.
const Cluster &cluster = *cluster_ptr;
const equiv_deco_t &equiv_deco = *cluster_cache.equiv_deco_ptr;
double delta_sp = 0.0;
// Calculate the change (delta) in spin product
for (const deco_t &deco : equiv_deco) {
if (this->assume_no_self_interactions) {
// Faster version for large cells with no self interaction
delta_sp += spin_product_one_atom_delta_no_si(sp_cache, cluster, deco);
} else {
// Safe fall-back version
delta_sp += spin_product_one_atom_delta(sp_cache, cluster, deco);
}
}
delta_sp *= cluster_cache.normalization;
next_cf[i] = current_cf[i] + delta_sp;
}
}
void CEUpdater::undo_changes() {
unsigned int buf_size = history->history_size();
undo_changes(buf_size - 1);
}
void CEUpdater::undo_changes(int num_steps) {
int buf_size = history->history_size();
if (num_steps > buf_size - 1) {
throw invalid_argument("Can't reset history beyond the buffer size!");
}
SymbolChange *last_changes;
for (int i = 0; i < num_steps; i++) {
history->pop(&last_changes);
symbols_with_id->set_symbol(last_changes->indx, last_changes->old_symb);
if (atoms != nullptr) {
set_symbol_in_atoms(atoms, last_changes->indx, last_changes->old_symb);
}
}
}
double CEUpdater::calculate(PyObject *system_changes) {
unsigned int size = list_size(system_changes);
if (size == 0) {
return get_energy();
} else if (size == 1) {
for (unsigned int i = 0; i < size; i++) {
update_cf(PyList_GetItem(system_changes, i));
}
return get_energy();
}
if (size % 2 == 0) {
bool sequence_arbitrary_moves = false;
vector<swap_move> sequence;
for (unsigned int i = 0; i < size / 2; i++) {
swap_move changes;
changes[0] = SymbolChange(PyList_GetItem(system_changes, 2 * i));
changes[1] = SymbolChange(PyList_GetItem(system_changes, 2 * i + 1));
if (!is_swap_move(changes)) {
sequence_arbitrary_moves = true;
break;
}
sequence.push_back(changes);
}
if (!sequence_arbitrary_moves) {
return calculate(sequence);
}
}
// Last option is that this is a sequence of arbitrary moves
vector<SymbolChange> changes(size);
for (unsigned int i = 0; i < size; i++) {
changes[i] = SymbolChange(PyList_GetItem(system_changes, i));
}
return calculate(changes);
}
double CEUpdater::calculate(swap_move &system_changes) {
if (symbols_with_id->id(system_changes[0].indx) ==
symbols_with_id->id(system_changes[1].indx)) {
cout << system_changes[0] << endl;
cout << system_changes[1] << endl;
throw runtime_error(
"This version of the calculate function assumes that the provided update is swapping "
"two atoms\n");
}
if (symbols_with_id->get_symbol(system_changes[0].indx) != system_changes[0].old_symb) {
throw runtime_error("The atom position tracker does not match the current state\n");
} else if (symbols_with_id->get_symbol(system_changes[1].indx) != system_changes[1].old_symb) {
throw runtime_error("The atom position tracker does not match the current state\n");
}
// Update correlation function
update_cf(system_changes[0]);
update_cf(system_changes[1]);
return get_energy();
}
void CEUpdater::clear_history() {
history->clear();
}
void CEUpdater::flattened_cf_names(vector<string> &flattened) {
flattened = eci.get_names();
// Sort the cluster names for consistency
sort(flattened.begin(), flattened.end());
}
PyObject *CEUpdater::get_cf() {
PyObject *cf_dict = PyDict_New();
cf &corrfunc = history->get_current();
for (unsigned int i = 0; i < corrfunc.size(); i++) {
PyObject *pyvalue = PyFloat_FromDouble(corrfunc[i]);
PyDict_SetItemString(cf_dict, corrfunc.name(i).c_str(), pyvalue);
Py_DECREF(pyvalue);
}
return cf_dict;
}
void CEUpdater::set_symbols(const vector<string> &new_symbs) {
if (new_symbs.size() != symbols_with_id->size()) {
throw runtime_error(
"The number of atoms in the updater cannot be changed via the set_symbols function\n");
}
symbols_with_id->set_symbols(new_symbs);
}
void CEUpdater::cluster_by_symm_group() {
m_cluster_by_symm.clear();
// Find unique symmetry groups
std::set<int> unique;
insert_in_set(this->trans_symm_group, unique);
for (const int symm : unique) {
if (symm == -1) {
// Background symmetry group
continue;
}
// 1 ClusterCache per ECI value
std::vector<ClusterCache> cluster_cache;
cluster_cache.reserve(this->m_parsed_names.size());
for (const ParsedName &parsed : this->m_parsed_names) {
ClusterCache cache;
if (parsed.size == 0 || parsed.size == 1 ||
!clusters.is_in_symm_group(parsed.prefix, symm)) {
/* Either 0- or 1-body cluster, or cluster is not in this
symmetry group. */
cluster_cache.push_back(cache);
continue;
}
Cluster *cluster_ptr = clusters.get_ptr(parsed.prefix, symm);
equiv_deco_t *equiv_ptr = cluster_ptr->get_equiv_deco_ptr(parsed.dec_str);
// Calculate the normalization of the resulting cluster functions
double normalization = static_cast<double>(cluster_ptr->size);
normalization /= equiv_ptr->size();
normalization /= normalisation_factor.at(parsed.prefix);
// Populate the new cache object
cache.cluster_ptr = cluster_ptr;
cache.equiv_deco_ptr = equiv_ptr;
cache.normalization = normalization;
cluster_cache.push_back(cache);
}
m_cluster_by_symm.insert({symm, cluster_cache});
}
}
void CEUpdater::set_eci(PyObject *pyeci) {
PyObject *key;
PyObject *value;
// Read the ECIs
Py_ssize_t pos = 0;
std::map<std::string, double> temp_eci;
while (PyDict_Next(pyeci, &pos, &key, &value)) {
temp_eci[py2string(key)] = PyFloat_AS_DOUBLE(value);
}
this->eci.init(temp_eci);
// Pre-parse the names of the clusters.
this->parse_eci_names();
// If status is not READY, then we're still initializing, and CF's are missing.
if (this->status == Status_t::READY && !all_eci_corresponds_to_cf()) {
throw invalid_argument("All ECIs has to correspond to a correlation function!");
}
// Update the cluster pointers to match the order with the ECI's.
cluster_by_symm_group();
}
bool CEUpdater::all_decoration_nums_equal(const vector<int> &dec_nums) const {
for (unsigned int i = 1; i < dec_nums.size(); i++) {
if (dec_nums[i] != dec_nums[0]) {
return false;
}
}
return true;
}
std::vector<double> CEUpdater::get_singlets() const {
size_t n = singlets.size();
std::vector<double> singlets_out(n, 0.0);
cf &cfs = history->get_current();
for (unsigned int i = 0; i < n; i++) {
singlets_out[i] = cfs[singlets[i]];
}
return singlets_out;
}
void CEUpdater::create_cname_with_dec(PyObject *cf) {
if (!PyDict_Check(cf)) {
throw invalid_argument("Correlation functons has to be dictionary!");
}
Py_ssize_t pos = 0;
PyObject *key;
PyObject *value;
while (PyDict_Next(cf, &pos, &key, &value)) {
string new_key = py2string(key);
ClusterName c_name = ClusterName(new_key);
unsigned int size = c_name.get_size();
#ifdef PRINT_DEBUG
cout << "Read CF: " << new_key << endl;
#endif
if ((size == 0 || size == 1)) {
cname_with_dec[new_key] = new_key;
} else {
std::string prefix = c_name.get_prefix();
cname_with_dec[prefix] = new_key;
}
}
}
void CEUpdater::build_trans_symm_group(PyObject *py_trans_symm_group) {
// Fill the symmetry group array with -1 indicating an invalid value
for (unsigned int i = 0; i < trans_symm_group.size(); i++) {
trans_symm_group[i] = -1;
}
unsigned int py_list_size = list_size(py_trans_symm_group);
for (unsigned int i = 0; i < py_list_size; i++) {
PyObject *sublist = PyList_GetItem(py_trans_symm_group, i);
unsigned int n_sites = list_size(sublist);
for (unsigned int j = 0; j < n_sites; j++) {
int indx = py2int(PyList_GetItem(sublist, j));
if (trans_symm_group[indx] != -1) {
throw runtime_error(
"One site appears to be present in more than one translation symmetry "
"group!");
}
trans_symm_group[indx] = i;
}
}
// Check that all sites belongs to one translational symmetry group
for (unsigned int i = 0; i < trans_symm_group.size(); i++) {
if ((trans_symm_group[i] == -1) && !is_background_index[i]) {
stringstream msg;
msg << "Site " << i << " has not been assigned to any translational symmetry group!";
throw runtime_error(msg.str());
}
}
// Count the number of atoms in each symmetry group
trans_symm_group_count.resize(py_list_size);
fill(trans_symm_group_count.begin(), trans_symm_group_count.end(), 0);
for (unsigned int i = 0; i < trans_symm_group.size(); i++) {
if (trans_symm_group[i] >= 0) {
trans_symm_group_count[trans_symm_group[i]] += 1;
}
}
}
bool CEUpdater::all_eci_corresponds_to_cf() {
cf &corrfunc = history->get_current();
return eci.names_are_equal(corrfunc);
}
double CEUpdater::calculate(vector<swap_move> &sequence) {
if (sequence.size() >= history->max_history / 2) {
throw invalid_argument(
"The length of sequence of swap move exceeds the buffer size for the history "
"tracker");
}
for (unsigned int i = 0; i < sequence.size(); i++) {
calculate(sequence[i]);
}
return get_energy();
}
double CEUpdater::calculate(vector<SymbolChange> &sequence) {
for (auto &change : sequence) {
update_cf(change);
}
return get_energy();
}
void CEUpdater::read_trans_matrix(PyObject *py_trans_mat) {
bool is_list = PyList_Check(py_trans_mat);
#ifdef PRINT_DEBUG
cout << "read_trans_matrix: Extracting unique indices" << endl;
#endif
set<int> unique_indx;
clusters.unique_indices(unique_indx);
vector<int> unique_indx_vec;
set2vector(unique_indx, unique_indx_vec);
// Compute the max index that is ever going to be checked
unsigned int max_indx = clusters.max_index();
if (is_list) {
unsigned int size = list_size(py_trans_mat);
#ifdef PRINT_DEBUG
cout << "read_trans_matrix: Updating size of trans_matrix" << endl;
#endif
trans_matrix.set_size(size, unique_indx_vec.size(), max_indx);
#ifdef PRINT_DEBUG
cout << "read_trans_matrix: Setting lookup values in trans_matrix" << endl;
#endif
trans_matrix.set_lookup_values(unique_indx_vec);
#ifdef PRINT_DEBUG
cout << "read_trans_matrix: Reading translation matrix from list of dictionaries" << endl;
#endif
unsigned int n_elements_insterted = 0;
for (unsigned int i = 0; i < size; i++) {
// Background atoms are ignored (and should never be accessed)
if (is_background_index[i] && ignore_background_indices) {
continue;
}
PyObject *dict = PyList_GetItem(py_trans_mat, i);
for (unsigned int j = 0; j < unique_indx_vec.size(); j++) {
int col = unique_indx_vec[j];
PyObject *value = PyDict_GetItem(dict, int2py(col));
if (value == NULL) {
stringstream ss;
ss << "Requested value " << col << " is not a key in the dictionary!";
throw invalid_argument(ss.str());
}
trans_matrix(i, col) = py2int(value);
n_elements_insterted++;
}
}
#ifdef PRINT_DEBUG
cout << "Inserted " << n_elements_insterted << " into the translation matrix\n";
#endif
} else {
// Safety, this should no longer be possible to hit.
throw std::runtime_error("trans_matrix must be a list");
}
}
void CEUpdater::sort_indices(int indices[], const vector<int> &order, unsigned int n_indices) {
// This function is called many times
// profiling (with YEP) revealed that
// [] operator of the vector used quite a bit of time
// Therefore we here use raw C-arrays or pointer arithmetics
int sorted[4];
const int *ptr = &order[0];
for (unsigned int i = 0; i < n_indices; i++) {
sorted[i] = indices[*(ptr + i)];
}
memcpy(indices, sorted, n_indices * sizeof(int));
}
bool CEUpdater::is_swap_move(const swap_move &move) const {
return (move[0].old_symb == move[1].new_symb) && (move[1].new_symb == move[1].old_symb);
}
void CEUpdater::read_background_indices(PyObject *bkg_indices) {
// Fill array with false
is_background_index.resize(symbols_with_id->size());
fill(is_background_index.begin(), is_background_index.end(), false);
// Set to true if index is in bkg_indices
int size = list_size(bkg_indices);
for (int i = 0; i < size; i++) {
PyObject *py_indx = PyList_GetItem(bkg_indices, i);
int indx = py2int(py_indx);
is_background_index[indx] = true;
}
}
void CEUpdater::count_non_bkg_sites() {
// Count and store the number of non-background sites
num_non_bkg_sites = 0;
for (unsigned int atom_no = 0; atom_no < symbols_with_id->size(); atom_no++) {
if (!is_background_index[atom_no] || !ignore_background_indices) {
num_non_bkg_sites += 1;
}
}
}
void CEUpdater::get_changes(const std::vector<std::string> &new_symbols,
std::vector<unsigned int> &changed_sites) const {
if (new_symbols.size() != symbols_with_id->size()) {
throw invalid_argument("Size of passed atoms does not match!");
}
for (unsigned int i = 0; i < new_symbols.size(); i++) {
unsigned int symb_id = symbols_with_id->id(i);
if (symbols_with_id->get_symbol_id(new_symbols[i]) != symb_id) {
changed_sites.push_back(i);
}
}
}
void CEUpdater::calculate_cf_from_scratch(const vector<string> &cf_names, map<string, double> &cf) {
cf.clear();
// Initialise all cluster names
for (const string &name : cf_names) {
cf[name] = 0.0;
}
// Loop over all clusters
for (const string &name : cf_names) {
ClusterName c_name = ClusterName(name);
unsigned int cluster_size = c_name.get_size();
// Handle empty cluster
if (cluster_size == 0) {
cf[name] = 1.0;
continue;
}
// Handle singlet cluster
if (cluster_size == 1) {
unsigned int dec = c_name.get_dec_num();
double new_value = 0.0;
// Normalise with respect to the actual number of atoms included
for (unsigned int atom_no = 0; atom_no < symbols_with_id->size(); atom_no++) {
if (!is_background_index[atom_no] || !ignore_background_indices) {
new_value += basis_functions->get(dec, symbols_with_id->id(atom_no));
}
}
cf[name] = new_value / num_non_bkg_sites;
continue;
}
// Handle the rest of the clusters
std::string prefix, dec_str;
c_name.get_prefix_and_dec_str(prefix, dec_str);
double sp = 0.0;
double count = 0;
for (unsigned int atom_no = 0; atom_no < symbols_with_id->size(); atom_no++) {
int symm = trans_symm_group[atom_no];
if ((!clusters.is_in_symm_group(prefix, symm)) ||
(is_background_index[atom_no] && ignore_background_indices)) {
continue;
}
const Cluster &cluster = clusters.get(prefix, symm);
const equiv_deco_t &equiv_deco = cluster.get_equiv_deco(dec_str);
unsigned int ref_id = symbols_with_id->id(atom_no);
double sp_temp = 0.0;
for (const vector<int> &deco : equiv_deco) {
sp_temp += spin_product_one_atom(atom_no, cluster, deco, ref_id);
}
sp += sp_temp / equiv_deco.size();
count += cluster.get().size();
}
if (count == 0) {
cf[name] = 0.0;
} else {
cf[name] = sp / count;
}
}
history->get_current().init(cf);
}
void CEUpdater::set_atoms(PyObject *py_atoms) {
unsigned int num_atoms = PySequence_Length(py_atoms);
if (num_atoms != symbols_with_id->size()) {
throw invalid_argument("Length of passed atoms object is different from current");
}
std::vector<std::string> symbols = get_symbols_from_atoms(py_atoms);
this->atoms = py_atoms;
symbols_with_id->set_symbols(symbols);
}
+30
-25

@@ -1,4 +0,4 @@

Metadata-Version: 2.1
Metadata-Version: 2.4
Name: clease
Version: 1.1.0
Version: 1.2.0
Summary: CLuster Expansion in Atomistic Simulation Environment

@@ -14,13 +14,10 @@ Home-page: https://gitlab.com/computationalmaterials/clease/

Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: Topic :: Scientific/Engineering :: Chemistry
Requires-Python: >=3.7
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE.md
Requires-Dist: ase>=3.22
Requires-Dist: numpy<2
Requires-Dist: numpy
Requires-Dist: cython

@@ -52,5 +49,4 @@ Requires-Dist: matplotlib

Requires-Dist: twine; extra == "dev"
Requires-Dist: black>=22.1.0; extra == "dev"
Requires-Dist: clang-format>=14.0.3; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: clang-format==21.1.5; extra == "dev"
Requires-Dist: ruff==0.14.5; extra == "dev"
Requires-Dist: pyclean>=2.0.0; extra == "dev"

@@ -62,21 +58,22 @@ Requires-Dist: pytest-cov; extra == "dev"

Provides-Extra: all
Requires-Dist: black>=22.1.0; extra == "all"
Requires-Dist: pyclean>=2.0.0; extra == "all"
Requires-Dist: pre-commit; extra == "all"
Requires-Dist: ipython; extra == "all"
Requires-Dist: sphinx; extra == "all"
Requires-Dist: pytest; extra == "all"
Requires-Dist: twine; extra == "all"
Requires-Dist: cython; extra == "all"
Requires-Dist: ruff==0.14.5; extra == "all"
Requires-Dist: pytest-benchmark[histogram]>=3.4.1; extra == "all"
Requires-Dist: mock; extra == "all"
Requires-Dist: twine; extra == "all"
Requires-Dist: ruff; extra == "all"
Requires-Dist: clang-format==21.1.5; extra == "all"
Requires-Dist: sphinx_rtd_theme; extra == "all"
Requires-Dist: pre-commit; extra == "all"
Requires-Dist: pyclean>=2.0.0; extra == "all"
Requires-Dist: pip; extra == "all"
Requires-Dist: pytest-cov; extra == "all"
Requires-Dist: clease-gui; extra == "all"
Requires-Dist: tox>=4; extra == "all"
Requires-Dist: pytest-mock; extra == "all"
Requires-Dist: build; extra == "all"
Requires-Dist: sphinx; extra == "all"
Requires-Dist: pytest-mock; extra == "all"
Requires-Dist: pytest; extra == "all"
Requires-Dist: cython; extra == "all"
Requires-Dist: ipython; extra == "all"
Requires-Dist: clang-format>=14.0.3; extra == "all"
Requires-Dist: tox>=4; extra == "all"
Requires-Dist: pytest-benchmark[histogram]>=3.4.1; extra == "all"
Requires-Dist: pytest-cov; extra == "all"
Dynamic: license-file
Dynamic: provides-extra

@@ -94,2 +91,10 @@ # CLEASE

# Documentation
The updated version of the CLEASE documentation is now available at:
https://www.clease.org
Please visit the new site for the latest documentation, tutorials, and resources.
# Installation

@@ -96,0 +101,0 @@

ase>=3.22
numpy<2
numpy
cython

@@ -16,21 +16,20 @@ matplotlib

[all]
black>=22.1.0
pyclean>=2.0.0
pre-commit
ipython
sphinx
pytest
twine
cython
ruff==0.14.5
pytest-benchmark[histogram]>=3.4.1
mock
twine
ruff
clang-format==21.1.5
sphinx_rtd_theme
pre-commit
pyclean>=2.0.0
pip
pytest-cov
clease-gui
tox>=4
pytest-mock
build
sphinx
pytest-mock
pytest
cython
ipython
clang-format>=14.0.3
tox>=4
pytest-benchmark[histogram]>=3.4.1
pytest-cov

@@ -43,5 +42,4 @@ [dev]

twine
black>=22.1.0
clang-format>=14.0.3
ruff
clang-format==21.1.5
ruff==0.14.5
pyclean>=2.0.0

@@ -48,0 +46,0 @@ pytest-cov

@@ -153,2 +153,3 @@ LICENSE.md

cxx/src/basis_function.cpp
cxx/src/ce_updater.cpp
cxx/src/cf_history_tracker.cpp

@@ -162,3 +163,2 @@ cxx/src/cluster.cpp

cxx/src/symbols_with_numbers.cpp
cxx/src/with_numpy/ce_updater.cpp
doc/source/acknowledgements.rst

@@ -165,0 +165,0 @@ doc/source/api_doc.rst

@@ -1,1 +0,1 @@

1.1.0
1.2.0
"""Module for setting up pseudospins and basis functions."""
from abc import ABC, abstractmethod
from collections.abc import Sequence
import math
from typing import Dict, List, Optional, Sequence

@@ -38,3 +39,3 @@ import numpy as np

@property
def unique_elements(self) -> List[str]:
def unique_elements(self) -> list[str]:
return self._unique_elements

@@ -51,7 +52,7 @@

@property
def spin_dict(self) -> Dict[str, int]:
def spin_dict(self) -> dict[str, int]:
return self.get_spin_dict()
@property
def basis_functions(self) -> List[Dict[str, float]]:
def basis_functions(self) -> list[dict[str, float]]:
"""Property access to :meth:`get_basis_functions`."""

@@ -89,3 +90,3 @@ return self.get_basis_functions()

def get_spin_dict(self) -> Dict[str, int]:
def get_spin_dict(self) -> dict[str, int]:
"""Define pseudospins for all consistuting elements."""

@@ -99,3 +100,3 @@ gram_schmidt = GramSchmidtMonimial(self.num_unique_elements)

def get_basis_functions(self) -> List[Dict[str, float]]:
def get_basis_functions(self) -> list[dict[str, float]]:
"""Create basis functions to guarantee the orthonormality."""

@@ -118,3 +119,3 @@ gram_schmidt = GramSchmidtMonimial(self.num_unique_elements)

def get_spin_dict(self) -> Dict[str, int]:
def get_spin_dict(self) -> dict[str, int]:
"""Define pseudospins for all consistuting elements."""

@@ -127,3 +128,3 @@ spin_values = list(range(self.num_unique_elements))

def get_basis_functions(self) -> List[Dict[str, float]]:
def get_basis_functions(self) -> list[dict[str, float]]:
"""Create basis functions to guarantee the orthonormality."""

@@ -175,3 +176,3 @@ alpha = list(range(1, self.num_unique_elements))

def __init__(self, unique_elements: List[str], redundant_element: Optional[str] = "auto"):
def __init__(self, unique_elements: list[str], redundant_element: str | None = "auto"):
super().__init__(unique_elements)

@@ -183,3 +184,3 @@ if redundant_element == "auto":

def get_spin_dict(self) -> Dict[str, int]:
def get_spin_dict(self) -> dict[str, int]:
"""Define pseudospins for all consistuting elements."""

@@ -192,3 +193,3 @@ spin_values = list(range(self.num_unique_elements))

def get_basis_functions(self) -> List[Dict[str, float]]:
def get_basis_functions(self) -> list[dict[str, float]]:
"""Create orthonormal basis functions.

@@ -195,0 +196,0 @@

@@ -1,2 +0,2 @@

from typing import Dict, Iterable, List, Optional, Sequence, Set, Union
from collections.abc import Iterable, Sequence

@@ -83,5 +83,5 @@ from ase import Atoms

settings: ClusterExpansionSettings,
eci: Dict[str, float],
vol_coeff: Dict[str, float],
init_cf: Optional[Dict[str, float]] = None,
eci: dict[str, float],
vol_coeff: dict[str, float],
init_cf: dict[str, float] | None = None,
):

@@ -92,5 +92,3 @@ if not eci_format_ok(eci.keys()):

if not vol_coeff_format_ok(vol_coeff.keys()):
raise ValueError(
"Invalid format of volume coefficient names. " f"Got\n{vol_coeff.keys()}"
)
raise ValueError(f"Invalid format of volume coefficient names. Got\n{vol_coeff.keys()}")

@@ -116,3 +114,3 @@ cf_to_track = unique_eci_names_no_vol(eci.keys())

def get_volume(self, cf: Optional[Dict[str, float]] = None) -> float:
def get_volume(self, cf: dict[str, float] | None = None) -> float:
"""

@@ -133,3 +131,3 @@ Returns the volume per atom

def get_pressure(self, cf: Optional[Dict[str, float]] = None) -> float:
def get_pressure(self, cf: dict[str, float] | None = None) -> float:
"""

@@ -155,3 +153,3 @@ Return the pressure

def get_bulk_modulus(self, cf: Optional[Dict[str, float]] = None) -> float:
def get_bulk_modulus(self, cf: dict[str, float] | None = None) -> float:
"""

@@ -173,3 +171,3 @@ Return the bulk modulus of the current atoms object

def _d2EdV2(self, cf: Dict[str, float], vol: float) -> float:
def _d2EdV2(self, cf: dict[str, float], vol: float) -> float:
"""

@@ -188,3 +186,3 @@ Return the double derivative of the energy with respect

def _d3EdV3(self, cf: Dict[str, float], vol: float) -> float:
def _d3EdV3(self, cf: dict[str, float], vol: float) -> float:
"""

@@ -208,3 +206,3 @@ Return the third derivative of the energy with respect to

def get_dBdP(self, cf: Optional[Dict[str, float]] = None) -> float:
def get_dBdP(self, cf: dict[str, float] | None = None) -> float:
"""

@@ -243,5 +241,5 @@ Return the pressure derivative of the bulk modulus of the

self,
atoms: Optional[Atoms] = None,
properties: Optional[List[str]] = None,
system_changes: Union[Sequence[SystemChange], None] = None,
atoms: Atoms | None = None,
properties: list[str] | None = None,
system_changes: Sequence[SystemChange] | None = None,
) -> float:

@@ -272,3 +270,3 @@ """Calculate the energy of the passed Atoms object.

def unique_eci_names_no_vol(eci_names: Iterable[str]) -> Set[str]:
def unique_eci_names_no_vol(eci_names: Iterable[str]) -> set[str]:
"""

@@ -275,0 +273,0 @@ Return a set with the unique ECI names without any volume tag

"""Calculator for Cluster Expansion."""
from collections.abc import Sequence
import contextlib
import sys
from typing import Any, Dict, List, Optional, Sequence, TextIO, Union
from typing import Any, TextIO

@@ -52,5 +54,5 @@ from ase import Atoms

settings: ClusterExpansionSettings,
eci: Dict[str, float],
init_cf: Optional[Dict[str, float]] = None,
logfile: Union[TextIO, str, None] = None,
eci: dict[str, float],
init_cf: dict[str, float] | None = None,
logfile: TextIO | str | None = None,
) -> None:

@@ -146,3 +148,3 @@ if not isinstance(settings, ClusterExpansionSettings):

def calculate_cf_from_scratch(self) -> Dict[str, float]:
def calculate_cf_from_scratch(self) -> dict[str, float]:
"""Calculate correlation functions from scratch."""

@@ -162,5 +164,5 @@ self.require_updater()

self,
atoms: Optional[Atoms] = None,
properties: Optional[List[str]] = None,
system_changes: Optional[SystemChanges] = None,
atoms: Atoms | None = None,
properties: list[str] | None = None,
system_changes: SystemChanges | None = None,
) -> float:

@@ -202,5 +204,3 @@ """Calculate the energy of the passed Atoms object.

def get_property(
self, name: str, atoms: Optional[Atoms] = None, allow_calculation: bool = True
):
def get_property(self, name: str, atoms: Atoms | None = None, allow_calculation: bool = True):
"""Get a property from the calculator.

@@ -216,3 +216,3 @@

def get_potential_energy(self, atoms: Optional[Atoms] = None) -> float:
def get_potential_energy(self, atoms: Atoms | None = None) -> float:
"""Calculate the energy from scratch with an atoms object"""

@@ -231,5 +231,3 @@ # self.set_atoms(atoms)

def calculation_required(
self, atoms: Atoms, properties: Optional[Sequence[str]] = None
) -> bool:
def calculation_required(self, atoms: Atoms, properties: Sequence[str] | None = None) -> bool:
"""Check whether a calculation is required for a given atoms object.

@@ -247,3 +245,3 @@ The ``properties`` argument only exists for compatibility reasons, and has no effect.

def check_state(self, atoms: Atoms) -> List[str]:
def check_state(self, atoms: Atoms) -> list[str]:
"""Method for checking if energy needs calculation.

@@ -266,3 +264,3 @@ Primarily for ASE compatibility.

@property
def indices_of_changed_atoms(self) -> List[int]:
def indices_of_changed_atoms(self) -> list[int]:
"""Return the indices of atoms that have been changed."""

@@ -277,3 +275,3 @@ changed = self.get_changed_sites(self.atoms)

def get_changed_sites(self, atoms: Atoms) -> List[int]:
def get_changed_sites(self, atoms: Atoms) -> list[int]:
"""Return the list of indices which differ from the internal ones."""

@@ -283,7 +281,7 @@ self.require_updater()

def get_cf(self) -> Dict[str, float]:
def get_cf(self) -> dict[str, float]:
"""Return the correlation functions as a dict"""
return self.updater.get_cf()
def update_cf(self, system_changes: Optional[SystemChanges] = None) -> None:
def update_cf(self, system_changes: SystemChanges | None = None) -> None:
"""Update correlation function based on the reference value.

@@ -315,3 +313,3 @@

@property
def cf(self) -> List[float]:
def cf(self) -> list[float]:
temp_cf = self.updater.get_cf()

@@ -327,3 +325,3 @@ return [temp_cf[x] for x in self.cf_names]

def update_eci(self, eci: Dict[str, float]) -> None:
def update_eci(self, eci: dict[str, float]) -> None:
"""Update the ECI values.

@@ -338,3 +336,4 @@

def get_singlets(self) -> np.ndarray:
return self.updater.get_singlets()
singlets = self.updater.get_singlets()
return np.array(singlets, dtype=float)

@@ -431,3 +430,3 @@ def get_energy(self) -> float:

@property
def parameters(self) -> Dict[str, Any]:
def parameters(self) -> dict[str, Any]:
"""Return a dictionary with relevant parameters."""

@@ -443,3 +442,3 @@ return {"eci": self.eci}

def _check_properties(properties: Optional[List[str]], implemented_properties: List[str]) -> None:
def _check_properties(properties: list[str] | None, implemented_properties: list[str]) -> None:
"""Check whether the passed properties is supported. If it is None, nothing is checked.

@@ -446,0 +445,0 @@ Raises PropertyNotImplementedError upon finding a bad property.

@@ -1,3 +0,1 @@

from typing import Dict, Optional
from ase import Atoms

@@ -14,4 +12,4 @@

atoms: Atoms,
eci: Optional[Dict[str, float]] = None,
num_threads: Optional[int] = None,
eci: dict[str, float] | None = None,
num_threads: int | None = None,
) -> Atoms:

@@ -46,3 +44,3 @@ """Utility function for an efficient initialization of large cells. Will set the atoms

def get_ce_energy(settings: ClusterExpansionSettings, atoms: Atoms, eci: Dict[str, float]) -> float:
def get_ce_energy(settings: ClusterExpansionSettings, atoms: Atoms, eci: dict[str, float]) -> float:
"""Get energy of the ASE Atoms object based on given ECI values.

@@ -49,0 +47,0 @@

@@ -0,4 +1,4 @@

from collections.abc import Sequence
from copy import deepcopy
import sys
from typing import Dict, List, Optional, Sequence, Tuple

@@ -56,3 +56,3 @@ from ase import Atoms

settings: ClusterExpansionSettings,
select_cond: Optional[Sequence[Tuple[str, str, str]]] = None,
select_cond: Sequence[tuple[str, str, str]] | None = None,
):

@@ -67,3 +67,3 @@ # Make copy such that we don't alter the settings object

def _unique_templates(self) -> List[Atoms]:
def _unique_templates(self) -> list[Atoms]:
unique_templates = []

@@ -78,3 +78,3 @@

def coverage(self, template: Atoms) -> Dict[str, float]:
def coverage(self, template: Atoms) -> dict[str, float]:
"""

@@ -101,3 +101,3 @@ Return the cluster coverage for the passed atoms object.

def max_coverage(self) -> Dict[str, float]:
def max_coverage(self) -> dict[str, float]:
"""

@@ -115,3 +115,3 @@ Return the maximum cluster coverage among all templates in the database.

def print_report(self, coverage: Optional[Dict[str, float]] = None, file=sys.stdout) -> None:
def print_report(self, coverage: dict[str, float] | None = None, file=sys.stdout) -> None:
"""

@@ -155,3 +155,3 @@ Prints a nicely formatted report of coverage.

def _grade(coverage: Dict[str, float]) -> str:
def _grade(coverage: dict[str, float]) -> str:
grade = "Very good (coverage > 0.75)"

@@ -158,0 +158,0 @@ for v in coverage.values():

@@ -0,3 +1,4 @@

from collections.abc import Iterable
from functools import total_ordering
from typing import Any, Iterable
from typing import Any

@@ -4,0 +5,0 @@ import attr

@@ -0,4 +1,4 @@

from collections.abc import Iterable, Iterator, Sequence
from itertools import product
from math import sqrt
from typing import Dict, Iterable, Iterator, List, Optional, Sequence, Set, Tuple, Union

@@ -52,3 +52,3 @@ from ase import Atoms

def _as_euc(self, x: Union[np.ndarray, FourVector]) -> np.ndarray:
def _as_euc(self, x: np.ndarray | FourVector) -> np.ndarray:
"""Helper function to translate vector to NumPy array format"""

@@ -62,3 +62,3 @@ if isinstance(x, FourVector):

self, cartesian: np.ndarray, sublattices: Sequence[int]
) -> List[FourVector]:
) -> list[FourVector]:
"""Translate many positions into FourVector's"""

@@ -90,3 +90,3 @@

def to_four_vector(self, cartesian: np.ndarray, sublattice: Optional[int] = None) -> FourVector:
def to_four_vector(self, cartesian: np.ndarray, sublattice: int | None = None) -> FourVector:
"""Translate a position in Cartesian coordinates to its FourVector"""

@@ -128,4 +128,4 @@ if cartesian.ndim != 1:

self,
x1: Union[np.ndarray, FourVector],
x2: Union[np.ndarray, FourVector],
x1: np.ndarray | FourVector,
x2: np.ndarray | FourVector,
) -> float:

@@ -204,3 +204,3 @@ """

self, cutoff: float, lattice: int
) -> Dict[FourVector, Set[FourVector]]:
) -> dict[FourVector, set[FourVector]]:
"""Prepare all sites which are within the cutoff sphere. Note, this only prepares sites

@@ -255,3 +255,3 @@ which are pair-wise within the cutoff, and does not consider the distance to the

self, size: int, cutoff: float, ref_lattice: int
) -> Tuple[List[List[Figure]], List[ClusterFingerprint]]:
) -> tuple[list[list[Figure]], list[ClusterFingerprint]]:
"""Generate all possible figures of a given size, are within a given cutoff radius

@@ -267,7 +267,7 @@ (from the center of mass of the figure), and from a reference lattice.

Returns:
List[List[Figure]], List[ClusterFingerprint]: The collection of figures and
list[list[Figure]], list[ClusterFingerprint]: The collection of figures and
their corresponding fingerprints.
"""
clusters: List[List[Figure]] = []
all_fps: List[ClusterFingerprint] = []
clusters: list[list[Figure]] = []
all_fps: list[ClusterFingerprint] = []

@@ -325,5 +325,5 @@ for new_figure in self.figure_iterator(size, cutoff, ref_lattice):

order = np.lexsort(dists.T)[::-1]
return Figure((fvs[i] for i in order))
return Figure(fvs[i] for i in order)
def to_atom_index(self, cluster: Cluster, lut: Dict[FourVector, int]) -> List[List[int]]:
def to_atom_index(self, cluster: Cluster, lut: dict[FourVector, int]) -> list[list[int]]:
"""

@@ -340,3 +340,3 @@ Convert the integer vector representation to an atomic index

def equivalent_sites(self, figure: Figure) -> List[List[int]]:
def equivalent_sites(self, figure: Figure) -> list[list[int]]:
"""Find the equivalent sites of a figure."""

@@ -352,3 +352,3 @@ dists = self._get_internal_distances(figure, sort=True)

# Merge pairs into groups
merged: List[Set[int]] = []
merged: list[set[int]] = []
for equiv in equiv_sites:

@@ -380,3 +380,3 @@ found_group = False

# Dictionary mapping a ref lattice to a list of four-vectors
self.pre_calc: Dict[int, List[FourVector]] = {}
self.pre_calc: dict[int, list[FourVector]] = {}

@@ -391,3 +391,3 @@ def must_generate(self, cutoff: float, ref_lattice: int) -> bool:

def get(self, cutoff: float, ref_lattice: int) -> List[FourVector]:
def get(self, cutoff: float, ref_lattice: int) -> list[FourVector]:
"""

@@ -404,3 +404,3 @@ Return sites within the cutoff

def site_iterator(
within_cutoff: Dict[FourVector, Set[FourVector]], size: int, ref_lattice: int
within_cutoff: dict[FourVector, set[FourVector]], size: int, ref_lattice: int
) -> Iterator[Figure]:

@@ -407,0 +407,0 @@ """

@@ -6,3 +6,3 @@ from __future__ import annotations

import logging
from typing import Any, Dict, List
from typing import Any

@@ -30,9 +30,9 @@ from ase import Atoms

# Format of the names cache: {num_bf: names}
self._all_cf_name_cache: Dict[int, List[str]] = {}
self._all_cf_name_cache: dict[int, list[str]] = {}
def todict(self) -> Dict[str, Any]:
def todict(self) -> dict[str, Any]:
return {"clusters": self.clusters}
@classmethod
def from_dict(cls, dct: Dict[str, Any]) -> ClusterList:
def from_dict(cls, dct: dict[str, Any]) -> ClusterList:
cluster_lst = cls()

@@ -45,3 +45,3 @@ clusters = dct["clusters"]

@property
def clusters(self) -> List[Cluster]:
def clusters(self) -> list[Cluster]:
return self._clusters

@@ -63,7 +63,7 @@

@property
def names(self) -> List[str]:
def names(self) -> list[str]:
"""Get all names in the cluster list"""
return [cluster.name for cluster in self.clusters]
def get_by_name(self, name) -> List[Cluster]:
def get_by_name(self, name) -> list[Cluster]:
return [cluster for cluster in self.clusters if cluster.name == name]

@@ -79,3 +79,3 @@

def get_by_size(self, size) -> List[Cluster]:
def get_by_size(self, size) -> list[Cluster]:
# Return all clusters with a given size

@@ -120,3 +120,3 @@ return [c for c in self.clusters if c.size == size]

def get_all_cf_names(self, num_bf: int) -> List[str]:
def get_all_cf_names(self, num_bf: int) -> list[str]:
"""

@@ -138,3 +138,3 @@ Return a list of all correlation function names

def _build_all_cf_names(self, num_bf: int) -> List[str]:
def _build_all_cf_names(self, num_bf: int) -> list[str]:
if not isinstance(num_bf, int):

@@ -199,3 +199,3 @@ raise TypeError(f"Number of basis functions must be integer, got {num_bf}")

def tolist(self) -> List[Cluster]:
def tolist(self) -> list[Cluster]:
"""Returns a copy of the ClusterList as a regular list."""

@@ -224,3 +224,3 @@ return list(self.clusters)

def multiplicity_factors(self, num_sites_per_group: List[int]) -> Dict[str, float]:
def multiplicity_factors(self, num_sites_per_group: list[int]) -> dict[str, float]:
mult_factors = {}

@@ -245,5 +245,5 @@ norm = {}

def get_figures(self, generator: ClusterGenerator) -> List[Atoms]:
def get_figures(self, generator: ClusterGenerator) -> list[Atoms]:
"""Get the figures (in their ASE Atoms object representation)"""
figures: List[Atoms] = []
figures: list[Atoms] = []
self.sort()

@@ -250,0 +250,0 @@ # We want to skip c0 and c1 anyways

@@ -0,1 +1,2 @@

from collections.abc import Callable, Iterator, Sequence
from copy import deepcopy

@@ -5,3 +6,2 @@ import functools

import logging
from typing import Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple

@@ -35,3 +35,3 @@ import ase

def __init__(self, prim_cell: ase.Atoms, background_syms: Optional[Set[str]] = None):
def __init__(self, prim_cell: ase.Atoms, background_syms: set[str] | None = None):
self._background_syms = background_syms or set()

@@ -56,3 +56,3 @@

@property
def background_syms(self) -> Set[str]:
def background_syms(self) -> set[str]:
"""The symbols which are considered background."""

@@ -118,4 +118,4 @@ return self._background_syms

num_lattices = range(len(self.prim))
all_fps: List[ClusterFingerprint] = []
all_figures: List[List[Figure]] = []
all_fps: list[ClusterFingerprint] = []
all_figures: list[list[Figure]] = []
lattices = []

@@ -233,3 +233,3 @@

def unique_four_vectors(self) -> Set[FourVector]:
def unique_four_vectors(self) -> set[FourVector]:
"""

@@ -250,3 +250,3 @@ Return a list with all unique 4-vectors which are

def get_figures(self) -> List[ase.Atoms]:
def get_figures(self) -> list[ase.Atoms]:
"""

@@ -257,3 +257,3 @@ Return a list of atoms object representing the clusters

def make_all_four_vectors(self, atoms: ase.Atoms) -> Tuple[np.ndarray, List[FourVector]]:
def make_all_four_vectors(self, atoms: ase.Atoms) -> tuple[np.ndarray, list[FourVector]]:
"""Construct the FourVector to every site which is not a background site.

@@ -273,3 +273,3 @@

def create_four_vector_lut(self, template: ase.Atoms) -> Dict[FourVector, int]:
def create_four_vector_lut(self, template: ase.Atoms) -> dict[FourVector, int]:
"""

@@ -290,3 +290,3 @@ Construct a lookup table (LUT) for the index in template given the

self, template: ase.Atoms, unique: Sequence[FourVector]
) -> Dict[FourVector, int]:
) -> dict[FourVector, int]:
"""Translate a set of unique FourVectors into their corresponding index

@@ -307,3 +307,3 @@ in a template atoms object."""

def translation_matrix(self, template: ase.Atoms) -> List[Dict[int, int]]:
def translation_matrix(self, template: ase.Atoms) -> list[dict[int, int]]:
"""

@@ -336,3 +336,3 @@ Construct the translation matrix.

def _make_site_mapping(index: int) -> Dict[int, int]:
def _make_site_mapping(index: int) -> dict[int, int]:
"""Helper function to calculate the translation mapping for each

@@ -399,3 +399,3 @@ atomic site."""

unique_xyz: np.ndarray,
sublattices: List[int],
sublattices: list[int],
rep_arr: np.ndarray,

@@ -415,3 +415,3 @@ ) -> Iterator[FourVector]:

cell: np.ndarray,
cell_T_inv: Optional[np.ndarray] = None,
cell_T_inv: np.ndarray | None = None,
) -> Iterator[FourVector]:

@@ -436,4 +436,4 @@ """Generalized FourVector wrapping function."""

max_cluster_dia: Sequence[float],
index_by_sublattice: List[List[int]],
) -> Tuple[ClusterList, TransMatrix]:
index_by_sublattice: list[list[int]],
) -> tuple[ClusterList, TransMatrix]:
"""Create a ClusterList and a TransMatrix object, and calculate the norm factors."""

@@ -458,3 +458,3 @@ # Ensure that we have built the clusters for the cutoff

def _set_norm_factors(
index_by_sublattice: List[List[int]],
index_by_sublattice: list[list[int]],
trans_matrix: TransMatrix,

@@ -461,0 +461,0 @@ cluster_list: ClusterList,

@@ -0,3 +1,4 @@

from collections.abc import Sequence
from functools import total_ordering
from typing import Any, Dict, List, Sequence
from typing import Any

@@ -34,3 +35,3 @@ from ase import Atoms

info: Dict[str, Any] = attr.field(default=attr.Factory(dict))
info: dict[str, Any] = attr.field(default=attr.Factory(dict))
# "indices" are the integer index representation of the Figures.

@@ -48,3 +49,3 @@ # therefore, "indices" and "ref_indx" depend on the currently active template,

raise TypeError(
f"All values must Figure type, got {value} " f"of type {type(v)} in index {ii}."
f"All values must Figure type, got {value} of type {type(v)} in index {ii}."
)

@@ -94,3 +95,3 @@

def _order_equiv_sites(self, figure: Sequence[int]) -> List[int]:
def _order_equiv_sites(self, figure: Sequence[int]) -> list[int]:
"""Sort equivalent sites of a figure in index representation."""

@@ -106,3 +107,3 @@ figure_cpy = list(figure)

@property
def num_fig_occurences(self) -> Dict[str, int]:
def num_fig_occurences(self) -> dict[str, int]:
"""Number of ocurrences for each figures."""

@@ -120,3 +121,3 @@ occ_count = {}

target_figure: Sequence[int],
trans_matrix: List[Dict[int, int]],
trans_matrix: list[dict[int, int]],
):

@@ -146,7 +147,6 @@ """Find figures that correspond to another reference index.

raise RuntimeError(
f"There are no matching figure for ref_indx: "
f"{ref_indx} and figure: {target_figure}!"
f"There are no matching figure for ref_indx: {ref_indx} and figure: {target_figure}!"
)
def get_all_figure_keys(self) -> List[str]:
def get_all_figure_keys(self) -> list[str]:
return [self.get_figure_key(fig) for fig in self.indices]

@@ -153,0 +153,0 @@

@@ -1,3 +0,1 @@

from typing import Optional
from ase.db import connect

@@ -48,3 +46,3 @@ import matplotlib.pyplot as plt

conc_scale=1.0,
conc_ranges: Optional[dict] = None,
conc_ranges: dict | None = None,
):

@@ -104,3 +102,3 @@ if conc_ranges is None:

v[f"{k2}_conc"] = 0.0
v["energy"] = np.infty
v["energy"] = np.inf

@@ -107,0 +105,0 @@ # Iterate through all rows, update

"""Module for calculating correlation functions."""
from collections.abc import Iterator
import logging
from typing import Any, Dict, Iterator, Tuple
from typing import Any

@@ -16,3 +18,3 @@ from ase.atoms import Atoms

# Type alias for a Correlation function
CF_T = Dict[str, float]
CF_T = dict[str, float]

@@ -148,3 +150,3 @@

def iter_reconfigure_db_entries(self, select_cond=None) -> Iterator[Tuple[int, int, int]]:
def iter_reconfigure_db_entries(self, select_cond=None) -> Iterator[tuple[int, int, int]]:
"""Iterator which reconfigures the correlation function values in the DB,

@@ -156,3 +158,3 @@ which yields after each reconfiguration and reports on the progress.

Yields:
Tuple[int, int, int]: (row_id, count, total) A tuple containing the ID
tuple[int, int, int]: (row_id, count, total) A tuple containing the ID
of the row which was just reconfigured, current

@@ -159,0 +161,0 @@ count which has been reconfigured, as well as the total number of

from abc import ABC, abstractmethod
from collections import defaultdict
from collections.abc import Sequence
from itertools import combinations_with_replacement as cwr
import logging
import sqlite3
from typing import Dict, List, Optional, Sequence, Set, Tuple

@@ -81,3 +81,3 @@ from ase.db import connect

@abstractmethod
def get_data(self, select_cond: List[tuple]) -> Tuple[np.ndarray, np.ndarray]:
def get_data(self, select_cond: list[tuple]) -> tuple[np.ndarray, np.ndarray]:
"""

@@ -88,4 +88,4 @@ Return the design matrix X and the target data y

def _get_data_from_getters(
self, select_cond: List[tuple], feature_getter: FeatureGetter, target_getter: TargetGetter
) -> Tuple[np.ndarray, np.ndarray]:
self, select_cond: list[tuple], feature_getter: FeatureGetter, target_getter: TargetGetter
) -> tuple[np.ndarray, np.ndarray]:
"""

@@ -178,3 +178,3 @@ Return the design matrix X and the target data y

def get_matching_names(self, pattern: str) -> List[str]:
def get_matching_names(self, pattern: str) -> list[str]:
"""

@@ -192,3 +192,3 @@ Get names that matches pattern

def get_cols(self, names: List[str]) -> np.ndarray:
def get_cols(self, names: list[str]) -> np.ndarray:
"""

@@ -203,3 +203,3 @@ Get all columns corresponding to the names

def groups(self) -> List[int]:
def groups(self) -> list[int]:
"""

@@ -235,3 +235,3 @@ Returns the group of each item in the X matrix. In the top-level

tab_name: str,
cf_names: Optional[List[str]] = None,
cf_names: list[str] | None = None,
order: int = 1,

@@ -244,3 +244,3 @@ ) -> None:

def get_data(self, select_cond: List[tuple]) -> Tuple[np.ndarray, np.ndarray]:
def get_data(self, select_cond: list[tuple]) -> tuple[np.ndarray, np.ndarray]:
"""

@@ -284,3 +284,3 @@ Return X and y, where X is the design matrix containing correlation

tab_name: str,
cf_names: Optional[List[str]] = None,
cf_names: list[str] | None = None,
order: int = 1,

@@ -294,3 +294,3 @@ ) -> None:

def get_data(self, select_cond: List[tuple]) -> Tuple[np.ndarray, np.ndarray]:
def get_data(self, select_cond: list[tuple]) -> tuple[np.ndarray, np.ndarray]:
"""

@@ -328,3 +328,3 @@ Return X and y, where X is the design matrix containing correlation

def __init__(
self, db_name: str, tab_name: str, cf_names: Optional[List[str]] = None, order: int = 1
self, db_name: str, tab_name: str, cf_names: list[str] | None = None, order: int = 1
) -> None:

@@ -350,3 +350,3 @@ super().__init__(db_name)

@staticmethod
def _is_matrix_representable(id_cf_names: Dict[int, List[str]]) -> bool:
def _is_matrix_representable(id_cf_names: dict[int, list[str]]) -> bool:
"""

@@ -356,3 +356,3 @@ Check if the extracted correlation functions can be represented as a

"""
reference = sorted(list(id_cf_names.values())[0])
reference = sorted(next(iter(id_cf_names.values())))

@@ -370,3 +370,3 @@ # Check the names of the correlation functions is equal for

@staticmethod
def _minimum_common_cf_set(id_cf_names: Dict[int, List[str]]) -> Set[str]:
def _minimum_common_cf_set(id_cf_names: dict[int, list[str]]) -> set[str]:
"""

@@ -376,3 +376,3 @@ Returns the minimum set of correlation functions that exists for all

"""
common_cf = set(list(id_cf_names.values())[0])
common_cf = set(next(iter(id_cf_names.values())))
for v in id_cf_names.values():

@@ -665,3 +665,3 @@ common_cf = common_cf.intersection(v)

tab_name: str,
cf_names: Optional[List[str]] = None,
cf_names: list[str] | None = None,
order: int = 1,

@@ -674,3 +674,3 @@ ) -> None:

def get_data(self, select_cond: List[tuple]) -> Tuple[np.ndarray, np.ndarray]:
def get_data(self, select_cond: list[tuple]) -> tuple[np.ndarray, np.ndarray]:
"""

@@ -693,3 +693,3 @@ Return X and y, where X is the design matrix containing correlation

def extract_num_atoms(cur: sqlite3.Cursor, ids: Set[int]) -> Dict[int, int]:
def extract_num_atoms(cur: sqlite3.Cursor, ids: set[int]) -> dict[int, int]:
"""

@@ -744,5 +744,5 @@ Extract the number of atoms for all ids

tab_name: str,
cf_names: List[str],
order: Optional[int] = 0,
properties: Tuple[str] = ("energy", "pressure"),
cf_names: list[str],
order: int | None = 0,
properties: tuple[str] = ("energy", "pressure"),
cf_order: int = 1,

@@ -758,3 +758,3 @@ ) -> None:

def build(self, ids: List[int]) -> np.ndarray:
def build(self, ids: list[int]) -> np.ndarray:
"""

@@ -877,3 +877,3 @@ Construct the design matrix and the target value required to fit a

def _extract_key(self, ids: Set[int], key: str) -> Dict[int, float]:
def _extract_key(self, ids: set[int], key: str) -> dict[int, float]:
"""

@@ -900,3 +900,3 @@ Extract a key from the database for the ids in the passed set that

def get_data(self, select_cond: List[tuple]) -> Tuple[np.ndarray, np.ndarray]:
def get_data(self, select_cond: list[tuple]) -> tuple[np.ndarray, np.ndarray]:
"""

@@ -914,3 +914,3 @@ Return the design matrix and the target values for the entries

def groups(self) -> List[int]:
def groups(self) -> list[int]:
"""

@@ -917,0 +917,0 @@ Return the group of each rows.

@@ -1,3 +0,1 @@

from typing import Tuple
import numpy as np

@@ -24,3 +22,3 @@

def normalize(self, X: np.ndarray, y: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
def normalize(self, X: np.ndarray, y: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""

@@ -27,0 +25,0 @@ Normalizes the each column of X to zero mean and unit variance. y is

"""This module defines the "Figure" class, which is a collection of FourVector objects."""
from typing import Any, Iterable, Optional, Tuple
from collections.abc import Iterable
from typing import Any
import ase

@@ -45,3 +47,3 @@ import attr

components: Tuple[FourVector] = attr.field(
components: tuple[FourVector] = attr.field(
converter=_convert_figure, validator=attr.validators.instance_of(tuple)

@@ -57,8 +59,7 @@ )

raise TypeError(
f"All values must FourVector type, got {value} "
f"of type {type(v)} in index {ii}."
f"All values must FourVector type, got {value} of type {type(v)} in index {ii}."
)
def to_cartesian(
self, prim: ase.Atoms, transposed_cell: Optional[np.ndarray] = None
self, prim: ase.Atoms, transposed_cell: np.ndarray | None = None
) -> np.ndarray:

@@ -84,3 +85,3 @@ """Get the Figure in terms of the cartesian coordinates, as defined

def get_diameter(self, prim: ase.Atoms, transposed_cell: Optional[np.ndarray] = None) -> float:
def get_diameter(self, prim: ase.Atoms, transposed_cell: np.ndarray | None = None) -> float:
"""Calculate the diameter of the figure, as the maximum distance to the

@@ -87,0 +88,0 @@ geometric center of the figure in cartesian coordinates.

@@ -6,3 +6,3 @@ from __future__ import annotations

from itertools import product
from typing import List, NamedTuple, Optional, Tuple
from typing import NamedTuple

@@ -34,3 +34,3 @@ from ase import Atoms

def to_cartesian(self, prim: Atoms, transposed_cell: Optional[np.ndarray] = None) -> np.ndarray:
def to_cartesian(self, prim: Atoms, transposed_cell: np.ndarray | None = None) -> np.ndarray:
"""Convert the four vector into cartesian coordinates

@@ -67,3 +67,3 @@

def to_tuple(self) -> Tuple[int]:
def to_tuple(self) -> tuple[int]:
"""Get the tuple representation of the four-vector"""

@@ -136,3 +136,3 @@ return attr.astuple(self)

def _make_grid(bbox: _Box, prim: Atoms) -> Tuple[List[FourVector], np.ndarray]:
def _make_grid(bbox: _Box, prim: Atoms) -> tuple[list[FourVector], np.ndarray]:
"""

@@ -165,3 +165,3 @@ Make a grid bounded by bbox using repetitions of the atomic positions in prim

def construct_four_vectors(prim: Atoms, supercell: Atoms) -> List[FourVector]:
def construct_four_vectors(prim: Atoms, supercell: Atoms) -> list[FourVector]:
"""

@@ -168,0 +168,0 @@ Calculate the all four vector corresponding to the passed Atoms object. A four-vector u is

@@ -1,2 +0,2 @@

from typing import Any, Dict
from typing import Any

@@ -38,3 +38,3 @@ import attr

# Is not included in equality comparison.
other: Dict[str, Any] = attr.field(default=attr.Factory(dict), eq=False)
other: dict[str, Any] = attr.field(default=attr.Factory(dict), eq=False)

@@ -41,0 +41,0 @@ @property

@@ -0,3 +1,3 @@

from collections.abc import Sequence
import typing
from typing import Sequence, Tuple

@@ -54,3 +54,3 @@ import ase

def astuple(self) -> Tuple[int, str, str, str]:
def astuple(self) -> tuple[int, str, str, str]:
"""Return the tuple representation of the SystemChange."""

@@ -57,0 +57,0 @@ return attr.astuple(self, recurse=False)

from __future__ import annotations
from typing import Dict, List
import attr

@@ -19,3 +17,3 @@ import numpy as np

trans_matrix: List[Dict[int, int]] = attr.field()
trans_matrix: list[dict[int, int]] = attr.field()

@@ -22,0 +20,0 @@ def key_array(self) -> np.ndarray:

from collections import defaultdict
from collections.abc import Sequence
from datetime import datetime
import logging
from typing import Dict, List, Sequence, Set, Union

@@ -51,3 +51,3 @@ import ase

# Type aliasing
_TABLE = Dict[str, Union[int, float, str, np.integer, np.floating]] # Alias for a table
_TABLE = dict[str, int | float | str | np.integer | np.floating] # Alias for a table
_ROW = ase.db.row.AtomsRow

@@ -239,6 +239,4 @@ _CONNECTION = ase.db.core.Database

raise ValueError(
(
f"Number of table names should match number of tables. Got {len(table_names)} "
f"table names, but {len(tables)} tables."
)
f"Number of table names should match number of tables. Got {len(table_names)} "
f"table names, but {len(tables)} tables."
)

@@ -287,3 +285,3 @@

def get_all_cf_names(db_name: str, tab_name: str) -> Set[str]:
def get_all_cf_names(db_name: str, tab_name: str) -> set[str]:
"""

@@ -309,3 +307,3 @@ Return a list with all correlation function names

def get_all_cf(db_name: str, tab_name: str, db_id: int) -> Dict[str, float]:
def get_all_cf(db_name: str, tab_name: str, db_id: int) -> dict[str, float]:
"""

@@ -333,3 +331,3 @@ Return all correlation functions associated with an entry in the database

def get_cf_tables(db_name: str) -> List[str]:
def get_cf_tables(db_name: str) -> list[str]:
"""

@@ -336,0 +334,0 @@ Return a list with table names that contain correlation functions

"""Module that fits ECIs to energy data."""
from collections import Counter, defaultdict
from collections.abc import Sequence
import json

@@ -8,3 +10,2 @@ import logging as lg

import sys
from typing import Dict, List, Optional, Sequence

@@ -107,3 +108,3 @@ from ase.db import connect

num_repetitions=1,
normalization_symbols: Optional[Sequence[str]] = None,
normalization_symbols: Sequence[str] | None = None,
):

@@ -179,3 +180,3 @@ """Initialize the Evaluate class."""

def set_normalization(self, normalization_symbols: Optional[Sequence[str]] = None) -> None:
def set_normalization(self, normalization_symbols: Sequence[str] | None = None) -> None:
"""Set the energy normalization factor, e.g. to normalize the final energy reports

@@ -345,3 +346,3 @@ in energy per metal atom, rather than energy per atom (i.e. every atom).

def get_eci_dict(self, cutoff_tol: float = 1e-14) -> Dict[str, float]:
def get_eci_dict(self, cutoff_tol: float = 1e-14) -> dict[str, float]:
"""Determine cluster names and their corresponding ECI value and return

@@ -355,3 +356,3 @@ them in a dictionary format.

Returns:
Dict[str, float]: Dictionary with the CF names and the corresponding
dict[str, float]: Dictionary with the CF names and the corresponding
ECI value.

@@ -369,3 +370,3 @@ """

def load_eci_dict(self, eci_dict: Dict[str, float]) -> None:
def load_eci_dict(self, eci_dict: dict[str, float]) -> None:
"""Load the ECI's from a dictionary. Any ECI's which are missing

@@ -392,3 +393,3 @@ from the internal cf_names list are assumed to be 0.

full_fname = add_file_extension(fname, ".json")
with open(full_fname, "r") as fd:
with open(full_fname) as fd:
self.load_eci_dict(json.load(fd))

@@ -485,3 +486,3 @@

0.01,
cv_name + f" = {cv:.3f} meV/atom \n" f"RMSE = {self.rmse() * 1000.0:.3f} meV/atom",
cv_name + f" = {cv:.3f} meV/atom \nRMSE = {self.rmse() * 1000.0:.3f} meV/atom",
verticalalignment="bottom",

@@ -669,7 +670,7 @@ horizontalalignment="right",

raise TypeError(
"Each entry in fitting_schemes should be an " "instance of LinearRegression"
"Each entry in fitting_schemes should be an instance of LinearRegression"
)
elif not scheme.is_scalar():
raise TypeError(
"plot_CV only supports the fitting schemes " "with a scalar paramater."
"plot_CV only supports the fitting schemes with a scalar paramater."
)

@@ -684,7 +685,7 @@

raise TypeError(
"Each entry in fitting_schemes should be an " "instance of LinearRegression"
"Each entry in fitting_schemes should be an instance of LinearRegression"
)
elif not scheme.is_scalar():
raise TypeError(
"plot_CV only supports the fitting schemes " "with a scalar paramater."
"plot_CV only supports the fitting schemes with a scalar paramater."
)

@@ -728,3 +729,3 @@

def cv_for_alpha(self, alphas: List[float]) -> None:
def cv_for_alpha(self, alphas: list[float]) -> None:
"""

@@ -840,5 +841,3 @@ Calculate the CV scores for alphas using the fitting scheme

0.01,
f"min. CV score:\n"
f"alpha = {min_alpha:.10f} \n"
f"CV = {min_cv * 1000.0:.3f} meV/atom",
f"min. CV score:\nalpha = {min_alpha:.10f} \nCV = {min_cv * 1000.0:.3f} meV/atom",
verticalalignment="bottom",

@@ -1124,4 +1123,5 @@ horizontalalignment="left",

raise ValueError(
"Inconsistent length of max_dia, size: {}, expected at most: {}".format(
size, max_size_expected
(
f"Inconsistent length of max_dia, size: {size}, ",
f"expected at most: {max_size_expected}",
)

@@ -1143,3 +1143,3 @@ )

def generalization_error(self, validation_id: List[int]):
def generalization_error(self, validation_id: list[int]):
"""

@@ -1223,3 +1223,3 @@ Estimate the generalization error to new datapoints

def get_eci_by_size(self) -> Dict[str, Dict[str, list]]:
def get_eci_by_size(self) -> dict[str, dict[str, list]]:
"""

@@ -1226,0 +1226,0 @@ Classify distance, eci and cf_name according to cluster body size

"""Module for tools pertaining to geometry of atoms and cells."""
from ase import Atoms

@@ -3,0 +4,0 @@ import numpy as np

@@ -1,3 +0,1 @@

from typing import Dict, List
import numpy as np

@@ -49,3 +47,3 @@

def spin_dict(self, symbols: List[str]) -> Dict[str, int]:
def spin_dict(self, symbols: list[str]) -> dict[str, int]:
"""

@@ -57,3 +55,3 @@ Return a dictionary with the spin variable for each of the

def basis_functions(self, symbols: List[str]) -> List[Dict[str, float]]:
def basis_functions(self, symbols: list[str]) -> list[dict[str, float]]:
"""

@@ -142,3 +140,3 @@ Construct the spin dictionary from a list of symbols

def norm(self, bf: List[float]) -> float:
def norm(self, bf: list[float]) -> float:
"""

@@ -154,3 +152,3 @@ Computes the norm of a basis function

def eval(self, bf: List[float], valueIndex: int) -> float:
def eval(self, bf: list[float], valueIndex: int) -> float:
"""

@@ -157,0 +155,0 @@ Evaluates the basis function

@@ -0,3 +1,3 @@

from collections.abc import Callable, Sequence
from tkinter import TclError
from typing import Callable, List, Sequence, Union

@@ -64,3 +64,3 @@ from ase.db import connect

def __init__(self, fig, annotated_axes: Union[AnnotatedAx, Sequence[AnnotatedAx]]):
def __init__(self, fig, annotated_axes: AnnotatedAx | Sequence[AnnotatedAx]):
self.fig = fig

@@ -89,3 +89,3 @@ self.annotated_axes = annotated_axes

def get_mpl_events(self) -> List[MPLEvent]:
def get_mpl_events(self) -> list[MPLEvent]:
event = MPLEvent("motion_notify_event", self.hover)

@@ -196,3 +196,3 @@ return [event]

class ShowStructureOnClick(InteractivePlot):
def __init__(self, fig, axes: Union[AnnotatedAx, Sequence[AnnotatedAx]], db_name: str):
def __init__(self, fig, axes: AnnotatedAx | Sequence[AnnotatedAx], db_name: str):
self.db_name = db_name

@@ -205,3 +205,3 @@ self.active_images = Images()

def get_mpl_events(self) -> List[MPLEvent]:
def get_mpl_events(self) -> list[MPLEvent]:
event = MPLEvent("button_press_event", self.on_click)

@@ -208,0 +208,0 @@ events = super().get_mpl_events()

import json
from typing import Any, Dict
from typing import Any

@@ -166,3 +166,3 @@ from ase.io import jsonio as aseio

def todict(self) -> Dict[str, Any]:
def todict(self) -> dict[str, Any]:
"""Convert into a dictionary representation."""

@@ -173,4 +173,4 @@ # Disable recursive, since the json module takes care of that

@classmethod
def from_dict(cls, dct: Dict[str, Any]):
def from_dict(cls, dct: dict[str, Any]):
"""Load an instance of the class from a dictionary."""
return cls(**dct)

@@ -0,5 +1,6 @@

from collections.abc import Iterator
from contextlib import contextmanager
import logging
import sys
from typing import Iterator, Optional, TextIO
from typing import TextIO

@@ -15,3 +16,3 @@ __all__ = ("log_stream", "log_stream_context", "get_root_clease_logger")

def log_stream(
level: Optional[int] = None, stream: TextIO = sys.stdout, fmt: Optional[str] = None
level: int | None = None, stream: TextIO = sys.stdout, fmt: str | None = None
) -> logging.StreamHandler:

@@ -54,3 +55,3 @@ """Helper function to enable CLEASE logging to a stream. Default stream is stdout.

def log_stream_context(
level: Optional[int] = None, stream: TextIO = sys.stdout, fmt: Optional[str] = None
level: int | None = None, stream: TextIO = sys.stdout, fmt: str | None = None
) -> Iterator[logging.StreamHandler]:

@@ -98,3 +99,3 @@ """Context which temporarily adds a stream handler to the root CLEASE logger.

def _make_stream_handler(
level: Optional[int], stream: TextIO, fmt: Optional[str] = None
level: int | None, stream: TextIO, fmt: str | None = None
) -> logging.StreamHandler:

@@ -118,3 +119,3 @@ """Helper function to create a stream handler with a specified level and stream.

def _get_effective_level(level: Optional[int]) -> int:
def _get_effective_level(level: int | None) -> int:
"""Helper function to get the effective level - if the level is None,

@@ -121,0 +122,0 @@ we use the current level of the CLEASEroot logger, otherwise use the provided level

@@ -0,1 +1,4 @@

from typing_extensions import Self
class Averager:

@@ -19,3 +22,3 @@ """

def __iadd__(self, value):
def __iadd__(self, value) -> Self:
"""

@@ -58,3 +61,3 @@ += Operator

def __idiv__(self, number):
def __idiv__(self, number) -> Self:
"""Divide by number.

@@ -61,0 +64,0 @@

from abc import ABC, abstractmethod
from typing import Dict, Union

@@ -17,3 +16,3 @@ from ase import Atoms

def __call__(self, system: Union[Atoms, MCEvaluator], system_changes: SystemChanges) -> float:
def __call__(self, system: Atoms | MCEvaluator, system_changes: SystemChanges) -> float:
"""Get the height of the barrier"""

@@ -61,3 +60,3 @@ # Ensure we have a valid evaluator object

self,
dilute_barrier: Dict[str, float],
dilute_barrier: dict[str, float],
alpha: float = 0.5,

@@ -64,0 +63,0 @@ ):

from abc import ABC
from typing import Union

@@ -17,3 +16,3 @@ from ase import Atoms

Args:
system (Union[Atoms, MCEvaluator]): Either an ASE Atoms object
system (Atoms | MCEvaluator): Either an ASE Atoms object
with an attached calculator, or a pre-initialized

@@ -25,3 +24,3 @@ :class:`~clease.montecarlo.mc_evaluator.MCEvaluator`

def __init__(self, system: Union[Atoms, MCEvaluator], temp: float):
def __init__(self, system: Atoms | MCEvaluator, temp: float):
self.evaluator = system

@@ -44,3 +43,3 @@ self.temperature = temp

@evaluator.setter
def evaluator(self, value: Union[Atoms, MCEvaluator]) -> None:
def evaluator(self, value: Atoms | MCEvaluator) -> None:
"""Set the evaluator object. If the value is an Atoms object,

@@ -47,0 +46,0 @@ the evaluator is created on basis of the attached calculator object.

# XXX: Some funny imports here. This file needs to be cleaned up some
from typing import Sequence
from collections.abc import Sequence

@@ -58,3 +58,3 @@ import numpy as np

dill.dump(self, outfile)
print(f"Pseudo binary free energy bias potential written to " f"{fname}")
print(f"Pseudo binary free energy bias potential written to {fname}")

@@ -179,3 +179,3 @@ @staticmethod

self._conc_init = pseudo_bin_conc_init
super(PseudoBinaryFreeEnergyBias, self).__init__(reac_crd, free_eng)
super().__init__(reac_crd, free_eng)

@@ -187,5 +187,3 @@ @property

if not isinstance(self._conc_init, PseudoBinaryConcInitializer):
raise TypeError(
"pseudo_bin_conc_init has to be of type " "PseudoBinaryConcInitializer!"
)
raise TypeError("pseudo_bin_conc_init has to be of type PseudoBinaryConcInitializer!")
return self._conc_init

@@ -198,5 +196,3 @@

if not isinstance(init, PseudoBinaryConcInitializer):
raise TypeError(
"pseudo_bin_conc_init has to be of type " "PseudoBinaryConcInitializer!"
)
raise TypeError("pseudo_bin_conc_init has to be of type PseudoBinaryConcInitializer!")
self._conc_init = init

@@ -255,3 +251,3 @@

def __init__(self, cov_range=None, reac_crd=(), free_eng=()):
super(CovarianceBiasPotential, self).__init__(reac_crd, free_eng)
super().__init__(reac_crd, free_eng)
self._cov_range = cov_range

@@ -258,0 +254,0 @@

@@ -1,2 +0,2 @@

from typing import Sequence
from collections.abc import Sequence

@@ -3,0 +3,0 @@ import numpy as np

@@ -1,2 +0,2 @@

from typing import Sequence
from collections.abc import Sequence

@@ -23,3 +23,3 @@ from ase import Atoms

index_by_basis: List[List[int]]
index_by_basis: list[list[int]]
Indices ordered by basis (same as ``index_by_basis`` parameter in

@@ -26,0 +26,0 @@ the :class:`~clease.settings.settings.ClusterExpansionSettings`

@@ -1,2 +0,2 @@

from typing import Sequence
from collections.abc import Sequence

@@ -3,0 +3,0 @@ from clease.datastructures import SystemChanges

@@ -1,2 +0,2 @@

from typing import Sequence
from collections.abc import Sequence

@@ -3,0 +3,0 @@ import numpy as np

@@ -0,5 +1,5 @@

from collections.abc import Sequence
import logging
import random
import time
from typing import List, Sequence, Tuple, Union
import warnings

@@ -52,3 +52,3 @@

self,
system: Union[Atoms, MCEvaluator],
system: Atoms | MCEvaluator,
temp: float,

@@ -80,3 +80,3 @@ barrier: BarrierModel,

def _update_epr(self, current: int, choice: int, swaps: List[int], cum_rates: np.ndarray):
def _update_epr(self, current: int, choice: int, swaps: list[int], cum_rates: np.ndarray):
"""

@@ -104,3 +104,3 @@ Update the EPR tracker (if set).

def _rates(self, vac_idx: int) -> Tuple[List[int], np.ndarray]:
def _rates(self, vac_idx: int) -> tuple[list[int], np.ndarray]:
"""

@@ -141,3 +141,3 @@ Return the rates for all possible swaps for a vacancy

def _mc_step(self, vac_idx: int, step_no: int) -> Tuple[int, MCStep]:
def _mc_step(self, vac_idx: int, step_no: int) -> tuple[int, MCStep]:
"""

@@ -206,3 +206,3 @@ Perform an MC step and return the new index of the moving vacancy

raise ValueError(
f"Index {vac_idx} is not a vacancy. " f"Symbol: {self.atoms[vac_idx].symbol}"
f"Index {vac_idx} is not a vacancy. Symbol: {self.atoms[vac_idx].symbol}"
)

@@ -209,0 +209,0 @@

from abc import ABCMeta
from typing import List

@@ -15,3 +14,3 @@ from ase import Atoms

def get_swaps(self, atoms: Atoms, vac_idx: int) -> List[int]:
def get_swaps(self, atoms: Atoms, vac_idx: int) -> list[int]:
raise NotImplementedError(f"get_swaps has not been implemented for class {self.name}")

@@ -35,3 +34,3 @@

def get_swaps(self, atoms: Atoms, vac_idx: int) -> List[int]:
def get_swaps(self, atoms: Atoms, vac_idx: int) -> list[int]:
return self.nl[vac_idx]
import logging
from typing import Optional, Union

@@ -31,3 +30,3 @@ from ase import Atoms

def get_energy(self, applied_changes: Optional[SystemChanges] = None) -> float:
def get_energy(self, applied_changes: SystemChanges | None = None) -> float:
"""Evaluate the energy of a system.

@@ -81,3 +80,3 @@ If a change is sufficiently local/small, it there, in some situations,

def keep_system_changes(self, system_changes: Optional[SystemChanges] = None) -> None:
def keep_system_changes(self, system_changes: SystemChanges | None = None) -> None:
"""A set of system changes are to be kept. Perform necessary actions to prepare

@@ -124,3 +123,3 @@ for a new evaluation."""

def get_energy(self, applied_changes: Optional[SystemChanges] = None) -> float:
def get_energy(self, applied_changes: SystemChanges | None = None) -> float:
return self.calc.get_energy()

@@ -156,3 +155,3 @@

def keep_system_changes(self, system_changes: Optional[SystemChanges] = None) -> None:
def keep_system_changes(self, system_changes: SystemChanges | None = None) -> None:
"""A set of system changes are to be kept. Perform necessary actions to prepare

@@ -197,3 +196,3 @@ for a new evaluation."""

def construct_evaluator(system: Union[Atoms, MCEvaluator]) -> MCEvaluator:
def construct_evaluator(system: Atoms | MCEvaluator) -> MCEvaluator:
"""Helper function for constructing a new evaluator object, either by passing

@@ -203,3 +202,3 @@ in an ASE atoms object or an explicit evaluator object.

Args:
system (Union[Atoms, MCEvaluator]): If the system is an Atoms object,
system (Atoms | MCEvaluator): If the system is an Atoms object,
then a new :class:`~clease.montecarlo.mc_evaluator.MCEvaluator`

@@ -206,0 +205,0 @@ object is created on the basis of the attached calculator.

@@ -145,3 +145,3 @@ from copy import deepcopy

raise PeakNotAcceptedError(
("Observer does not accept peak as a keyword argument to __call__")
"Observer does not accept peak as a keyword argument to __call__"
)

@@ -175,3 +175,3 @@

if time.perf_counter() - now > self.log_freq:
msg = f"Sweep no. {int(counter/len(self.mc.atoms))} "
msg = f"Sweep no. {int(counter / len(self.mc.atoms))} "
msg += f"Average visits: {self.progress_info['mean']:.2e}. "

@@ -178,0 +178,0 @@ msg += f"Min/avg: {self.progress_info['minval']:.2e} "

"""Monte Carlo method for ase."""
from collections import Counter
from collections.abc import Iterator
from datetime import datetime

@@ -8,3 +10,3 @@ import logging

import time
from typing import Any, Dict, Iterator, Optional, Union
from typing import Any

@@ -32,3 +34,3 @@ from ase import Atoms

Args:
system (Union[ase.Atoms, MCEvaluator]): Either an ASE Atoms object
system (ase.Atoms | MCEvaluator): Either an ASE Atoms object
with an attached calculator, or a pre-initialized

@@ -47,5 +49,5 @@ :class:`~clease.montecarlo.mc_evaluator.MCEvaluator`

self,
system: Union[Atoms, MCEvaluator],
system: Atoms | MCEvaluator,
temp: float,
generator: Optional[TrialMoveGenerator] = None,
generator: TrialMoveGenerator | None = None,
):

@@ -266,3 +268,3 @@ # We cannot cause an energy calculation trigger in init,

@property
def meta_info(self) -> Dict[str, str]:
def meta_info(self) -> dict[str, str]:
"""Return dict with meta info."""

@@ -284,3 +286,3 @@ # Get the timestamp with millisecond precision

def get_thermodynamic_quantities(self) -> Dict[str, Any]:
def get_thermodynamic_quantities(self) -> dict[str, Any]:
"""Compute thermodynamic quantities."""

@@ -310,3 +312,3 @@ quantities = {}

def _get_obs_averages(self) -> Dict[str, Any]:
def _get_obs_averages(self) -> dict[str, Any]:
"""Get average measurements from observers"""

@@ -373,3 +375,3 @@ obs_avgs = {}

def count_atoms(self) -> Dict[str, int]:
def count_atoms(self) -> dict[str, int]:
"""Count the number of each element."""

@@ -376,0 +378,0 @@ return dict(Counter(self.atoms.symbols))

@@ -1,3 +0,1 @@

from typing import Dict
from clease.datastructures import SystemChanges

@@ -48,3 +46,3 @@ from clease.montecarlo.observers.mc_observer import MCObserver

def get_averages(self) -> Dict[str, float]:
def get_averages(self) -> dict[str, float]:
"""

@@ -51,0 +49,0 @@ Return dictionary with the rate such that it is added to thermodynaic quantities

from pathlib import Path
from typing import Optional, Union

@@ -19,3 +18,3 @@ import numpy as np

def __init__(self, size: int = 1000, fname: Optional[Union[str, Path]] = None):
def __init__(self, size: int = 1000, fname: str | Path | None = None):
self._buffer = np.zeros(size)

@@ -22,0 +21,0 @@ self._next = 0

@@ -1,3 +0,1 @@

from typing import Dict
import ase

@@ -80,3 +78,3 @@

def get_averages(self) -> Dict[str, float]:
def get_averages(self) -> dict[str, float]:
mean_conc = self.avg_conc.mean

@@ -83,0 +81,0 @@ var_conc = self.avg_conc_sq.mean - mean_conc**2

from pathlib import Path
from typing import List, Union

@@ -35,3 +34,3 @@ import numpy as np

def __init__(self, buffer_length: int = 10000, logfile: Union[str, Path] = "epr.txt"):
def __init__(self, buffer_length: int = 10000, logfile: str | Path = "epr.txt"):
self._buffer = BufferedArray(size=buffer_length, fname=logfile)

@@ -59,3 +58,3 @@ self.prev_swap = -1

def update(self, current: int, choice: int, cumulative_rates: np.ndarray, swaps: List[int]):
def update(self, current: int, choice: int, cumulative_rates: np.ndarray, swaps: list[int]):
"""

@@ -62,0 +61,0 @@ Update the buffer

from __future__ import annotations
from typing import Iterator, List
from collections.abc import Iterator

@@ -45,3 +45,3 @@ import ase

def reconstruct(self) -> List[ase.Atoms]:
def reconstruct(self) -> list[ase.Atoms]:
"""Rebuild the atoms objects as defined by the observed changes."""

@@ -48,0 +48,0 @@ return list(self.reconstruct_iter())

@@ -1,3 +0,1 @@

from typing import Dict, List, Tuple
from ase.units import kB

@@ -22,3 +20,3 @@ import numpy as np

def __init__(self, temp: float, chem_pot: Dict[str, float]):
def __init__(self, temp: float, chem_pot: dict[str, float]):
self.temp = temp

@@ -40,3 +38,3 @@ self.chem_pot = chem_pot

prefix += "_".join(
f"{k}{sign_indicator(v)}{int(1000.0*abs(v))}" for k, v in self.chem_pot.items()
f"{k}{sign_indicator(v)}{int(1000.0 * abs(v))}" for k, v in self.chem_pot.items()
)

@@ -96,3 +94,3 @@ return prefix

def __init__(self, ref_state: SGCState, thermo_states: List[SGCState], calc: Clease):
def __init__(self, ref_state: SGCState, thermo_states: list[SGCState], calc: Clease):
super().__init__()

@@ -110,3 +108,3 @@ self.thermo_states = thermo_states

def __call__(self, system_changes: List[Tuple[int, str, str]]):
def __call__(self, system_changes: list[tuple[int, str, str]]):
"""

@@ -130,3 +128,3 @@ Observers are called after system update, thus the calcualtor already

def get_averages(self) -> Dict[str, float]:
def get_averages(self) -> dict[str, float]:
"""

@@ -133,0 +131,0 @@ Return a dictionary with the calculated averages

@@ -1,2 +0,2 @@

from typing import Sequence
from collections.abc import Sequence

@@ -3,0 +3,0 @@ from ase.atoms import Atoms

@@ -1,2 +0,3 @@

from typing import Any, Dict, Optional, Sequence
from collections.abc import Sequence
from typing import Any

@@ -37,3 +38,3 @@ from ase import Atoms

symbols: Sequence[str] = (),
generator: Optional[TrialMoveGenerator] = None,
generator: TrialMoveGenerator | None = None,
observe_singlets: bool = False,

@@ -105,3 +106,3 @@ ):

@chemical_potential.setter
def chemical_potential(self, chem_pot: Dict[str, float]):
def chemical_potential(self, chem_pot: dict[str, float]):
eci = self.calc.eci

@@ -122,3 +123,3 @@ if any(key not in eci for key in chem_pot):

def _include_chemical_potential_in_eci(self, chem_pot: Dict[str, float], eci: Dict[str, float]):
def _include_chemical_potential_in_eci(self, chem_pot: dict[str, float], eci: dict[str, float]):
"""

@@ -150,3 +151,3 @@ Including the chemical potentials in the ECIs

def _reset_eci_to_original(self, eci_with_chem_pot: Dict[str, float]):
def _reset_eci_to_original(self, eci_with_chem_pot: dict[str, float]):
"""

@@ -174,3 +175,3 @@ Resets the ECIs to their original value

call_observers: bool = True,
chem_pot: Optional[Dict[str, float]] = None,
chem_pot: dict[str, float] | None = None,
):

@@ -200,3 +201,3 @@ """

def singlet2composition(self, avg_singlets: Dict[str, float]):
def singlet2composition(self, avg_singlets: dict[str, float]):
"""Convert singlets to composition."""

@@ -229,3 +230,3 @@ bf = self.settings.basis_functions

def get_thermodynamic_quantities(self, reset_eci: bool = False) -> Dict[str, Any]:
def get_thermodynamic_quantities(self, reset_eci: bool = False) -> dict[str, Any]:
"""Compute thermodynamic quantities.

@@ -232,0 +233,0 @@

@@ -0,3 +1,3 @@

from collections.abc import Sequence
import random
from typing import Dict, List, Optional, Sequence

@@ -17,4 +17,4 @@ import ase

def __init__(self, atoms, indices: Optional[Sequence[int]] = None):
self.tracker: Dict[str, List[int]] = {}
def __init__(self, atoms, indices: Sequence[int] | None = None):
self.tracker: dict[str, list[int]] = {}
self.index_loc = None

@@ -29,6 +29,6 @@ self._last_move = []

@staticmethod
def _unique_symbols_from_atoms(atoms) -> List[str]:
def _unique_symbols_from_atoms(atoms) -> list[str]:
return sorted(set(atoms.symbols))
def get_unique_symbols(self) -> List[str]:
def get_unique_symbols(self) -> list[str]:
return list(self.tracker.keys())

@@ -46,3 +46,3 @@

def _init_tracker(self, atoms: ase.Atoms, indices: Optional[Sequence[int]] = None) -> None:
def _init_tracker(self, atoms: ase.Atoms, indices: Sequence[int] | None = None) -> None:
"""Initialize the tracker with the numbers."""

@@ -124,5 +124,5 @@ # Cache the unique symbols for faster access

def get_two_random_symbols(self) -> List[str]:
def get_two_random_symbols(self) -> list[str]:
"""Get two different random unique symbols"""
# random.sample samples without replacement.
return random.sample(self.unique_symbols, 2)
from abc import ABC, abstractmethod
from collections.abc import Sequence
import random
from random import choice
from typing import Dict, List, Optional, Sequence, Set, Tuple

@@ -133,3 +133,3 @@ from ase import Atoms

def made_changes(self, changes: Sequence[SystemChange]) -> List[SystemChange]:
def made_changes(self, changes: Sequence[SystemChange]) -> list[SystemChange]:
"""

@@ -155,5 +155,3 @@ Extract the subset system changes made by an instance of itself.

def __init__(
self, symbols: Set[str], atoms: Atoms, indices: Optional[List[int]] = None, **kwargs
):
def __init__(self, symbols: set[str], atoms: Atoms, indices: list[int] | None = None, **kwargs):
super().__init__(**kwargs)

@@ -176,3 +174,3 @@ self.symbols = symbols

def _make_possible_flips(self) -> Dict[str, List[str]]:
def _make_possible_flips(self) -> dict[str, list[str]]:
"""Compute a map of possible flips, given a site has a particular symbol."""

@@ -184,3 +182,3 @@ possible = {}

def get_single_trial_move(self) -> List[SystemChange]:
def get_single_trial_move(self) -> list[SystemChange]:
"""Get a random flip of an included site into a different element."""

@@ -208,3 +206,3 @@ pos = choice(self.indices)

def __init__(self, atoms: Atoms, indices: Optional[List[int]] = None, **kwargs):
def __init__(self, atoms: Atoms, indices: list[int] | None = None, **kwargs):
super().__init__(**kwargs)

@@ -217,7 +215,6 @@ self.indices = indices

raise TooFewElementsError(
"After filtering there are less than two symbol type left. "
"Must have at least two."
"After filtering there are less than two symbol type left. Must have at least two."
)
def get_single_trial_move(self) -> List[SystemChange]:
def get_single_trial_move(self) -> list[SystemChange]:
"""

@@ -278,7 +275,7 @@ Create a swap move

@property
def generators(self) -> Tuple[SingleTrialMoveGenerator]:
def generators(self) -> tuple[SingleTrialMoveGenerator]:
return (self.flipper, self.swapper)
@property
def weights(self) -> Tuple[float]:
def weights(self) -> tuple[float]:
"""The probability weights for each generator"""

@@ -351,3 +348,3 @@ return (self.flip_prob, 1.0 - self.flip_prob)

atoms: Atoms,
indices: Optional[Sequence[Sequence[int]]] = None,
indices: Sequence[Sequence[int]] | None = None,
**kwargs,

@@ -354,0 +351,0 @@ ):

"""Logger that can be used together with multiprocessing funcions."""
import logging as lg

@@ -3,0 +4,0 @@ import multiprocessing as mp

@@ -1,3 +0,1 @@

from typing import List, Optional, Tuple
from matplotlib.figure import Figure

@@ -12,3 +10,3 @@ import matplotlib.pyplot as plt

def plot_fit(
evaluate: Evaluate, plot_args: Optional[dict] = None, interactive: bool = False
evaluate: Evaluate, plot_args: dict | None = None, interactive: bool = False
) -> Figure:

@@ -67,3 +65,3 @@ """

0.01,
cv_name + f" = {cv:.3f} meV/atom\n" f"RMSE = {rmse:.3f} meV/atom",
cv_name + f" = {cv:.3f} meV/atom\nRMSE = {rmse:.3f} meV/atom",
verticalalignment="bottom",

@@ -96,3 +94,3 @@ horizontalalignment="right",

def plot_fit_residual(
evaluate: Evaluate, plot_args: Optional[dict] = None, interactive: bool = False
evaluate: Evaluate, plot_args: dict | None = None, interactive: bool = False
) -> Figure:

@@ -159,3 +157,3 @@ """

evaluate: Evaluate,
plot_args: Optional[dict] = None,
plot_args: dict | None = None,
ignore_sizes=(),

@@ -245,3 +243,3 @@ interactive: bool = False,

def plot_cv(evaluate: Evaluate, plot_args: Optional[dict] = None) -> Figure:
def plot_cv(evaluate: Evaluate, plot_args: dict | None = None) -> Figure:
"""

@@ -365,3 +363,3 @@ Figure object of CV values according to alpha values

def _make_annotations_hull(evaluate: Evaluate) -> Tuple[List[str], List[str]]:
def _make_annotations_hull(evaluate: Evaluate) -> tuple[list[str], list[str]]:
"""Helper function to make annotations for interactive plots."""

@@ -389,3 +387,3 @@ e_pred = evaluate.get_energy_predict()

def _make_annotations_plot_fit(evaluate: Evaluate) -> Tuple[List[str], List[str]]:
def _make_annotations_plot_fit(evaluate: Evaluate) -> tuple[list[str], list[str]]:
"""Helper function to make annotations for interactive plots."""

@@ -392,0 +390,0 @@ e_pred = evaluate.get_energy_predict()

@@ -361,3 +361,3 @@ from itertools import product

bayes.fname = fname
with open(fname, "r") as infile:
with open(fname) as infile:
data = json.load(infile)

@@ -442,8 +442,8 @@

msg = f"Iter: {iteration} "
msg += f"RMSE: {1000.0*self.rmse():.3E} "
msg += f"LOOCV (approx.): {1000.0*self.estimate_loocv():.3E} "
msg += f"RMSE: {1000.0 * self.rmse():.3E} "
msg += f"LOOCV (approx.): {1000.0 * self.estimate_loocv():.3E} "
msg += f"Num ECI: {self.num_ecis} "
msg += f"Lamb: {self.lamb:.3E} "
msg += f"Shape lamb: {self.shape_lamb:.3E} "
msg += f"Noise: {np.sqrt(1.0/self.inv_variance):.3E}"
msg += f"Noise: {np.sqrt(1.0 / self.inv_variance):.3E}"
logger.info(msg)

@@ -450,0 +450,0 @@ now = time.perf_counter()

@@ -1,3 +0,1 @@

from typing import Tuple
import numpy as np

@@ -41,3 +39,3 @@

def kkt_system(self, X: np.ndarray, y: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
def kkt_system(self, X: np.ndarray, y: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""

@@ -44,0 +42,0 @@ Return the Karush-Kuhn-Tucker (KKT) system of equations. By solving

@@ -128,3 +128,3 @@ import logging

individuals = []
with open(self.fname, "r") as infile:
with open(self.fname) as infile:
for line in infile:

@@ -131,0 +131,0 @@ individual = np.zeros(self.num_genes, dtype=np.uint8)

import logging
from typing import Optional

@@ -45,3 +44,3 @@ import numpy as np

def __init__(self, alpha: Optional[np.ndarray] = None) -> None:
def __init__(self, alpha: np.ndarray | None = None) -> None:
super().__init__()

@@ -199,6 +198,4 @@ self.alpha = alpha

logger.warning(
(
"Warning! The effective number of parameters is negative. "
"Try to change the initial guess for alpha."
)
"Warning! The effective number of parameters is negative. "
"Try to change the initial guess for alpha."
)

@@ -205,0 +202,0 @@ logger.info("Best GCV: %.3f", np.sqrt(res.fun))

@@ -0,4 +1,4 @@

from collections.abc import Callable, Sequence
import logging
import time
from typing import Callable, Dict, List, Optional, Sequence, Tuple, Union

@@ -78,6 +78,6 @@ import numpy as np

lamb_dia: float = 1e-6,
size_decay: Union[str, Callable[[int], float]] = "linear",
dia_decay: Union[str, Callable[[int], float]] = "linear",
size_decay: str | Callable[[int], float] = "linear",
dia_decay: str | Callable[[int], float] = "linear",
normalize: bool = True,
cf_names: Optional[List[str]] = None,
cf_names: list[str] | None = None,
) -> None:

@@ -109,3 +109,3 @@ super().__init__()

def fit_data(self, X: np.ndarray, y: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
def fit_data(self, X: np.ndarray, y: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""

@@ -130,3 +130,3 @@ If normalize is True, a normalized version of the passed data is

@size_decay.setter
def size_decay(self, decay: Union[str, Callable[[int], float]]) -> None:
def size_decay(self, decay: str | Callable[[int], float]) -> None:
self._size_decay = get_size_decay(decay)

@@ -139,6 +139,6 @@

@dia_decay.setter
def dia_decay(self, decay: Union[str, Callable[[int], float]]) -> None:
def dia_decay(self, decay: str | Callable[[int], float]) -> None:
self._dia_decay = get_dia_decay(decay)
def sizes_from_names(self, names: List[str]) -> None:
def sizes_from_names(self, names: list[str]) -> None:
"""

@@ -154,3 +154,3 @@ Extract the sizes from a list of correlation function names

def diameters_from_names(self, names: List[str]) -> None:
def diameters_from_names(self, names: list[str]) -> None:
"""

@@ -238,3 +238,3 @@ Extract the diameters from a list of correltion function names

def get_size_decay(decay: Union[str, Callable[[int], float]]) -> Callable[[int], float]:
def get_size_decay(decay: str | Callable[[int], float]) -> Callable[[int], float]:
if isinstance(decay, str):

@@ -255,3 +255,3 @@ if decay == "linear":

def get_dia_decay(decay: Union[str, Callable[[int], float]]) -> Callable[[int], float]:
def get_dia_decay(decay: str | Callable[[int], float]) -> Callable[[int], float]:
if isinstance(decay, str):

@@ -274,3 +274,3 @@ if decay == "linear":

phys_ridge: PhysicalRidge,
params: Dict,
params: dict,
X: np.ndarray,

@@ -281,3 +281,3 @@ y: np.ndarray,

groups: Sequence[int] = (),
) -> Dict:
) -> dict:
"""

@@ -355,4 +355,4 @@ Estimate the hyper parameters of the Physical Ridge by random search.

msg = (
f"{i} of {num_trials}. CV: {best_cv*1000.0} meV/atom. "
f"MSE: {best_mse*1000.0} meV/atom. Params: {best_param}"
f"{i} of {num_trials}. CV: {best_cv * 1000.0} meV/atom. "
f"MSE: {best_mse * 1000.0} meV/atom. Params: {best_param}"
)

@@ -359,0 +359,0 @@ logger.info(msg)

"""Collection of classess to perform regression."""
from typing import List, Optional, Union

@@ -57,3 +56,3 @@ import numpy as np

def get_instance_array(self) -> List[object]:
def get_instance_array(self) -> list[object]:
return [LinearRegression()]

@@ -94,3 +93,3 @@

self,
alpha: Union[float, np.ndarray] = 1e-5,
alpha: float | np.ndarray = 1e-5,
penalize_bias_term: bool = False,

@@ -171,4 +170,4 @@ normalize: bool = True,

num_alpha: int = 10,
scale: Optional[str] = "log",
) -> List[object]:
scale: str | None = "log",
) -> list[object]:
if scale == "log":

@@ -225,3 +224,3 @@ alpha = np.logspace(

alpha_min: float, alpha_max: float, num_alpha: int = 10, scale: str = "log"
) -> List[object]:
) -> list[object]:
if scale == "log":

@@ -228,0 +227,0 @@ alpha = np.logspace(

@@ -64,5 +64,3 @@ import logging

for i in range(20):
print(
f"Num. coeff: {len(coeffs[srt_idx[i]]):9d} " f"CV: {cvs[srt_idx[i]]:9.3f} meV/atom"
)
print(f"Num. coeff: {len(coeffs[srt_idx[i]]):9d} CV: {cvs[srt_idx[i]]:9.3f} meV/atom")
print("--------------------------------------------")

@@ -69,0 +67,0 @@

from __future__ import annotations
from typing import List, Optional, Sequence
from collections.abc import Sequence

@@ -19,3 +19,3 @@ from ase import Atoms

def __init__(self, atoms: Optional[Atoms] = None) -> None:
def __init__(self, atoms: Atoms | None = None) -> None:
self.atoms = atoms

@@ -28,9 +28,6 @@

@atoms.setter
def atoms(self, other: Optional[Atoms]) -> None:
def atoms(self, other: Atoms | None) -> None:
if other is not None and not isinstance(other, Atoms):
raise TypeError(
(
"Trying to set atoms with a non-atoms object. "
f"Expected atoms, got {type(other)}"
)
f"Trying to set atoms with a non-atoms object. Expected atoms, got {type(other)}"
)

@@ -45,3 +42,3 @@ self._atoms = other

def index_by_tag(self) -> List[List[int]]:
def index_by_tag(self) -> list[list[int]]:
"""Return atomic indices that are grouped by their tags.

@@ -60,3 +57,3 @@

def index_by_symbol(self, symbols: List) -> List[List[int]]:
def index_by_symbol(self, symbols: list) -> list[list[int]]:
"""Group atomic indices by its atomic symbols.

@@ -91,3 +88,3 @@

def unique_elements(self, ignore: Sequence[str] = ()) -> List[str]:
def unique_elements(self, ignore: Sequence[str] = ()) -> list[str]:
"""Return a list of symbols of unique elements.

@@ -100,3 +97,3 @@

def single_element_sites(self, allowed_elements: List[List[str]]) -> List[int]:
def single_element_sites(self, allowed_elements: list[list[str]]) -> list[int]:
"""

@@ -103,0 +100,0 @@ Return a list of sites that can only be occupied by a single element

@@ -443,3 +443,3 @@ """Class containing a manager for setting up concentrations of species."""

if ref_element is None:
raise RuntimeError(f"Did not find reference element for symbol" f" {variable}")
raise RuntimeError(f"Did not find reference element for symbol {variable}")

@@ -535,3 +535,3 @@ # Extract the coefficients of other elements with their

raise ValueError(f"Did not find any basis containing " f"{variable_symbol}")
raise ValueError(f"Did not find any basis containing {variable_symbol}")

@@ -538,0 +538,0 @@ def _get_col_of_element_in_basis(self, basis, element):

@@ -6,2 +6,3 @@ """Definitions of Cluster Expansion settings for bulk.

"""
from copy import deepcopy

@@ -38,3 +39,3 @@

concentration (Union[Concentration, dict]):
concentration (Concentration | dict):
Concentration object or dictionary specifying the basis elements and

@@ -86,3 +87,5 @@ concentration range of constituting species

settings = ClusterExpansionSettings(prim, concentration, **kwargs)
settings = ClusterExpansionSettings(
prim, concentration, _allow_direct_instantiation=True, **kwargs
)

@@ -116,3 +119,3 @@ settings.kwargs.update(

concentration (Union[Concentration, dict]):
concentration (Concentration | dict):
Concentration object or dictionary specifying the basis elements and

@@ -125,3 +128,3 @@ concentration range of constituting species

basis (List[float]):
basis (list[float]):
List of scaled coordinates.

@@ -170,3 +173,5 @@ Positions of the unique sites corresponding to symbols given

settings = ClusterExpansionSettings(prim, concentration, **kwargs)
settings = ClusterExpansionSettings(
prim, concentration, _allow_direct_instantiation=True, **kwargs
)
settings.kwargs.update(

@@ -173,0 +178,0 @@ {

@@ -1,2 +0,2 @@

from typing import Optional, Sequence, Tuple, Union
from collections.abc import Sequence

@@ -17,6 +17,6 @@ from ase import Atoms

def CESlab(
conventional_cell: Union[Atoms, str],
miller: Tuple[int],
conventional_cell: Atoms | str,
miller: tuple[int],
concentration: Concentration,
size: Optional[Sequence[int]] = (1, 1, 1),
size: Sequence[int] | None = (1, 1, 1),
**kwargs,

@@ -52,3 +52,5 @@ ) -> ClusterExpansionSettings:

# Slab should always have one cell vector along the z-axis
settings = ClusterExpansionSettings(prim, concentration, size=size, **kwargs)
settings = ClusterExpansionSettings(
prim, concentration, size=size, _allow_direct_instantiation=True, **kwargs
)

@@ -71,3 +73,3 @@ dict_rep = conventional_cell.todict()

def get_prim_slab_cell(conventional_cell: Union[Atoms, str], miller: Tuple[int]) -> Atoms:
def get_prim_slab_cell(conventional_cell: Atoms | str, miller: tuple[int]) -> Atoms:
"""

@@ -74,0 +76,0 @@ Returns the primitive cell used for slab CE

@@ -6,7 +6,9 @@ """Definition of ClusterExpansionSettings Class.

"""
from __future__ import annotations
from collections.abc import Sequence
from copy import deepcopy
import logging
from typing import Any, Dict, List, Optional, Sequence, Set, Union
from typing import Any

@@ -43,10 +45,17 @@ from ase import Atoms

.. note::
This class should not be instantiated directly. Use one of the factory
functions instead:
- :func:`clease.settings.CEBulk` for bulk materials with crystal structures
- :func:`clease.settings.CECrystal` for bulk materials with space groups
- :func:`clease.settings.CESlab` for slab/surface calculations
Args:
prim (Atoms): The primitive atoms object.
concentration (Union[Concentration, dict]): Concentration object or
concentration (Concentration | dict): Concentration object or
dictionary specifying the basis elements and
concentration range of constituting species.
size (List[int] | None, optional): Size of the supercell
size (list[int] | None, optional): Size of the supercell
(e.g., [2, 2, 2] for 2x2x2 cell).

@@ -97,5 +106,5 @@ ``supercell_factor`` is ignored if both ``size`` and ``supercell_factor``

prim: Atoms,
concentration: Union[Concentration, dict],
size: Optional[List[int]] = None,
supercell_factor: Optional[int] = 27,
concentration: Concentration | dict,
size: list[int] | None = None,
supercell_factor: int | None = 27,
db_name: str = "clease.db",

@@ -105,3 +114,13 @@ max_cluster_dia: Sequence[float] = (5.0, 5.0, 5.0),

basis_func_type="polynomial",
_allow_direct_instantiation: bool = False,
) -> None:
if not _allow_direct_instantiation:
raise TypeError(
"ClusterExpansionSettings cannot be instantiated directly. "
"Please use one of the factory functions instead:\n"
" - CEBulk() for bulk materials with crystal structures\n"
" - CECrystal() for bulk materials with space groups\n"
" - CESlab() for slab/surface calculations\n"
"See clease.settings documentation for details."
)
self._include_background_atoms = include_background_atoms

@@ -212,3 +231,3 @@ self._cluster_mng = None

@property
def all_elements(self) -> List[str]:
def all_elements(self) -> list[str]:
return sorted([item for row in self.basis_elements for item in row])

@@ -221,3 +240,3 @@

@property
def unique_elements(self) -> List[str]:
def unique_elements(self) -> list[str]:
return sorted(list(set(deepcopy(self.all_elements))))

@@ -230,3 +249,3 @@

@property
def ref_index_trans_symm(self) -> List[int]:
def ref_index_trans_symm(self) -> list[int]:
return [i[0] for i in self.index_by_sublattice]

@@ -247,3 +266,3 @@

@property
def background_indices(self) -> List[int]:
def background_indices(self) -> list[int]:
"""Get indices of the background atoms."""

@@ -259,3 +278,3 @@ # check if any basis consists of only one element type

@property
def non_background_indices(self) -> List[int]:
def non_background_indices(self) -> list[int]:
"""Indices of sites which are not background"""

@@ -289,3 +308,3 @@ bkg = set(self.background_indices)

@property
def spin_dict(self) -> Dict[str, float]:
def spin_dict(self) -> dict[str, float]:
return self.basis_func_type.spin_dict

@@ -302,3 +321,3 @@

@property
def multiplicity_factor(self) -> Dict[str, float]:
def multiplicity_factor(self) -> dict[str, float]:
"""Return the multiplicity factor of each cluster."""

@@ -309,3 +328,3 @@ num_sites_in_group = [len(x) for x in self.index_by_sublattice]

@property
def all_cf_names(self) -> List[str]:
def all_cf_names(self) -> list[str]:
num_bf = len(self.basis_functions)

@@ -320,3 +339,3 @@ return self.cluster_list.get_all_cf_names(num_bf)

@property
def index_by_basis(self) -> List[List[int]]:
def index_by_basis(self) -> list[list[int]]:
first_symb_in_basis = [x[0] for x in self.basis_elements]

@@ -326,3 +345,3 @@ return self.atoms_mng.index_by_symbol(first_symb_in_basis)

@property
def index_by_sublattice(self) -> List[List[int]]:
def index_by_sublattice(self) -> list[list[int]]:
return self.atoms_mng.index_by_tag()

@@ -339,3 +358,3 @@

@property
def size(self) -> Union[np.ndarray, None]:
def size(self) -> np.ndarray | None:
return self.template_atoms.size

@@ -349,3 +368,3 @@

@property
def supercell_factor(self) -> Union[int, None]:
def supercell_factor(self) -> int | None:
return self.template_atoms.supercell_factor

@@ -370,3 +389,3 @@

def get_active_sublattices(self) -> List[bool]:
def get_active_sublattices(self) -> list[bool]:
"""List of booleans indicating if a (grouped) sublattice is active"""

@@ -378,3 +397,3 @@ unique_no_bkg = self.unique_element_without_background()

@property
def ignored_species_and_conc(self) -> Dict[str, float]:
def ignored_species_and_conc(self) -> dict[str, float]:
"""

@@ -396,6 +415,4 @@ Return the ignored species and their concentrations normalised to the total number

raise ValueError(
(
"Ignored sublattice contains multiple elements -"
"this does not make any sense"
)
"Ignored sublattice contains multiple elements -"
"this does not make any sense"
)

@@ -453,3 +470,3 @@ if elem not in ignored:

def get_bg_syms(self) -> Set[str]:
def get_bg_syms(self) -> set[str]:
"""

@@ -612,3 +629,3 @@ Return the symbols in the basis where there is only one element

def get_all_figures_as_atoms(self) -> List[Atoms]:
def get_all_figures_as_atoms(self) -> list[Atoms]:
"""Get the list of all possible figures, in their

@@ -661,3 +678,3 @@ ASE Atoms representation."""

def todict(self) -> Dict:
def todict(self) -> dict:
"""Return a dictionary representation of the settings class.

@@ -680,3 +697,3 @@

@classmethod
def from_dict(cls, dct: Dict[str, Any]) -> ClusterExpansionSettings:
def from_dict(cls, dct: dict[str, Any]) -> ClusterExpansionSettings:
"""Load a new ClusterExpansionSettings class from a dictionary representation.

@@ -711,2 +728,4 @@

# Allow instantiation from from_dict (legitimate use case)
kwargs["_allow_direct_instantiation"] = True
settings = cls(*args, **kwargs)

@@ -786,3 +805,3 @@

def _get_concentration(concentration: Union[Concentration, dict]) -> Concentration:
def _get_concentration(concentration: Concentration | dict) -> Concentration:
"""Helper function to format the concentration"""

@@ -789,0 +808,0 @@ if isinstance(concentration, Concentration):

"""Class containing a manager for creating template atoms."""
from collections.abc import Iterator
from contextlib import contextmanager
from itertools import product
from typing import Iterator, List, Optional, Union

@@ -58,3 +59,3 @@ import ase

@property
def size(self) -> Union[None, np.ndarray]:
def size(self) -> None | np.ndarray:
return self._size

@@ -73,7 +74,7 @@

@property
def supercell_factor(self) -> Union[int, None]:
def supercell_factor(self) -> int | None:
return self._supercell_factor
@supercell_factor.setter
def supercell_factor(self, value: Optional[int]) -> None:
def supercell_factor(self, value: int | None) -> None:
if value is not None:

@@ -244,7 +245,7 @@ # Unset the size, since we provided a value for supercell_factor

def get_all_templates(self) -> List[ase.Atoms]:
def get_all_templates(self) -> list[ase.Atoms]:
"""Return a list with all templates."""
return list(self.iterate_all_templates())
def iterate_all_templates(self, max_per_size: Optional[int] = None) -> Iterator[ase.Atoms]:
def iterate_all_templates(self, max_per_size: int | None = None) -> Iterator[ase.Atoms]:
"""Get all possible templates in an iterator.

@@ -417,3 +418,3 @@

def _to_3x3_matrix(size: Union[List[int], List[List[int]], np.ndarray]) -> np.ndarray:
def _to_3x3_matrix(size: list[int] | list[list[int]] | np.ndarray) -> np.ndarray:
"""Convert a list of ints (1D) or list of list of ints (2D) into a

@@ -420,0 +421,0 @@ 3x3 transformation matrix, if possible."""

@@ -160,3 +160,3 @@ from abc import ABC, abstractmethod

third_vec = set([0, 1, 2]) - set(span)
third_vec = list(third_vec)[0]
third_vec = next(iter(third_vec))
d = normal.dot(cell[third_vec, :])

@@ -163,0 +163,0 @@ return abs(d)

@@ -1,3 +0,1 @@

from typing import List, Tuple
from matplotlib import pyplot as plt

@@ -28,3 +26,3 @@ import numpy as np

self, fitter: LinearRegression, X: np.ndarray, y: np.ndarray
) -> Tuple[List[int], np.ndarray]:
) -> tuple[list[int], np.ndarray]:
"""

@@ -31,0 +29,0 @@ Remove the feature corresponding to the smallest coefficient

"""Module for generating new structures for training."""
from copy import deepcopy

@@ -7,3 +8,3 @@ from functools import reduce

import os
from typing import Any, Dict, List, Optional, Tuple, Union
from typing import Any

@@ -65,3 +66,3 @@ from ase import Atoms

settings: ClusterExpansionSettings,
generation_number: Optional[int] = None,
generation_number: int | None = None,
struct_per_gen: int = 5,

@@ -99,5 +100,5 @@ check_db: bool = True,

self,
atoms: Optional[Atoms] = None,
init_temp: Optional[float] = None,
final_temp: Optional[float] = None,
atoms: Atoms | None = None,
init_temp: float | None = None,
final_temp: float | None = None,
num_temp: int = 5,

@@ -204,3 +205,3 @@ num_steps_per_temp: int = 1000,

self,
eci: Dict[str, float],
eci: dict[str, float],
num_templates: int = 20,

@@ -245,3 +246,3 @@ num_prim_cells: int = 2,

self,
eci: Dict[str, float],
eci: dict[str, float],
num_templates: int = 20,

@@ -315,4 +316,4 @@ num_prim_cells: int = 2,

self,
atoms: Union[Atoms, List[Atoms]],
eci: Dict[str, float],
atoms: Atoms | list[Atoms],
eci: dict[str, float],
init_temp: float = 2000.0,

@@ -411,3 +412,3 @@ final_temp: float = 1.0,

def generate_random_structures(self, atoms: Optional[Atoms] = None) -> None:
def generate_random_structures(self, atoms: Atoms | None = None) -> None:
"""

@@ -452,3 +453,3 @@ Generate random structures until the number of structures with

def generate_one_random_structure(self, atoms: Optional[Atoms] = None) -> bool:
def generate_one_random_structure(self, atoms: Atoms | None = None) -> bool:
"""

@@ -493,4 +494,4 @@ Generate and insert a random structure to database if a unique

def _set_initial_structures(
self, atoms: Union[Atoms, List[Atoms]], random_composition: bool = False
) -> List[Atoms]:
self, atoms: Atoms | list[Atoms], random_composition: bool = False
) -> list[Atoms]:
structs = []

@@ -549,3 +550,3 @@ if isinstance(atoms, Atoms):

def generate_initial_pool(self, atoms: Optional[Atoms] = None) -> None:
def generate_initial_pool(self, atoms: Atoms | None = None) -> None:
"""

@@ -576,6 +577,4 @@ Generate initial pool of structures.

logger.info(
(
"Generating one structure per concentration where the number "
"of an element is at max/min"
)
"Generating one structure per concentration where the number "
"of an element is at max/min"
)

@@ -605,3 +604,3 @@ indx_in_each_basis = []

def generate_metropolis_trajectory(
self, atoms: Optional[Atoms] = None, random_comp: bool = True
self, atoms: Atoms | None = None, random_comp: bool = True
) -> None:

@@ -664,3 +663,3 @@ """

raise NotValidTemplateException(
("Did not find an atoms with a satisfactory concentration.")
"Did not find an atoms with a satisfactory concentration."
)

@@ -671,3 +670,3 @@

def insert_structures(
self, traj_init: str, traj_final: Optional[str] = None, cb=lambda num, tot: None
self, traj_init: str, traj_final: str | None = None, cb=lambda num, tot: None
) -> None:

@@ -719,9 +718,9 @@ """

self,
init_struct: Union[Atoms, str],
final_struct: Optional[Union[Atoms, str]] = None,
name: Optional[str] = None,
cf: Optional[Dict[str, float]] = None,
meta: Optional[Dict[str, Any]] = None,
init_struct: Atoms | str,
final_struct: Atoms | str | None = None,
name: str | None = None,
cf: dict[str, float] | None = None,
meta: dict[str, Any] | None = None,
warn_on_skip: bool = True,
) -> Optional[Union[int, Tuple[int, int]]]:
) -> int | tuple[int, int] | None:
"""Insert a structure to the database.

@@ -807,3 +806,3 @@

def _exists_in_db(self, atoms: Atoms, formula_unit: Optional[str] = None) -> bool:
def _exists_in_db(self, atoms: Atoms, formula_unit: str | None = None) -> bool:
"""

@@ -846,3 +845,3 @@ Check to see if the passed atoms already exists in DB.

def _get_kvp(self, formula_unit: Optional[str] = None) -> Dict:
def _get_kvp(self, formula_unit: str | None = None) -> dict:
"""

@@ -849,0 +848,0 @@ Create a dictionary of key-value pairs and return it.

"""Module for generating new structures."""
from abc import ABC

@@ -119,3 +120,3 @@ from copy import deepcopy

msg = (
f"T Step: {Ti+1:>{len_max_temp}} of {N}, "
f"T Step: {Ti + 1:>{len_max_temp}} of {N}, "
# Print same len as max required

@@ -126,3 +127,3 @@ f"Temp: {temp:>10.3f}, "

f"{self.num_steps_per_temp}, "
f"Accept. rate: {acc_rate*100:.2f} %"
f"Accept. rate: {acc_rate * 100:.2f} %"
)

@@ -372,3 +373,3 @@ logger.info(msg)

else:
raise IOError(f"'{fname}' not found.")
raise OSError(f"'{fname}' not found.")
else:

@@ -556,4 +557,4 @@ self.o_mv = mean_variance_approx(self.o_cfm)

# if inverting matrix leads to a singular matrix, reduce the matrix
except np.linalg.linalg.LinAlgError:
except np.linalg.LinAlgError:
prec = pinv(cfm.T.dot(cfm))
return prec

@@ -1,3 +0,1 @@

from typing import Tuple
from ase.atoms import Atoms, Cell

@@ -78,3 +76,3 @@ from ase.geometry import find_mic

def snap_to_lattice(self, atoms: Atoms, template: Atoms) -> Tuple[Atoms, TransformInfo]:
def snap_to_lattice(self, atoms: Atoms, template: Atoms) -> tuple[Atoms, TransformInfo]:
"""

@@ -81,0 +79,0 @@ Snaps atoms to an ideal lattice. If the number of atoms in the

"""A collection of miscellaneous functions used for Cluster Expansion."""
from collections.abc import Iterable
from collections.abc import Iterable, Sequence
from collections.abc import Iterable as tIterable
from itertools import chain, combinations, filterfalse, permutations, product

@@ -7,4 +9,3 @@ import logging

import re
from typing import Dict, List, Optional, Sequence, Set, Tuple, Union
from typing import Iterable as tIterable
from typing import Protocol

@@ -20,3 +21,2 @@ import ase

from scipy.spatial import cKDTree as KDTree
from typing_extensions import Protocol

@@ -56,3 +56,3 @@ ASE_VERSION = parse(ase.__version__)

def index_by_position(atoms: ase.Atoms) -> List[int]:
def index_by_position(atoms: ase.Atoms) -> list[int]:
"""Set atomic indices by its position."""

@@ -179,4 +179,4 @@ # add zero to avoid negative zeros

db_name=None,
custom_kvp_init: Optional[dict] = None,
custom_kvp_final: Optional[dict] = None,
custom_kvp_init: dict | None = None,
custom_kvp_final: dict | None = None,
):

@@ -289,3 +289,3 @@ """Update the database.

X: np.ndarray, y: np.ndarray, nsplits: int = 10, groups: Sequence[int] = ()
) -> List[Dict[str, np.ndarray]]:
) -> list[dict[str, np.ndarray]]:
"""Split the dataset such that it can be used for k-fold

@@ -350,3 +350,3 @@ cross validation.

def random_validation_set(
num: int = 10, select_cond: Optional[list] = None, db_name: Optional[str] = None
num: int = 10, select_cond: list | None = None, db_name: str | None = None
):

@@ -372,3 +372,3 @@ """

def exclude_ids(ids: List[int]) -> List[tuple]:
def exclude_ids(ids: list[int]) -> list[tuple]:
"""

@@ -455,3 +455,3 @@ Construct a select condition based on the ids passed.

# Opposite facet
remaining = list(set([0, 1, 2]) - set(plane))[0]
remaining = next(iter(set([0, 1, 2]) - set(plane)))
vec = cell[remaining, :]

@@ -873,3 +873,3 @@ dist = np.abs(n.dot(x - vec))

def get_extension(fname: Union[str, Path]) -> str:
def get_extension(fname: str | Path) -> str:
"""

@@ -886,3 +886,3 @@ Return the file extension of a filename

def add_file_extension(fname: Union[str, Path], ext: str) -> str:
def add_file_extension(fname: str | Path, ext: str) -> str:
"""

@@ -934,3 +934,3 @@ Adds the wanted file extension to a filename. If a file extension

def sort_cf_names(cf_names: tIterable[str]) -> List[str]:
def sort_cf_names(cf_names: tIterable[str]) -> list[str]:
"""

@@ -952,3 +952,3 @@ Return a sorted list of correlation function names. The names are

def get_ids(select_cond: List[tuple], db_name: str) -> List[int]:
def get_ids(select_cond: list[tuple], db_name: str) -> list[int]:
"""

@@ -978,3 +978,3 @@ Return ids in the datase that correspond to the passed selection.

class SQLCursor(Protocol):
def execute(self, sql: str, placeholder: Tuple[str]) -> None:
def execute(self, sql: str, placeholder: tuple[str]) -> None:
pass

@@ -986,3 +986,3 @@

def get_attribute(ids: List[int], cur: SQLCursor, key: str, table: str) -> list:
def get_attribute(ids: list[int], cur: SQLCursor, key: str, table: str) -> list:
"""

@@ -1016,3 +1016,3 @@ Retrieve the value of the given key for the rows with the given ID of

def common_cf_names(ids: Set[int], cur: SQLCursor, table: str) -> Set[str]:
def common_cf_names(ids: set[int], cur: SQLCursor, table: str) -> set[str]:
"""

@@ -1048,4 +1048,4 @@ Extracts all correlation function names that are present for all

d: float,
A_eq: Optional[np.ndarray] = None,
b_eq: Optional[np.ndarray] = None,
A_eq: np.ndarray | None = None,
b_eq: np.ndarray | None = None,
) -> bool:

@@ -1086,5 +1086,5 @@ """

b_lb: np.ndarray,
A_eq: Optional[np.ndarray] = None,
b_eq: Optional[np.ndarray] = None,
) -> Tuple[np.ndarray, np.ndarray]:
A_eq: np.ndarray | None = None,
b_eq: np.ndarray | None = None,
) -> tuple[np.ndarray, np.ndarray]:
"""

@@ -1234,3 +1234,3 @@ Remove all redundant constraints from A_lb and b_lb.

if n_target != len(superatoms):
msg = "Number of atoms in supercell: {}, expected: {}".format(n_target, len(superatoms))
msg = f"Number of atoms in supercell: {n_target}, expected: {len(superatoms)}"
raise ase_sc.SupercellError(msg)

@@ -1299,3 +1299,3 @@

center=(0.5, 0.5, 0.5),
cell_T_inv: Optional[np.ndarray] = None,
cell_T_inv: np.ndarray | None = None,
eps: float = 1e-7,

@@ -1302,0 +1302,0 @@ ) -> np.ndarray:

@@ -31,3 +31,3 @@ # distutils: language = c++

object get_singlets()
vector[double] get_singlets()

@@ -34,0 +34,0 @@ const vector[string]& get_symbols() const

# distutils: language = c++
# cython: c_string_type=str, c_string_encoding=ascii
import numpy as np
cimport numpy as np # Initialize the Numpy API
from libcpp cimport bool
np.import_array()
include "pyce_updater.pyx"

@@ -14,5 +11,1 @@ include "py_cluster.pyx"

cpdef bool has_parallel()
# Files that use the Numpy Array API are included here
cdef extern from "with_numpy/ce_updater.cpp":
pass
#include <cmath>
template<class key,class value>
std::ostream& operator <<( std::ostream &out, const std::map<key,value> &map )
{
for ( auto iter=map.begin(); iter != map.end(); ++iter )
{
out << iter->first << ":" << iter->second << "\n";
}
return out;
template <class key, class value>
std::ostream &operator<<(std::ostream &out, const std::map<key, value> &map) {
for (auto iter = map.begin(); iter != map.end(); ++iter) {
out << iter->first << ":" << iter->second << "\n";
}
return out;
};
template<class T>
std::ostream& operator << (std::ostream &out, const std::vector<T> &vec )
{
out << "[";
for ( unsigned int i=0;i<vec.size(); i++ )
{
out << vec[i] << " ";
}
out << "]";
return out;
template <class T>
std::ostream &operator<<(std::ostream &out, const std::vector<T> &vec) {
out << "[";
for (unsigned int i = 0; i < vec.size(); i++) {
out << vec[i] << " ";
}
out << "]";
return out;
};
template<class T>
std::vector<T>& cyclic_permute( std::vector<T> &vec )
{
for ( unsigned int i=0;i<vec.size()-1;i++ )
{
T prev = vec[0];
vec[0] = vec[i+1];
vec[i+1] = prev;
}
return vec;
template <class T>
std::vector<T> &cyclic_permute(std::vector<T> &vec) {
for (unsigned int i = 0; i < vec.size() - 1; i++) {
T prev = vec[0];
vec[0] = vec[i + 1];
vec[i + 1] = prev;
}
return vec;
};
template<class T>
void keys( std::map<std::string,T> &dict, std::vector<std::string> &names )
{
names.clear();
for ( auto iter=dict.begin(); iter != dict.end(); ++iter )
{
names.push_back(iter->first);
}
template <class T>
void keys(std::map<std::string, T> &dict, std::vector<std::string> &names) {
names.clear();
for (auto iter = dict.begin(); iter != dict.end(); ++iter) {
names.push_back(iter->first);
}
};
template<class T>
void set2vector( const std::set<T> &s, std::vector<T> &vec )
{
vec.clear();
for ( auto iter = s.begin(); iter != s.end(); ++iter )
{
vec.push_back(*iter);
}
template <class T>
void set2vector(const std::set<T> &s, std::vector<T> &vec) {
vec.clear();
for (auto iter = s.begin(); iter != s.end(); ++iter) {
vec.push_back(*iter);
}
};
template<class T, unsigned int N>
std::ostream& operator <<(std::ostream& out, std::array<T, N> &array)
{
for (unsigned int i=0;i<N;i++ )
{
out << array[i];
}
out << "\n";
return out;
template <class T, unsigned int N>
std::ostream &operator<<(std::ostream &out, std::array<T, N> &array) {
for (unsigned int i = 0; i < N; i++) {
out << array[i];
}
out << "\n";
return out;
};
template<class T>
std::ostream& operator <<(std::ostream& out, std::set<T> &set){
for (auto iter=set.begin(); iter != set.end(); ++iter){
out << *iter << " ";
}
out << "\n";
return out;
template <class T>
std::ostream &operator<<(std::ostream &out, std::set<T> &set) {
for (auto iter = set.begin(); iter != set.end(); ++iter) {
out << *iter << " ";
}
out << "\n";
return out;
}
template<class T>
bool is_in_vector(const T& value, const std::vector<T> &vector){
for (const T& elem : vector){
if (value == elem) return true;
}
return false;
template <class T>
bool is_in_vector(const T &value, const std::vector<T> &vector) {
for (const T &elem : vector) {
if (value == elem) return true;
}
return false;
}
template<class T>
void insert_in_set(const std::vector<T> &vec, std::set<T> &unique){
for (const T& elem : vec){
unique.insert(elem);
}
template <class T>
void insert_in_set(const std::vector<T> &vec, std::set<T> &unique) {
for (const T &elem : vec) {
unique.insert(elem);
}
}

@@ -81,4 +81,3 @@ #ifndef CE_UPDATER_H

/** Returns the value of the singlets */
void get_singlets(PyObject *npy_array) const;
PyObject *get_singlets() const;
std::vector<double> get_singlets() const;

@@ -85,0 +84,0 @@ /** Updates the CF */

template<class T>
Matrix<T>::Matrix( unsigned int n_rows, unsigned int n_cols ):n_rows(n_rows), n_cols(n_cols)
{
data = new T[n_rows*n_cols];
template <class T>
Matrix<T>::Matrix(unsigned int n_rows, unsigned int n_cols) : n_rows(n_rows), n_cols(n_cols) {
data = new T[n_rows * n_cols];
};
template<class T>
Matrix<T>::~Matrix()
{
delete [] data;
template <class T>
Matrix<T>::~Matrix() {
delete[] data;
};
template <class T>
T& Matrix<T>::operator()( unsigned int i, unsigned int j )
{
return data[j*n_rows+i];
T &Matrix<T>::operator()(unsigned int i, unsigned int j) {
return data[j * n_rows + i];
};
template<class T>
const T& Matrix<T>::operator()( unsigned int i, unsigned int j ) const
{
return data[j*n_rows+i];
template <class T>
const T &Matrix<T>::operator()(unsigned int i, unsigned int j) const {
return data[j * n_rows + i];
};
template<class T>
void Matrix<T>::set_size( unsigned int nr, unsigned int nc )
{
n_rows = nr;
n_cols = nc;
data = new T[n_rows*n_cols];
template <class T>
void Matrix<T>::set_size(unsigned int nr, unsigned int nc) {
n_rows = nr;
n_cols = nc;
data = new T[n_rows * n_cols];
};
template<class T>
void swap( Matrix<T> &first, const Matrix<T> &second )
{
first.n_rows = second.n_rows;
first.n_cols = second.n_cols;
first.data = new T[second.n_rows*second.n_cols];
for ( unsigned int i=0;i<first.n_rows*first.n_cols;i++ )
{
first.data[i] = second.data[i];
}
template <class T>
void swap(Matrix<T> &first, const Matrix<T> &second) {
first.n_rows = second.n_rows;
first.n_cols = second.n_cols;
first.data = new T[second.n_rows * second.n_cols];
for (unsigned int i = 0; i < first.n_rows * first.n_cols; i++) {
first.data[i] = second.data[i];
}
}
template<class T>
Matrix<T>::Matrix( const Matrix<T> &other )
{
swap(*this,other);
template <class T>
Matrix<T>::Matrix(const Matrix<T> &other) {
swap(*this, other);
}
template<class T>
Matrix<T>& Matrix<T>::operator=(const Matrix<T> &other )
{
swap(*this,other);
return *this;
template <class T>
Matrix<T> &Matrix<T>::operator=(const Matrix<T> &other) {
swap(*this, other);
return *this;
}

@@ -10,2 +10,11 @@ .. CLEASE documentation master file, created by

.. note::
**This documentation has moved!**
The updated version of the CLEASE documentation is now available at:
https://www.clease.org
Please visit the new site for the latest documentation, tutorials, and resources.
Cluster expansion (CE) is a widely used method for studying thermondynamic

@@ -12,0 +21,0 @@ properties of disordered materials. CLEASE is a cluster expansion code which strives

@@ -7,2 +7,11 @@ .. _releasenotes:

1.2.0
=====
* **Python 3.10 is now the minimum supported version**
* Removed requirement `numpy < 2`
* No longer requires Numpy to build the C++ extension
* `ClusterExpansionSettings` can no longer be instantiated directly,
unless overridden by the `_allow_direct_instantiation` keyword.
1.1.0

@@ -9,0 +18,0 @@ ======

+30
-25

@@ -1,4 +0,4 @@

Metadata-Version: 2.1
Metadata-Version: 2.4
Name: clease
Version: 1.1.0
Version: 1.2.0
Summary: CLuster Expansion in Atomistic Simulation Environment

@@ -14,13 +14,10 @@ Home-page: https://gitlab.com/computationalmaterials/clease/

Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: Topic :: Scientific/Engineering :: Chemistry
Requires-Python: >=3.7
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE.md
Requires-Dist: ase>=3.22
Requires-Dist: numpy<2
Requires-Dist: numpy
Requires-Dist: cython

@@ -52,5 +49,4 @@ Requires-Dist: matplotlib

Requires-Dist: twine; extra == "dev"
Requires-Dist: black>=22.1.0; extra == "dev"
Requires-Dist: clang-format>=14.0.3; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: clang-format==21.1.5; extra == "dev"
Requires-Dist: ruff==0.14.5; extra == "dev"
Requires-Dist: pyclean>=2.0.0; extra == "dev"

@@ -62,21 +58,22 @@ Requires-Dist: pytest-cov; extra == "dev"

Provides-Extra: all
Requires-Dist: black>=22.1.0; extra == "all"
Requires-Dist: pyclean>=2.0.0; extra == "all"
Requires-Dist: pre-commit; extra == "all"
Requires-Dist: ipython; extra == "all"
Requires-Dist: sphinx; extra == "all"
Requires-Dist: pytest; extra == "all"
Requires-Dist: twine; extra == "all"
Requires-Dist: cython; extra == "all"
Requires-Dist: ruff==0.14.5; extra == "all"
Requires-Dist: pytest-benchmark[histogram]>=3.4.1; extra == "all"
Requires-Dist: mock; extra == "all"
Requires-Dist: twine; extra == "all"
Requires-Dist: ruff; extra == "all"
Requires-Dist: clang-format==21.1.5; extra == "all"
Requires-Dist: sphinx_rtd_theme; extra == "all"
Requires-Dist: pre-commit; extra == "all"
Requires-Dist: pyclean>=2.0.0; extra == "all"
Requires-Dist: pip; extra == "all"
Requires-Dist: pytest-cov; extra == "all"
Requires-Dist: clease-gui; extra == "all"
Requires-Dist: tox>=4; extra == "all"
Requires-Dist: pytest-mock; extra == "all"
Requires-Dist: build; extra == "all"
Requires-Dist: sphinx; extra == "all"
Requires-Dist: pytest-mock; extra == "all"
Requires-Dist: pytest; extra == "all"
Requires-Dist: cython; extra == "all"
Requires-Dist: ipython; extra == "all"
Requires-Dist: clang-format>=14.0.3; extra == "all"
Requires-Dist: tox>=4; extra == "all"
Requires-Dist: pytest-benchmark[histogram]>=3.4.1; extra == "all"
Requires-Dist: pytest-cov; extra == "all"
Dynamic: license-file
Dynamic: provides-extra

@@ -94,2 +91,10 @@ # CLEASE

# Documentation
The updated version of the CLEASE documentation is now available at:
https://www.clease.org
Please visit the new site for the latest documentation, tutorials, and resources.
# Installation

@@ -96,0 +101,0 @@

[build-system]
# setuptools 61.0 broke, see !488
requires = ["setuptools >=58, !=61.0", "wheel", "Cython", "numpy<2"]
requires = ["setuptools >=62", "wheel", "Cython"]
build-backend = "setuptools.build_meta"
[tool.black]
line-length = 100
target-version = ["py37"]
[tool.ruff]
select = ["E", "F", "NPY", "RUF", "I"]
exclude = ["tests", "__init__.py"]
ignore = ["NPY002", "RUF012"]
# Same as Black.
line-length = 100
target-version = "py37"
target-version = "py310"
extend-exclude = ["tests", "__init__.py", "clease/gui/*", "__pycache__"]
[tool.ruff.isort]
[tool.ruff.lint]
select = ["E", "F", "NPY", "RUF", "I", "UP"]
ignore = [
"E402", # Allow module level import not at top of file
"N802", # Allow class names to start with an underscore
"N803", # Allow function names to start with an underscore
"N806", # Allow function names to start with an underscore
"NPY002", # Checks for the use of legacy np.random function calls.
"NPY201", # Checks for deprecated function calls removed in NumPy 2.0.
"RUF012", # Mutable class attributes should be annotated with typing.ClassVar
"RUF015", # Prefer next({iterable}) over single element slice
"RUF022", # `__all__` is not sorted
]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401", "F403", "F405"]
[tool.ruff.lint.isort]
force-sort-within-sections = true
[tool.cibuildwheel]
# Documentation: https://cibuildwheel.pypa.io/en/stable/options/#options
# cibuildwheel automatically reads the supported Python versions from setup.cfg
environment = "CLEASE_OMP='-fopenmp'"
skip = ["cp3??t-*"] # Disable free-threading builds
build = "cp*" # Must be CPython
archs = ["auto"]

@@ -12,2 +12,10 @@ # CLEASE

# Documentation
The updated version of the CLEASE documentation is now available at:
https://www.clease.org
Please visit the new site for the latest documentation, tutorials, and resources.
# Installation

@@ -14,0 +22,0 @@

@@ -22,6 +22,3 @@ [metadata]

License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3
Topic :: Scientific/Engineering :: Physics

@@ -32,7 +29,7 @@ Topic :: Scientific/Engineering :: Chemistry

packages = find:
python_requires = >=3.7
python_requires = >=3.10
include_package_data = true
install_requires =
ase>=3.22
numpy<2
numpy
cython

@@ -46,3 +43,3 @@ matplotlib

attrs>=21.4.0
scipy>=1.7.0 # Last minor version which allows python 3.7
scipy>=1.7.0
packaging # Replaces deprecated distutils

@@ -49,0 +46,0 @@ threadpoolctl # For controlling parallelization of NumPy

@@ -32,8 +32,2 @@ import os

def get_npy_include_folder():
import numpy as np
return np.get_include()
def build_ext(ext_module):

@@ -56,4 +50,3 @@ from Cython.Build import cythonize

# C++ sources, excluding the ones which rely on NumPy
# those are included directly from the Cython source
# C++ sources
sources = [str(f) for f in cpp_sources]

@@ -95,3 +88,2 @@

cxx_inc_folder,
get_npy_include_folder(),
cxx_src_folder,

@@ -129,5 +121,4 @@ py_include,

"twine",
"black>=22.1.0", # Style formatting
"clang-format>=14.0.3", # c++ style formatting
"ruff",
"clang-format==21.1.5", # c++ style formatting
"ruff==0.14.5", # Python style and formatting
"pyclean>=2.0.0", # For removing __pycache__ and .pyc files

@@ -134,0 +125,0 @@ "pytest-cov",

#include "ce_updater.hpp"
#include <algorithm>
#include <cassert>
#include <iostream>
#include <iterator>
#include <sstream>
#include <stdexcept>
#include "additional_tools.hpp"
#include "atomic_numbers.hpp"
#include "basis_function.hpp"
#include "cluster_name.hpp"
#include "config.hpp"
using namespace std;
CEUpdater::CEUpdater(){};
CEUpdater::~CEUpdater(){};
void CEUpdater::init(PyObject *py_atoms, PyObject *settings, PyObject *corrFunc, PyObject *pyeci,
PyObject *cluster_list) {
atoms = py_atoms;
if (settings == nullptr) {
throw invalid_argument("Settings object is nullptr!");
}
#ifdef PRINT_DEBUG
cout << "Getting symbols from settings object\n";
#endif
PyObject *py_ignore_bck = get_attr(settings, "ignore_background_atoms");
ignore_background_indices = PyObject_IsTrue(py_ignore_bck);
Py_DECREF(py_ignore_bck);
unsigned int n_atoms = PyObject_Length(atoms);
if (n_atoms < 0) {
throw invalid_argument("Could not retrieve the length of the atoms object!");
}
// Read the atomic symbols
std::vector<std::string> symbols = get_symbols_from_atoms(py_atoms);
trans_symm_group.resize(n_atoms);
set<string> unique_symbols;
// Extract unique symbols from settings
PyObject *py_unique_symb = get_attr(settings, "unique_elements");
for (unsigned int i = 0; i < list_size(py_unique_symb); i++) {
unique_symbols.insert(py2string(PyList_GetItem(py_unique_symb, i)));
}
Py_DECREF(py_unique_symb);
insert_in_set(symbols, unique_symbols);
symbols_with_id = std::make_unique<Symbols>(symbols, unique_symbols);
// Build read the translational sites
PyObject *py_trans_symm_group = get_attr(settings, "index_by_sublattice");
if (py_trans_symm_group == nullptr) {
throw invalid_argument("index_by_sublattice is nullptr!");
}
#ifdef PRINT_DEBUG
cout << "Reading background indices\n";
#endif
// Read the backgound indices from settings
PyObject *bkg_indx = get_attr(settings, "background_indices");
read_background_indices(bkg_indx);
Py_DECREF(bkg_indx);
count_non_bkg_sites();
build_trans_symm_group(py_trans_symm_group);
Py_DECREF(py_trans_symm_group);
#ifdef PRINT_DEBUG
cout << "Getting cluster names from atoms object\n";
#endif
// Read cluster names
create_cname_with_dec(corrFunc);
#ifdef PRINT_DEBUG
cout << "Cluster names with decoration number created...\n";
#endif
PyObject *py_num_elements = get_attr(settings, "num_unique_elements");
if (py_num_elements == nullptr) {
throw invalid_argument("num_unique_elements is nullptr!");
}
int num_bfs = py2int(py_num_elements) - 1;
Py_DECREF(py_num_elements);
if (cluster_list == nullptr) {
throw invalid_argument("cluster_list is nullptr!");
}
// unsigned int num_trans_symm = list_size(cluster_info);
unsigned int num_clusters = PySequence_Size(cluster_list);
#ifdef PRINT_DEBUG
cout << "Parsing cluster list...\n";
#endif
PyObject *py_no_si = get_attr(cluster_list, "assume_no_self_interactions");
assume_no_self_interactions = PyObject_IsTrue(py_no_si);
Py_DECREF(py_no_si);
#ifdef PRINT_DEBUG
std::cout << "Assuming no self-interaction?: " << assume_no_self_interactions << std::endl;
#endif
for (unsigned int i = 0; i < num_clusters; i++) {
PyObject *py_cluster = PySequence_GetItem(cluster_list, i);
Cluster new_clst(py_cluster);
PyObject *py_cluster_name = get_attr(py_cluster, "name");
string cluster_name = py2string(py_cluster_name);
Py_DECREF(py_cluster_name);
Py_DECREF(py_cluster);
new_clst.construct_equivalent_deco(num_bfs);
clusters.append(new_clst);
int norm_fact = new_clst.get().size() * trans_symm_group_count[new_clst.symm_group];
if (normalisation_factor.find(cluster_name) == normalisation_factor.end()) {
normalisation_factor[cluster_name] = norm_fact;
} else {
normalisation_factor[cluster_name] += norm_fact;
}
}
#ifdef PRINT_DEBUG
cout << "Finished reading cluster_info\n";
#endif
#ifdef PRINT_DEBUG
cout << "Reading basis functions from settings object\n";
#endif
PyObject *bfs = get_attr(settings, "basis_functions");
if (bfs == NULL) {
status = Status_t::INIT_FAILED;
return;
}
// Reading basis functions from python object
PyObject *key;
PyObject *value;
unsigned int n_bfs = list_size(bfs);
bf_list basis_func_raw;
for (unsigned int i = 0; i < n_bfs; i++) {
Py_ssize_t pos = 0;
map<string, double> new_entry;
PyObject *bf_dict = PyList_GetItem(bfs, i);
while (PyDict_Next(bf_dict, &pos, &key, &value)) {
new_entry[py2string(key)] = PyFloat_AS_DOUBLE(value);
}
basis_func_raw.push_back(new_entry);
}
this->basis_functions = std::make_unique<BasisFunction>(basis_func_raw, *symbols_with_id);
#ifdef PRINT_DEBUG
cout << "Reading translation matrix from settings\n";
#endif
// Retrieve the TransMatrix object
PyObject *trans_mat_obj = get_attr(settings, "trans_matrix");
if (trans_mat_obj == NULL) {
status = Status_t::INIT_FAILED;
return;
}
// Get the internal trans_matrix object from within the TransMatrix object
PyObject *trans_mat_orig = get_attr(trans_mat_obj, "trans_matrix");
read_trans_matrix(trans_mat_orig);
Py_DECREF(trans_mat_obj);
Py_DECREF(trans_mat_orig);
// Read the ECIs, and parse the names.
this->set_eci(pyeci);
#ifdef PRINT_DEBUG
cout << "Parsing correlation function\n";
#endif
vector<string> flattened_cnames;
flattened_cf_names(flattened_cnames);
history = std::make_unique<CFHistoryTracker>(eci.get_names());
history->insert(corrFunc, nullptr);
// Store the singlets names
for (unsigned int i = 0; i < flattened_cnames.size(); i++) {
std::string name = flattened_cnames[i];
// Fetch the pre-parsed version of the name.
const ParsedName parsed = this->m_parsed_names[i];
if (parsed.size == 1) {
singlets.push_back(name);
}
}
status = Status_t::READY;
clear_history();
#ifdef PRINT_DEBUG
cout << "CEUpdater initialized sucessfully!\n";
#endif
// Verify that the ECIs given corresponds to a correlation function
if (!all_eci_corresponds_to_cf()) {
throw invalid_argument("All ECIs does not correspond to a correlation function!");
}
}
void CEUpdater::parse_eci_names() {
std::size_t num = eci.size();
this->m_parsed_names.clear();
this->m_parsed_names.reserve(num);
for (unsigned int i = 0; i < num; i++) {
std::string name = eci.name(i);
ClusterName c_name = ClusterName(name);
ParsedName parsed = c_name.get_parsed();
this->m_parsed_names.emplace_back(parsed);
}
}
double CEUpdater::get_energy() {
double energy = 0.0;
cf &corr_func = history->get_current();
energy = eci.dot(corr_func);
return energy * symbols_with_id->size();
}
double CEUpdater::spin_product_one_atom(int ref_indx, const Cluster &cluster,
const vector<int> &dec, int ref_id) const {
double sp = 0.0;
// Note: No duplication factor is included here, since this method is used for calculating the
// CF from scratch (which will account for self-interactions with no issue), and not updating
// (which must account for self-interactions via the duplication factor).
// i.e. spin_product_one_atom_delta must accounts for the self-interaction.
const vector<vector<int>> &indx_list = cluster.get();
unsigned int num_indx = indx_list.size();
unsigned int n_memb = indx_list[0].size();
// Cache the relevant row from the trans matrix.
int *trans_matrix_row = trans_matrix.get_row(ref_indx);
for (unsigned int i = 0; i < num_indx; i++) {
double sp_temp = 1.0;
// Use pointer arithmetics in the inner most loop
const int *indices = &indx_list[i][0];
for (unsigned int j = 0; j < n_memb; j++) {
int trans_index = trans_matrix.lookup_in_row(trans_matrix_row, indices[j]);
int id = (trans_index == ref_indx) ? ref_id : symbols_with_id->id(trans_index);
sp_temp *= basis_functions->get(dec[j], id);
}
sp += sp_temp;
}
return sp;
}
int CEUpdater::get_original_index(int ref_indx) const {
int *trans_matrix_row = trans_matrix.get_row(ref_indx);
int *allowed_lu = trans_matrix.get_allowed_lookup_values();
for (unsigned int j = 0; j < trans_matrix.get_num_non_zero(); j++) {
int col = allowed_lu[j];
int indx = trans_matrix.lookup_in_row(trans_matrix_row, col);
if (indx == ref_indx) {
return col;
}
}
std::stringstream err;
err << "Did not locate original index for ref index: " << ref_indx;
throw std::runtime_error(err.str());
}
double CEUpdater::spin_product_one_atom_delta_no_si(const SpinProductCache &sp_cache,
const Cluster &cluster,
const deco_t &deco) const {
/* Note: This function assumes no self-interaction within a cluster. */
// Figure out how many times we need to iterate
unsigned int num_indx = cluster.get_num_figures(); // Outer loop count
// Assume 1 ref site in a figure, so we iterate 1 less
unsigned int n_non_ref = cluster.get_size() - 1; // Inner loop count
int *tm_row = sp_cache.trans_matrix_row;
/* There are n_non_ref sites per each ref site, so the non_ref_site_ptr
iterates faster than the ref_site_ptr. Figures are placed contiguously
in a 1D array.*/
const ClusterSite *non_ref_site_ptr = &cluster.get_non_ref_sites()[0];
const int *ref_site_ptr = &cluster.get_ref_cluster_sites()[0];
// Keep track of the change in spin-product
double sp_delta = 0.0;
// Iterate each figure in the cluster. 1 reference site is assumed per cluster
for (unsigned int i = 0; i < num_indx; i++, ++ref_site_ptr) {
/* Calculate the spin product for both new and the old (ref)
The constant term to the spin product from the sites which didn't change.*/
const int dec_ref = deco[*ref_site_ptr];
double new_bf = basis_functions->get(dec_ref, sp_cache.new_symb_id);
double old_bf = basis_functions->get(dec_ref, sp_cache.old_symb_id);
double sp_change = new_bf - old_bf;
/* Iterate the remaining non-reference sites, as we already took care of the reference
site (assuming no self-interaction)*/
for (unsigned int j = 0; j < n_non_ref; j++, ++non_ref_site_ptr) {
const ClusterSite &site = *non_ref_site_ptr;
const int dec_j = deco[site.cluster_index];
const int trans_index = trans_matrix.lookup_in_row(tm_row, site.lattice_index);
sp_change *= basis_functions->get(dec_j, symbols_with_id->id(trans_index));
}
sp_delta += sp_change;
}
return sp_delta;
}
double CEUpdater::spin_product_one_atom_delta(const SpinProductCache &sp_cache,
const Cluster &cluster, const deco_t &deco) const {
// Keep track of the change in spin-product
double sp_delta = 0.0;
// List of figures in the cluster
const vector<vector<int>> &indx_list = cluster.get();
// Account for the self-interaction, in case we updated 2 sites with 1 change
const std::vector<double> &dup_factors = cluster.get_duplication_factors();
unsigned int num_indx = indx_list.size();
unsigned int n_memb = indx_list[0].size();
int *tm_row = sp_cache.trans_matrix_row;
// Iterate each site in the cluster
for (unsigned int i = 0; i < num_indx; i++) {
// Use pointer arithmetics in the inner most loop
const int *indices = &indx_list[i][0];
// Calculate the spin product for both new and the old (ref)
double sp_temp_new = 1.0, sp_temp_ref = 1.0;
// The constant term to the spin product from the sites which didn't change.
double sp_const = dup_factors[i];
for (unsigned int j = 0; j < n_memb; j++) {
const int site_index = indices[j];
const int dec_j = deco[j];
if (site_index == sp_cache.original_index) {
// This site is the reference index.
// Look up the BF values directly for the new and old symbols
sp_temp_new *= basis_functions->get(dec_j, sp_cache.new_symb_id);
sp_temp_ref *= basis_functions->get(dec_j, sp_cache.old_symb_id);
} else {
// Look up the symbol of the non-reference site, which hasn't changed.
const int trans_index = trans_matrix.lookup_in_row(tm_row, site_index);
sp_const *= basis_functions->get(dec_j, symbols_with_id->id(trans_index));
}
}
// The change in spin-product is the difference in SP between the site(s) which
// changed, multiplied by the constant SP from the other un-changed sites (since
// these contribute equally before and after the change).
sp_delta += (sp_temp_new - sp_temp_ref) * sp_const;
}
return sp_delta;
}
void CEUpdater::update_cf(PyObject *single_change) {
SymbolChange symb_change = SymbolChange(single_change);
update_cf(symb_change);
}
void CEUpdater::py_changes2_symb_changes(PyObject *all_changes,
vector<SymbolChange> &symb_changes) {
unsigned int size = list_size(all_changes);
for (unsigned int i = 0; i < size; i++) {
SymbolChange symb_change = SymbolChange(PyList_GetItem(all_changes, i));
symb_changes.push_back(symb_change);
}
}
SpinProductCache CEUpdater::build_sp_cache(const SymbolChange &symb_change) const {
int ref_indx = symb_change.indx;
// Look up the untranslated index of the reference index.
int orig_indx = this->get_original_index(ref_indx);
// Cache the relevant row from the trans matrix.
int *trans_matrix_row = this->trans_matrix.get_row(ref_indx);
unsigned int old_symb_id = symbols_with_id->get_symbol_id(symb_change.old_symb);
unsigned int new_symb_id = symbols_with_id->get_symbol_id(symb_change.new_symb);
SpinProductCache sp_cache = {ref_indx, orig_indx, trans_matrix_row, new_symb_id, old_symb_id};
return sp_cache;
}
cf &CEUpdater::get_next_cf(SymbolChange &symb_change) {
SymbolChange *symb_change_track;
cf *next_cf_ptr = nullptr;
history->get_next(&next_cf_ptr, &symb_change_track);
cf &next_cf = *next_cf_ptr;
symb_change_track->indx = symb_change.indx;
symb_change_track->old_symb = symb_change.old_symb;
symb_change_track->new_symb = symb_change.new_symb;
symb_change_track->track_indx = symb_change.track_indx;
return next_cf;
}
void CEUpdater::update_cf(SymbolChange &symb_change) {
if (symb_change.old_symb == symb_change.new_symb) {
return;
}
if (is_background_index[symb_change.indx]) {
throw runtime_error("Attempting to move a background atom!");
}
cf &current_cf = history->get_current();
cf &next_cf = get_next_cf(symb_change);
symbols_with_id->set_symbol(symb_change.indx, symb_change.new_symb);
/* The following prepares a range of properties which will be
useful for all of the clusters, so we don't compute more
than we have to inside the main ECI loop */
SpinProductCache sp_cache = this->build_sp_cache(symb_change);
if (atoms != nullptr) {
set_symbol_in_atoms(atoms, symb_change.indx, symb_change.new_symb);
}
int symm = trans_symm_group[symb_change.indx];
const std::vector<ClusterCache> &clusters_cache = m_cluster_by_symm[symm];
// Loop over all ECIs
// As work load for different clusters are different due to a different
// multiplicity factor, we need to apply a dynamic schedule
#ifdef HAS_OMP
bool is_par = this->cf_update_num_threads > 1;
#pragma omp parallel for if (is_par) num_threads(this->cf_update_num_threads) schedule(dynamic)
#endif
for (unsigned int i = 0; i < eci.size(); i++) {
// The pre-parsed version of the cluster name.
const ParsedName &parsed = this->m_parsed_names[i];
// 0-body
if (parsed.size == 0) {
next_cf[i] = current_cf[i];
continue;
}
// Singlet
if (parsed.size == 1) {
unsigned int dec = parsed.dec_num;
double new_bf = basis_functions->get(dec, sp_cache.new_symb_id);
double old_bf = basis_functions->get(dec, sp_cache.old_symb_id);
next_cf[i] = current_cf[i] + (new_bf - old_bf) / num_non_bkg_sites;
continue;
}
// n-body
const ClusterCache &cluster_cache = clusters_cache[i];
const Cluster *cluster_ptr = cluster_cache.cluster_ptr;
if (cluster_ptr == nullptr) {
// This cluster was not present in the symmetry group.
next_cf[i] = current_cf[i];
continue;
}
// The cluster is in the symmetry group, so calculate the spin product
// change for this cluster.
const Cluster &cluster = *cluster_ptr;
const equiv_deco_t &equiv_deco = *cluster_cache.equiv_deco_ptr;
double delta_sp = 0.0;
// Calculate the change (delta) in spin product
for (const deco_t &deco : equiv_deco) {
if (this->assume_no_self_interactions) {
// Faster version for large cells with no self interaction
delta_sp += spin_product_one_atom_delta_no_si(sp_cache, cluster, deco);
} else {
// Safe fall-back version
delta_sp += spin_product_one_atom_delta(sp_cache, cluster, deco);
}
}
delta_sp *= cluster_cache.normalization;
next_cf[i] = current_cf[i] + delta_sp;
}
}
void CEUpdater::undo_changes() {
unsigned int buf_size = history->history_size();
undo_changes(buf_size - 1);
}
void CEUpdater::undo_changes(int num_steps) {
int buf_size = history->history_size();
if (num_steps > buf_size - 1) {
throw invalid_argument("Can't reset history beyond the buffer size!");
}
SymbolChange *last_changes;
for (int i = 0; i < num_steps; i++) {
history->pop(&last_changes);
symbols_with_id->set_symbol(last_changes->indx, last_changes->old_symb);
if (atoms != nullptr) {
set_symbol_in_atoms(atoms, last_changes->indx, last_changes->old_symb);
}
}
}
double CEUpdater::calculate(PyObject *system_changes) {
unsigned int size = list_size(system_changes);
if (size == 0) {
return get_energy();
} else if (size == 1) {
for (unsigned int i = 0; i < size; i++) {
update_cf(PyList_GetItem(system_changes, i));
}
return get_energy();
}
if (size % 2 == 0) {
bool sequence_arbitrary_moves = false;
vector<swap_move> sequence;
for (unsigned int i = 0; i < size / 2; i++) {
swap_move changes;
changes[0] = SymbolChange(PyList_GetItem(system_changes, 2 * i));
changes[1] = SymbolChange(PyList_GetItem(system_changes, 2 * i + 1));
if (!is_swap_move(changes)) {
sequence_arbitrary_moves = true;
break;
}
sequence.push_back(changes);
}
if (!sequence_arbitrary_moves) {
return calculate(sequence);
}
}
// Last option is that this is a sequence of arbitrary moves
vector<SymbolChange> changes(size);
for (unsigned int i = 0; i < size; i++) {
changes[i] = SymbolChange(PyList_GetItem(system_changes, i));
}
return calculate(changes);
}
double CEUpdater::calculate(swap_move &system_changes) {
if (symbols_with_id->id(system_changes[0].indx) ==
symbols_with_id->id(system_changes[1].indx)) {
cout << system_changes[0] << endl;
cout << system_changes[1] << endl;
throw runtime_error(
"This version of the calculate function assumes that the provided update is swapping "
"two atoms\n");
}
if (symbols_with_id->get_symbol(system_changes[0].indx) != system_changes[0].old_symb) {
throw runtime_error("The atom position tracker does not match the current state\n");
} else if (symbols_with_id->get_symbol(system_changes[1].indx) != system_changes[1].old_symb) {
throw runtime_error("The atom position tracker does not match the current state\n");
}
// Update correlation function
update_cf(system_changes[0]);
update_cf(system_changes[1]);
return get_energy();
}
void CEUpdater::clear_history() {
history->clear();
}
void CEUpdater::flattened_cf_names(vector<string> &flattened) {
flattened = eci.get_names();
// Sort the cluster names for consistency
sort(flattened.begin(), flattened.end());
}
PyObject *CEUpdater::get_cf() {
PyObject *cf_dict = PyDict_New();
cf &corrfunc = history->get_current();
for (unsigned int i = 0; i < corrfunc.size(); i++) {
PyObject *pyvalue = PyFloat_FromDouble(corrfunc[i]);
PyDict_SetItemString(cf_dict, corrfunc.name(i).c_str(), pyvalue);
Py_DECREF(pyvalue);
}
return cf_dict;
}
void CEUpdater::set_symbols(const vector<string> &new_symbs) {
if (new_symbs.size() != symbols_with_id->size()) {
throw runtime_error(
"The number of atoms in the updater cannot be changed via the set_symbols function\n");
}
symbols_with_id->set_symbols(new_symbs);
}
void CEUpdater::cluster_by_symm_group() {
m_cluster_by_symm.clear();
// Find unique symmetry groups
std::set<int> unique;
insert_in_set(this->trans_symm_group, unique);
for (const int symm : unique) {
if (symm == -1) {
// Background symmetry group
continue;
}
// 1 ClusterCache per ECI value
std::vector<ClusterCache> cluster_cache;
cluster_cache.reserve(this->m_parsed_names.size());
for (const ParsedName &parsed : this->m_parsed_names) {
ClusterCache cache;
if (parsed.size == 0 || parsed.size == 1 ||
!clusters.is_in_symm_group(parsed.prefix, symm)) {
/* Either 0- or 1-body cluster, or cluster is not in this
symmetry group. */
cluster_cache.push_back(cache);
continue;
}
Cluster *cluster_ptr = clusters.get_ptr(parsed.prefix, symm);
equiv_deco_t *equiv_ptr = cluster_ptr->get_equiv_deco_ptr(parsed.dec_str);
// Calculate the normalization of the resulting cluster functions
double normalization = static_cast<double>(cluster_ptr->size);
normalization /= equiv_ptr->size();
normalization /= normalisation_factor.at(parsed.prefix);
// Populate the new cache object
cache.cluster_ptr = cluster_ptr;
cache.equiv_deco_ptr = equiv_ptr;
cache.normalization = normalization;
cluster_cache.push_back(cache);
}
m_cluster_by_symm.insert({symm, cluster_cache});
}
}
void CEUpdater::set_eci(PyObject *pyeci) {
PyObject *key;
PyObject *value;
// Read the ECIs
Py_ssize_t pos = 0;
std::map<std::string, double> temp_eci;
while (PyDict_Next(pyeci, &pos, &key, &value)) {
temp_eci[py2string(key)] = PyFloat_AS_DOUBLE(value);
}
this->eci.init(temp_eci);
// Pre-parse the names of the clusters.
this->parse_eci_names();
// If status is not READY, then we're still initializing, and CF's are missing.
if (this->status == Status_t::READY && !all_eci_corresponds_to_cf()) {
throw invalid_argument("All ECIs has to correspond to a correlation function!");
}
// Update the cluster pointers to match the order with the ECI's.
cluster_by_symm_group();
}
bool CEUpdater::all_decoration_nums_equal(const vector<int> &dec_nums) const {
for (unsigned int i = 1; i < dec_nums.size(); i++) {
if (dec_nums[i] != dec_nums[0]) {
return false;
}
}
return true;
}
void CEUpdater::get_singlets(PyObject *npy_obj) const {
PyObject *npy_array = PyArray_FROM_OTF(npy_obj, NPY_DOUBLE, NPY_OUT_ARRAY);
unsigned int npy_array_size = PyArray_SIZE(npy_array);
if (npy_array_size < singlets.size()) {
string msg("The passed Numpy array is too small to hold all the singlets terms!\n");
stringstream ss;
ss << "Minimum size: " << singlets.size() << ". Given size: " << npy_array_size;
msg += ss.str();
Py_DECREF(npy_array);
throw runtime_error(msg);
}
cf &cfs = history->get_current();
for (unsigned int i = 0; i < singlets.size(); i++) {
double *ptr = static_cast<double *>(PyArray_GETPTR1(npy_array, i));
*ptr = cfs[singlets[i]];
}
Py_DECREF(npy_array);
}
PyObject *CEUpdater::get_singlets() const {
npy_intp dims[1] = {static_cast<npy_intp>(singlets.size())};
PyObject *npy_array = PyArray_SimpleNew(1, dims, NPY_DOUBLE);
get_singlets(npy_array);
return npy_array;
}
void CEUpdater::create_cname_with_dec(PyObject *cf) {
if (!PyDict_Check(cf)) {
throw invalid_argument("Correlation functons has to be dictionary!");
}
Py_ssize_t pos = 0;
PyObject *key;
PyObject *value;
while (PyDict_Next(cf, &pos, &key, &value)) {
string new_key = py2string(key);
ClusterName c_name = ClusterName(new_key);
unsigned int size = c_name.get_size();
#ifdef PRINT_DEBUG
cout << "Read CF: " << new_key << endl;
#endif
if ((size == 0 || size == 1)) {
cname_with_dec[new_key] = new_key;
} else {
std::string prefix = c_name.get_prefix();
cname_with_dec[prefix] = new_key;
}
}
}
void CEUpdater::build_trans_symm_group(PyObject *py_trans_symm_group) {
// Fill the symmetry group array with -1 indicating an invalid value
for (unsigned int i = 0; i < trans_symm_group.size(); i++) {
trans_symm_group[i] = -1;
}
unsigned int py_list_size = list_size(py_trans_symm_group);
for (unsigned int i = 0; i < py_list_size; i++) {
PyObject *sublist = PyList_GetItem(py_trans_symm_group, i);
unsigned int n_sites = list_size(sublist);
for (unsigned int j = 0; j < n_sites; j++) {
int indx = py2int(PyList_GetItem(sublist, j));
if (trans_symm_group[indx] != -1) {
throw runtime_error(
"One site appears to be present in more than one translation symmetry "
"group!");
}
trans_symm_group[indx] = i;
}
}
// Check that all sites belongs to one translational symmetry group
for (unsigned int i = 0; i < trans_symm_group.size(); i++) {
if ((trans_symm_group[i] == -1) && !is_background_index[i]) {
stringstream msg;
msg << "Site " << i << " has not been assigned to any translational symmetry group!";
throw runtime_error(msg.str());
}
}
// Count the number of atoms in each symmetry group
trans_symm_group_count.resize(py_list_size);
fill(trans_symm_group_count.begin(), trans_symm_group_count.end(), 0);
for (unsigned int i = 0; i < trans_symm_group.size(); i++) {
if (trans_symm_group[i] >= 0) {
trans_symm_group_count[trans_symm_group[i]] += 1;
}
}
}
bool CEUpdater::all_eci_corresponds_to_cf() {
cf &corrfunc = history->get_current();
return eci.names_are_equal(corrfunc);
}
double CEUpdater::calculate(vector<swap_move> &sequence) {
if (sequence.size() >= history->max_history / 2) {
throw invalid_argument(
"The length of sequence of swap move exceeds the buffer size for the history "
"tracker");
}
for (unsigned int i = 0; i < sequence.size(); i++) {
calculate(sequence[i]);
}
return get_energy();
}
double CEUpdater::calculate(vector<SymbolChange> &sequence) {
for (auto &change : sequence) {
update_cf(change);
}
return get_energy();
}
void CEUpdater::read_trans_matrix(PyObject *py_trans_mat) {
bool is_list = PyList_Check(py_trans_mat);
#ifdef PRINT_DEBUG
cout << "read_trans_matrix: Extracting unique indices" << endl;
#endif
set<int> unique_indx;
clusters.unique_indices(unique_indx);
vector<int> unique_indx_vec;
set2vector(unique_indx, unique_indx_vec);
// Compute the max index that is ever going to be checked
unsigned int max_indx = clusters.max_index();
if (is_list) {
unsigned int size = list_size(py_trans_mat);
#ifdef PRINT_DEBUG
cout << "read_trans_matrix: Updating size of trans_matrix" << endl;
#endif
trans_matrix.set_size(size, unique_indx_vec.size(), max_indx);
#ifdef PRINT_DEBUG
cout << "read_trans_matrix: Setting lookup values in trans_matrix" << endl;
#endif
trans_matrix.set_lookup_values(unique_indx_vec);
#ifdef PRINT_DEBUG
cout << "read_trans_matrix: Reading translation matrix from list of dictionaries" << endl;
#endif
unsigned int n_elements_insterted = 0;
for (unsigned int i = 0; i < size; i++) {
// Background atoms are ignored (and should never be accessed)
if (is_background_index[i] && ignore_background_indices) {
continue;
}
PyObject *dict = PyList_GetItem(py_trans_mat, i);
for (unsigned int j = 0; j < unique_indx_vec.size(); j++) {
int col = unique_indx_vec[j];
PyObject *value = PyDict_GetItem(dict, int2py(col));
if (value == NULL) {
stringstream ss;
ss << "Requested value " << col << " is not a key in the dictionary!";
throw invalid_argument(ss.str());
}
trans_matrix(i, col) = py2int(value);
n_elements_insterted++;
}
}
#ifdef PRINT_DEBUG
cout << "Inserted " << n_elements_insterted << " into the translation matrix\n";
#endif
} else {
PyObject *trans_mat = PyArray_FROM_OTF(py_trans_mat, NPY_INT32, NPY_ARRAY_IN_ARRAY);
npy_intp *size = PyArray_DIMS(trans_mat);
trans_matrix.set_size(size[0], unique_indx_vec.size(), max_indx);
trans_matrix.set_lookup_values(unique_indx_vec);
#ifdef PRINT_DEBUG
cout << "Dimension of translation matrix stored: " << size[0] << " "
<< unique_indx_vec.size() << endl;
#endif
if (max_indx + 1 > size[1]) {
stringstream ss;
ss << "Something is wrong with the translation matrix passed.\n";
ss << "Shape of translation matrix (" << size[0] << "," << size[1] << ")\n";
ss << "Maximum index encountered in the cluster lists: " << max_indx << endl;
throw invalid_argument(ss.str());
}
for (unsigned int i = 0; i < size[0]; i++)
for (unsigned int j = 0; j < unique_indx_vec.size(); j++) {
int col = unique_indx_vec[j];
trans_matrix(i, col) = *static_cast<int *>(PyArray_GETPTR2(trans_mat, i, col));
}
Py_DECREF(trans_mat);
}
}
void CEUpdater::sort_indices(int indices[], const vector<int> &order, unsigned int n_indices) {
// This function is called many times
// profiling (with YEP) revealed that
// [] operator of the vector used quite a bit of time
// Therefore we here use raw C-arrays or pointer arithmetics
int sorted[4];
const int *ptr = &order[0];
for (unsigned int i = 0; i < n_indices; i++) {
sorted[i] = indices[*(ptr + i)];
}
memcpy(indices, sorted, n_indices * sizeof(int));
}
bool CEUpdater::is_swap_move(const swap_move &move) const {
return (move[0].old_symb == move[1].new_symb) && (move[1].new_symb == move[1].old_symb);
}
void CEUpdater::read_background_indices(PyObject *bkg_indices) {
// Fill array with false
is_background_index.resize(symbols_with_id->size());
fill(is_background_index.begin(), is_background_index.end(), false);
// Set to true if index is in bkg_indices
int size = list_size(bkg_indices);
for (int i = 0; i < size; i++) {
PyObject *py_indx = PyList_GetItem(bkg_indices, i);
int indx = py2int(py_indx);
is_background_index[indx] = true;
}
}
void CEUpdater::count_non_bkg_sites() {
// Count and store the number of non-background sites
num_non_bkg_sites = 0;
for (unsigned int atom_no = 0; atom_no < symbols_with_id->size(); atom_no++) {
if (!is_background_index[atom_no] || !ignore_background_indices) {
num_non_bkg_sites += 1;
}
}
}
void CEUpdater::get_changes(const std::vector<std::string> &new_symbols,
std::vector<unsigned int> &changed_sites) const {
if (new_symbols.size() != symbols_with_id->size()) {
throw invalid_argument("Size of passed atoms does not match!");
}
for (unsigned int i = 0; i < new_symbols.size(); i++) {
unsigned int symb_id = symbols_with_id->id(i);
if (symbols_with_id->get_symbol_id(new_symbols[i]) != symb_id) {
changed_sites.push_back(i);
}
}
}
void CEUpdater::calculate_cf_from_scratch(const vector<string> &cf_names, map<string, double> &cf) {
cf.clear();
// Initialise all cluster names
for (const string &name : cf_names) {
cf[name] = 0.0;
}
// Loop over all clusters
for (const string &name : cf_names) {
ClusterName c_name = ClusterName(name);
unsigned int cluster_size = c_name.get_size();
// Handle empty cluster
if (cluster_size == 0) {
cf[name] = 1.0;
continue;
}
// Handle singlet cluster
if (cluster_size == 1) {
unsigned int dec = c_name.get_dec_num();
double new_value = 0.0;
// Normalise with respect to the actual number of atoms included
for (unsigned int atom_no = 0; atom_no < symbols_with_id->size(); atom_no++) {
if (!is_background_index[atom_no] || !ignore_background_indices) {
new_value += basis_functions->get(dec, symbols_with_id->id(atom_no));
}
}
cf[name] = new_value / num_non_bkg_sites;
continue;
}
// Handle the rest of the clusters
std::string prefix, dec_str;
c_name.get_prefix_and_dec_str(prefix, dec_str);
double sp = 0.0;
double count = 0;
for (unsigned int atom_no = 0; atom_no < symbols_with_id->size(); atom_no++) {
int symm = trans_symm_group[atom_no];
if ((!clusters.is_in_symm_group(prefix, symm)) ||
(is_background_index[atom_no] && ignore_background_indices)) {
continue;
}
const Cluster &cluster = clusters.get(prefix, symm);
const equiv_deco_t &equiv_deco = cluster.get_equiv_deco(dec_str);
unsigned int ref_id = symbols_with_id->id(atom_no);
double sp_temp = 0.0;
for (const vector<int> &deco : equiv_deco) {
sp_temp += spin_product_one_atom(atom_no, cluster, deco, ref_id);
}
sp += sp_temp / equiv_deco.size();
count += cluster.get().size();
}
if (count == 0) {
cf[name] = 0.0;
} else {
cf[name] = sp / count;
}
}
history->get_current().init(cf);
}
void CEUpdater::set_atoms(PyObject *py_atoms) {
unsigned int num_atoms = PySequence_Length(py_atoms);
if (num_atoms != symbols_with_id->size()) {
throw invalid_argument("Length of passed atoms object is different from current");
}
std::vector<std::string> symbols = get_symbols_from_atoms(py_atoms);
this->atoms = py_atoms;
symbols_with_id->set_symbols(symbols);
}

Sorry, the diff of this file is too big to display