From 4c6fbcb5c68d9535e438c018c442099434e33391 Mon Sep 17 00:00:00 2001 From: lbluque Date: Fri, 2 Sep 2022 19:59:24 -0700 Subject: [PATCH 1/4] use dtype int and bool per numpy deprecation warning --- smol/cofe/extern/ewald.py | 4 ++-- smol/cofe/space/clusterspace.py | 2 +- smol/cofe/space/orbit.py | 2 +- smol/cofe/wrangling/wrangler.py | 4 ++-- smol/moca/processor/base.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/smol/cofe/extern/ewald.py b/smol/cofe/extern/ewald.py index fd0c02b0c..4aa122448 100644 --- a/smol/cofe/extern/ewald.py +++ b/smol/cofe/extern/ewald.py @@ -93,7 +93,7 @@ def get_ewald_structure(structure): inds[i] = len(ewald_sites) ewald_sites.append(PeriodicSite(spec, site.frac_coords, site.lattice)) ewald_inds.append(inds) - ewald_inds = np.array(ewald_inds, dtype=np.int) + ewald_inds = np.array(ewald_inds, dtype=int) ewald_structure = Structure.from_sites(ewald_sites) return ewald_structure, ewald_inds @@ -124,7 +124,7 @@ def get_ewald_occu(occu, num_ewald_sites, ewald_inds): # i_inds = i_inds[i_inds != -1] # just make b_inds one longer than it needs to be and don't return # the last value - b_inds = np.zeros(num_ewald_sites + 1, dtype=np.bool) + b_inds = np.zeros(num_ewald_sites + 1, dtype=bool) b_inds[i_inds] = True return b_inds[:-1] diff --git a/smol/cofe/space/clusterspace.py b/smol/cofe/space/clusterspace.py index 8d2bdf79b..df9cf7306 100644 --- a/smol/cofe/space/clusterspace.py +++ b/smol/cofe/space/clusterspace.py @@ -713,7 +713,7 @@ def occupancy_from_structure( if site_mapping is None: site_mapping = self.structure_site_mapping(supercell, structure) - occu = [] # np.zeros(len(self.supercell_structure), dtype=np.int) + occu = [] # np.zeros(len(self.supercell_structure), dtype=int) for i, allowed_species in enumerate(get_allowed_species(supercell)): # rather than starting with all vacancies and looping diff --git a/smol/cofe/space/orbit.py b/smol/cofe/space/orbit.py index 02e9f4f36..039a5b502 100644 --- a/smol/cofe/space/orbit.py +++ b/smol/cofe/space/orbit.py @@ -142,7 +142,7 @@ def bit_combos(self): all_combos = [] for bit_combo in product(*self.bits): if not any(np.array_equal(bit_combo, bc) for bc in chain(*all_combos)): - bit_combo = np.array(bit_combo, dtype=np.int_) + bit_combo = np.array(bit_combo, dtype=int) new_bits = np.unique(bit_combo[self.cluster_permutations], axis=0) all_combos.append(new_bits) self._bit_combos = tuple(all_combos) diff --git a/smol/cofe/wrangling/wrangler.py b/smol/cofe/wrangling/wrangler.py index f55ff52b4..fb87f79f7 100644 --- a/smol/cofe/wrangling/wrangler.py +++ b/smol/cofe/wrangling/wrangler.py @@ -618,7 +618,7 @@ def remove_properties(self, *property_keys): for entry in self._entries: del entry.data["properties"][key] except KeyError: - warnings.warn(f"Propertiy {key} does not exist.", RuntimeWarning) + warnings.warn(f"Property {key} does not exist.", RuntimeWarning) def remove_entry(self, entry): """Remove a given structure and associated data.""" @@ -877,7 +877,7 @@ def as_dict(self): "@class": self.__class__.__name__, "_subspace": self._subspace.as_dict(), "_entries": [entry.as_dict() for entry in self._entries], - "_ind_sets": jsanitize(self._ind_sets), # jic for np.int's + "_ind_sets": jsanitize(self._ind_sets), # jic for int's "metadata": self.metadata, } return wrangler_dict diff --git a/smol/moca/processor/base.py b/smol/moca/processor/base.py index d8e3265e3..59f91d0ae 100644 --- a/smol/moca/processor/base.py +++ b/smol/moca/processor/base.py @@ -190,7 +190,7 @@ def structure_from_occupancy(self, occupancy): def encode_occupancy(self, occupancy): """Encode occupancy string of Species object to ints.""" - # TODO check if setting to np.intc improves speed + # TODO check if setting to intc improves speed return np.array( [ species.index(spec) From 03fb6327ad0b01297d3612ea894a77f92a43538e Mon Sep 17 00:00:00 2001 From: lbluque Date: Fri, 2 Sep 2022 20:00:18 -0700 Subject: [PATCH 2/4] edit test_wrangler.py --- tests/test_cofe/test_wrangler.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_cofe/test_wrangler.py b/tests/test_cofe/test_wrangler.py index d4c12a5ce..08a2b0641 100644 --- a/tests/test_cofe/test_wrangler.py +++ b/tests/test_cofe/test_wrangler.py @@ -80,12 +80,10 @@ def test_add_data(structure_wrangler, rng): "normalized1", structure_wrangler.get_property_vector("energy", normalize=True) ) assert all( - prop in ["normalized_energy", "normalized", "normalized1", "random"] + prop in ["normalized", "normalized1", "random"] for prop in structure_wrangler.available_properties ) - structure_wrangler.remove_properties( - "normalized_energy", "normalized", "normalized1" - ) + structure_wrangler.remove_properties("normalized", "normalized1") assert structure_wrangler.available_properties == ["random"] # heavily distorted structure From cacb1c37a9b1c28bde7bdd548adc0ae98029366d Mon Sep 17 00:00:00 2001 From: lbluque Date: Fri, 2 Sep 2022 20:44:53 -0700 Subject: [PATCH 3/4] add warning filters to tests --- tests/test_cofe/test_basis.py | 1 + tests/test_cofe/test_clusterspace.py | 2 ++ tests/test_cofe/test_wrangler.py | 5 +++++ tests/test_moca/test_processor.py | 2 ++ 4 files changed, 10 insertions(+) diff --git a/tests/test_cofe/test_basis.py b/tests/test_cofe/test_basis.py index a79e40881..6bba7b892 100644 --- a/tests/test_cofe/test_basis.py +++ b/tests/test_cofe/test_basis.py @@ -11,6 +11,7 @@ from smol.utils import get_subclasses from tests.utils import assert_msonable +pytestmark = pytest.mark.filterwarnings("ignore:The measure given does not sum to 1.") basis_iterators = list(get_subclasses(basis.BasisIterator).values()) diff --git a/tests/test_cofe/test_clusterspace.py b/tests/test_cofe/test_clusterspace.py index 35ebe3ca8..7b7e10953 100644 --- a/tests/test_cofe/test_clusterspace.py +++ b/tests/test_cofe/test_clusterspace.py @@ -19,6 +19,8 @@ from smol.exceptions import StructureMatchError from tests.utils import assert_msonable, gen_random_structure +pytestmark = pytest.mark.filterwarnings("ignore:All bit combos have been removed") + def test_from_cutoffs(structure): cutoffs = {2: 5, 3: 4, 4: 4} diff --git a/tests/test_cofe/test_wrangler.py b/tests/test_cofe/test_wrangler.py index 08a2b0641..f047e59d6 100644 --- a/tests/test_cofe/test_wrangler.py +++ b/tests/test_cofe/test_wrangler.py @@ -9,6 +9,11 @@ from smol.cofe.extern import EwaldTerm from tests.utils import assert_msonable, gen_fake_training_data, gen_random_structure +pytestmark = [ + pytest.mark.filterwarnings("ignore:.*supercell_structure. Throwing out."), + pytest.mark.filterwarnings("ignore:.*have duplicated correlation vectors"), +] + def test_add_data(structure_wrangler, rng): for entry in gen_fake_training_data( diff --git a/tests/test_moca/test_processor.py b/tests/test_moca/test_processor.py index ba78f6589..721489ddf 100644 --- a/tests/test_moca/test_processor.py +++ b/tests/test_moca/test_processor.py @@ -13,6 +13,8 @@ from smol.moca.processor.base import Processor from tests.utils import assert_msonable, gen_random_occupancy, gen_random_structure +pytestmark = pytest.mark.filterwarnings("ignore:All bit combos have been removed") + RTOL = 0.0 # relative tolerance to check property change functions # absolute tolerance to check property change functions (eps is approx 2E-16) ATOL = 2e4 * np.finfo(float).eps From e74c2d89f8dc46f2a9f384d3bd3de3f7f456fae0 Mon Sep 17 00:00:00 2001 From: lbluque Date: Fri, 2 Sep 2022 21:01:43 -0700 Subject: [PATCH 4/4] remove canonical and semigrand ensembles --- smol/moca/__init__.py | 15 +- smol/moca/{ensemble => }/ensemble.py | 0 smol/moca/ensemble/__init__.py | 1 - smol/moca/ensemble/base.py | 254 --------------------------- smol/moca/ensemble/canonical.py | 68 ------- smol/moca/ensemble/semigrand.py | 247 -------------------------- 6 files changed, 5 insertions(+), 580 deletions(-) rename smol/moca/{ensemble => }/ensemble.py (100%) delete mode 100644 smol/moca/ensemble/__init__.py delete mode 100644 smol/moca/ensemble/base.py delete mode 100644 smol/moca/ensemble/canonical.py delete mode 100644 smol/moca/ensemble/semigrand.py diff --git a/smol/moca/__init__.py b/smol/moca/__init__.py index 950d6fbb8..66d5f7c4b 100644 --- a/smol/moca/__init__.py +++ b/smol/moca/__init__.py @@ -5,23 +5,18 @@ Monte Carlo simulations using Cluster Expansion Hamiltonians. """ +from smol.moca.ensemble import Ensemble +from smol.moca.processor.composite import CompositeProcessor +from smol.moca.processor.ewald import EwaldProcessor +from smol.moca.processor.expansion import ClusterExpansionProcessor from smol.moca.sampler.container import SampleContainer - -from .ensemble.canonical import CanonicalEnsemble -from .ensemble.ensemble import Ensemble -from .ensemble.semigrand import SemiGrandEnsemble -from .processor.composite import CompositeProcessor -from .processor.ewald import EwaldProcessor -from .processor.expansion import ClusterExpansionProcessor -from .sampler.sampler import Sampler +from smol.moca.sampler.sampler import Sampler __all__ = [ "ClusterExpansionProcessor", "EwaldProcessor", "CompositeProcessor", "Ensemble", - "CanonicalEnsemble", - "SemiGrandEnsemble", "Sampler", "SampleContainer", ] diff --git a/smol/moca/ensemble/ensemble.py b/smol/moca/ensemble.py similarity index 100% rename from smol/moca/ensemble/ensemble.py rename to smol/moca/ensemble.py diff --git a/smol/moca/ensemble/__init__.py b/smol/moca/ensemble/__init__.py deleted file mode 100644 index 26c5d5c09..000000000 --- a/smol/moca/ensemble/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Ensemble classes to run MC simulations based on Cluster Expansions.""" diff --git a/smol/moca/ensemble/base.py b/smol/moca/ensemble/base.py deleted file mode 100644 index d1bfd78a9..000000000 --- a/smol/moca/ensemble/base.py +++ /dev/null @@ -1,254 +0,0 @@ -"""Abstract base class for Monte Carlo Ensembles.""" - -__author__ = "Luis Barroso-Luque" - -import warnings -from abc import ABC, abstractmethod - -from smol.moca.processor import ( - ClusterExpansionProcessor, - CompositeProcessor, - EwaldProcessor, -) - - -class BaseEnsemble(ABC): - """Abstract base class for Monte Carlo Ensembles. - - Attributes: - num_energy_coefs (int): - Number of coefficients in the natural parameters array that - correspond to energy only. - thermo_boundaries (dict): - Dictionary with corresponding thermodynamic boundaries, i.e. - chemical potentials or fugacity fractions. This is kept only for - descriptive purposes. - """ - - def __init__(self, processor, sublattices=None): - """Initialize class instance. - - Args: - processor (Processor): - a processor that can compute the change in property given - a set of flips. - sublattices (list of Sublattice): optional - list of Sublattice objects representing sites in the processor - supercell with same site spaces. - """ - # deprecation warning - warnings.warn( - f"{type(self).__name__} is deprecated; use Ensemble in smol.moca instead.", - category=FutureWarning, - stacklevel=2, - ) - - if sublattices is None: - sublattices = processor.get_sublattices() - self.num_energy_coefs = len(processor.coefs) - self.thermo_boundaries = {} # not pretty way to save general info - self._processor = processor - self._sublattices = sublattices - - @classmethod - def from_cluster_expansion(cls, cluster_expansion, supercell_matrix, **kwargs): - """Initialize an ensemble from a cluster expansion. - - Convenience constructor to instantiate an ensemble. This will take - care of initializing the correct processor based on the - ClusterExpansion. - - Args: - cluster_expansion (ClusterExpansion): - A cluster expansion object. - supercell_matrix (ndarray): - Supercell matrix defining the system size. - **kwargs: - Keyword arguments to pass to ensemble constructor. Such as - sublattices, sublattice_probabilities, chemical_potentials, - fugacity_fractions. - - Returns: - Ensemble - """ - if len(cluster_expansion.cluster_subspace.external_terms) > 0: - processor = CompositeProcessor( - cluster_expansion.cluster_subspace, supercell_matrix - ) - ceprocessor = ClusterExpansionProcessor( - cluster_expansion.cluster_subspace, - supercell_matrix, - cluster_expansion.coefs[:-1], - ) - processor.add_processor(ceprocessor) - # at some point determine term and spinup processor maybe with a - # factory, if we ever implement more external terms. - ewald_term = cluster_expansion.cluster_subspace.external_terms[0] - ewprocessor = EwaldProcessor( - cluster_expansion.cluster_subspace, - supercell_matrix, - ewald_term=ewald_term, - coefficient=cluster_expansion.coefs[-1], - ) - processor.add_processor(ewprocessor) - else: - processor = ClusterExpansionProcessor( - cluster_expansion.cluster_subspace, - supercell_matrix, - cluster_expansion.coefs, - ) - return cls(processor, **kwargs) - - @property - def num_sites(self): - """Get the total number of sites in the supercell.""" - return self.processor.num_sites - - @property - def system_size(self): - """Get size of supercell in number of prims.""" - return self.processor.size - - @property - def processor(self): - """Get the ensemble processor.""" - return self._processor - - # TODO make a setter for these that checks sublattices are correct and - # all sites are included. - @property - def sublattices(self): - """Get list of Sublattices included in ensemble.""" - return self._sublattices - - @property - def active_sublattices(self): - """Get list of active sub-lattices.""" - return [s for s in self.sublattices if s.is_active] - - @property - def restricted_sites(self): - """Get indices of all restricted sites.""" - sites = [] - for sublattice in self.sublattices: - sites += sublattice.restricted_sites - return sites - - @property - def species(self): - """Species on active sublattices. - - These are minimal species required in setting chemical potentials. - """ - return list( - {sp for sublatt in self.active_sublattices for sp in sublatt.site_space} - ) - - def split_sublattice_by_species(self, sublattice_id, occu, species_in_partitions): - """Split a sub-lattice in system by its occupied species. - - An example use case might be simulating topotactic Li extraction - and insertion, where we want to consider Li/Vac, TM and O as - different sub-lattices that can not be mixed by swapping. - - Args: - sublattice_id (int): - The index of sub-lattice to split in self.sublattices. - occu (np.ndarray[int]): - An occupancy array to reference with. - species_in_partitions (List[List[int|Species|Vacancy|Element|str]]): - Each sub-list contains a few species or encodings of species in - the site space to be grouped as a new sub-lattice, namely, - sites with occu[sites] == specie in the sub-list, will be - used to initialize a new sub-lattice. - Sub-lists will be pre-sorted to ascending order. - """ - splits = self.sublattices[sublattice_id].split_by_species( - occu, species_in_partitions - ) - self._sublattices = ( - self._sublattices[:sublattice_id] - + splits - + self._sublattices[sublattice_id + 1 :] - ) - - @property - @abstractmethod - def natural_parameters(self): - """Get the vector of natural parameters. - - The natural parameters correspond to the fit coefficients of the - underlying processor plus any additional terms involved in the Legendre - transformation corresponding to the ensemble. - """ - return - - @abstractmethod - def compute_feature_vector(self, occupancy): - """Compute the feature vector for a given occupancy. - - The feature vector includes the necessary features required to compute - the exponent determining the relative probability for the given - occupancy (i.e. a generalized enthalpy). The feature vector for - ensembles represents the sufficient statistics. - - For a cluster expansion, the feature vector is the - correlation vector x system size - - Args: - occupancy (ndarray): - encoded occupancy string - - Returns: - ndarray: feature vector - """ - return - - @abstractmethod - def compute_feature_vector_change(self, occupancy, step): - """Compute the change in the feature vector from a given step. - - Args: - occupancy (ndarray): - encoded occupancy string. - step (list of tuple): - a sequence of flips given by MCUsher.propose_step - - Returns: - ndarray: difference in feature vector - """ - return - - def restrict_sites(self, sites): - """Restricts (freezes) the given sites. - - This will exclude those sites from being flipped during a Monte Carlo - run. If some of the given indices refer to inactive sites, there will - be no effect. - - Args: - sites (Sequence): - indices of sites in the occupancy string to restrict. - """ - for sublattice in self.sublattices: - sublattice.restrict_sites(sites) - - def reset_restricted_sites(self): - """Unfreeze all previously restricted sites.""" - for sublattice in self.sublattices: - sublattice.reset_restricted_sites() - - def as_dict(self): - """Get Json-serialization dict representation. - - Returns: - MSONable dict - """ - ensemble_d = { - "@module": self.__class__.__module__, - "@class": self.__class__.__name__, - "thermo_boundaries": self.thermo_boundaries, - "processor": self._processor.as_dict(), - "sublattices": [s.as_dict() for s in self._sublattices], - } - return ensemble_d diff --git a/smol/moca/ensemble/canonical.py b/smol/moca/ensemble/canonical.py deleted file mode 100644 index d9003577f..000000000 --- a/smol/moca/ensemble/canonical.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -Implementation of a Canonical Ensemble class. - -These are used when running Monte Carlo simulations for systems -with a fixed number of sites and fixed concentration of species. -""" - -__author__ = "Luis Barroso-Luque" - - -from monty.json import MSONable - -from smol.moca.ensemble.base import BaseEnsemble -from smol.moca.processor.base import Processor -from smol.moca.sublattice import Sublattice - - -class CanonicalEnsemble(BaseEnsemble, MSONable): - """Canonical Ensemble class to run Monte Carlo Simulations.""" - - @property - def natural_parameters(self): - """Get the vector of exponential parameters.""" - return self.processor.coefs - - def compute_feature_vector(self, occupancy): - """Compute the feature vector for a given occupancy. - - In the canonical case it is just the feature vector from the underlying - processor. - - Args: - occupancy (ndarray): - encoded occupancy string - - Returns: - ndarray: feature vector - """ - return self.processor.compute_feature_vector(occupancy) - - def compute_feature_vector_change(self, occupancy, step): - """Compute the change in the feature vector from a given step. - - Args: - occupancy (ndarray): - encoded occupancy string. - step (list of tuple): - A sequence of flips as given by MCUsher.propose_step - - Returns: - ndarray: difference in feature vector - """ - return self.processor.compute_feature_vector_change(occupancy, step) - - @classmethod - def from_dict(cls, d): - """Instantiate a CanonicalEnsemble from dict representation. - - Args: - d (dict): - dictionary representation. - Returns: - CanonicalEnsemble - """ - return cls( - Processor.from_dict(d["processor"]), - [Sublattice.from_dict(s) for s in d["sublattices"]], - ) diff --git a/smol/moca/ensemble/semigrand.py b/smol/moca/ensemble/semigrand.py deleted file mode 100644 index 6555284a0..000000000 --- a/smol/moca/ensemble/semigrand.py +++ /dev/null @@ -1,247 +0,0 @@ -"""Implementation of Semi-Grand Canonical Ensemble classes. - -These are used to run Monte Carlo sampling for systems with -a fixed number of sites but variable concentration of species. -""" - -__author__ = "Luis Barroso-Luque" - - -from collections import Counter - -import numpy as np -from monty.json import MSONable -from pymatgen.core import DummySpecies, Element, Species - -from smol.cofe.space.domain import Vacancy, get_species -from smol.moca.processor.base import Processor -from smol.moca.sublattice import Sublattice - -from .base import BaseEnsemble - - -class SemiGrandEnsemble(BaseEnsemble, MSONable): - """Relative chemical potential-based SemiGrand Ensemble. - - A Semi-Grand Canonical Ensemble for Monte Carlo simulations where species' - relative chemical potentials are predefined. Note that in the SGC Ensemble - implemented here, only the differences in chemical potentials with - respect to a reference species on each sublattice are fixed, and not the - absolute values. To obtain the absolute values you must calculate the - reference chemical potential and then subtract it from the given values. - - Attributes: - thermo_boundaries (dict): - dict of chemical potentials. - """ - - def __init__(self, processor, chemical_potentials, sublattices=None): - """Initialize MuSemiGrandEnsemble. - - Args: - processor (Processor): - a processor that can compute the change in property given - a set of flips. - chemical_potentials (dict): - Dictionary with species and chemical potentials. - sublattices (list of Sublattice): optional - list of Sublattice objects representing sites in the processor - supercell with same site spaces. - """ - super().__init__(processor, sublattices=sublattices) - self._params = np.append(self.processor.coefs, -1.0) - # check that species are valid - chemical_potentials = { - get_species(k): v for k, v in chemical_potentials.items() - } - # Excessive species not appeared on active sub-lattices - # will be dropped. - for spec in self.species: - if spec not in chemical_potentials.keys(): - raise ValueError( - f"Species {spec} was not assigned a chemical " - " potential, a value must be provided." - ) - - # preallocate this for slight speed improvements - self._dfeatures = np.empty(len(processor.coefs) + 1) - self._features = np.empty(len(processor.coefs) + 1) - - self._mus = {k: v for k, v in chemical_potentials.items() if k in self.species} - self._mu_table = self._build_mu_table(self._mus) - self.thermo_boundaries = { - "chemical-potentials": {str(k): v for k, v in self._mus.items()} - } - - @property - def natural_parameters(self): - """Get the vector of natural parameters. - - For SGC an extra -1 is added for the chemical part of the Legendre - transform. - """ - return self._params - - @property - def chemical_potentials(self): - """Get the chemical potentials for species in the system.""" - return self._mus - - @chemical_potentials.setter - def chemical_potentials(self, value): - """Set the chemical potentials and update table.""" - for spec, count in Counter(map(get_species, value.keys())).items(): - if count > 1: - raise ValueError( - f"{count} values of the chemical potential for the same " - f"species {spec} were provided.\n Make sure the dictionary " - "you are using has only string keys or only Species " - "objects as keys." - ) - value = {get_species(k): v for k, v in value.items() if k in self.species} - if set(value.keys()) != set(self.species): - raise ValueError( - "Chemical potentials given are missing species. " - "Values must be given for each of the following:" - f" {self.species}" - ) - self._mus = value - self._mu_table = self._build_mu_table(value) - self.thermo_boundaries = { - "chemical-potentials": {str(k): v for k, v in self._mus.items()} - } - - def compute_feature_vector(self, occupancy): - """Compute the relevant feature vector for a given occupancy. - - In the semigrand case it is the feature vector and the chemical work - term. - - Args: - occupancy (ndarray): - encoded occupancy string - - Returns: - ndarray: feature vector - """ - self._features[:-1] = self.processor.compute_feature_vector(occupancy) - self._features[-1] = self.compute_chemical_work(occupancy) - return self._features - - def compute_feature_vector_change(self, occupancy, step): - """Return the change in the feature vector from a given step. - - Args: - occupancy (ndarray): - encoded occupancy string. - step (list of tuple): - a sequence of flips given by MCUsher.propose_step - - Returns: - ndarray: difference in feature vector - """ - self._dfeatures[:-1] = self.processor.compute_feature_vector_change( - occupancy, step - ) - self._dfeatures[-1] = sum( - self._mu_table[f[0]][f[1]] - self._mu_table[f[0]][occupancy[f[0]]] - for f in step - ) - return self._dfeatures - - def compute_chemical_work(self, occupancy): - """Compute sum of mu * N for given occupancy.""" - return sum( - self._mu_table[site][species] for site, species in enumerate(occupancy) - ) - - def split_sublattice_by_species(self, sublattice_id, occu, species_in_partitions): - """Split a sub-lattice in system by its occupied species. - - An example use case might be simulating topotactic Li extraction - and insertion, where we want to consider Li/Vac, TM and O as - different sub-lattices that can not be mixed by swapping. - - In the grand canonical ensemble, the mu table will also be updated - after split. - - Args: - sublattice_id (int): - The index of sub-lattice to split in self.sublattices. - occu (np.ndarray[int]): - An occupancy array to reference with. - species_in_partitions (List[List[int|Species|Vacancy|Element|str]]): - Each sub-list contains a few species or encodings of species in - the site space to be grouped as a new sub-lattice, namely, - sites with occu[sites] == specie in the sub-list, will be - used to initialize a new sub-lattice. - Sub-lists will be pre-sorted to ascending order. - """ - super().split_sublattice_by_species(sublattice_id, occu, species_in_partitions) - # Species in active sub-lattices may change after split. - # Need to reset and rebuild mu table. - new_chemical_potentials = {spec: self._mus[spec] for spec in self.species} - self.chemical_potentials = new_chemical_potentials - - def _build_mu_table(self, chemical_potentials): - """Build an array for chemical potentials for all sites in system. - - Rows represent sites and columns species. This allows quick evaluation - of chemical potential changes from flips. Not that the total number - of columns will be the number of species in the largest site space. For - smaller site spaces the values at those rows are meaningless and will - be given values of 0. Also rows representing sites with not partial - occupancy will have all 0 values and should never be used. - """ - # Mu table should be built with ensemble, rather than processor data. - # Otherwise you may get wrong species encoding if the sub-lattices are - # split. - num_cols = max(max(sl.encoding) for sl in self.sublattices) + 1 - # Sublattice can only be initialized as default, or splitted from default. - table = np.zeros((self.num_sites, num_cols)) - for sublatt in self.active_sublattices: - ordered_pots = [chemical_potentials[sp] for sp in sublatt.site_space] - table[sublatt.sites[:, None], sublatt.encoding] = ordered_pots - return table - - def as_dict(self): - """Get Json-serialization dict representation. - - Returns: - MSONable dict - """ - sgce_d = super().as_dict() - sgce_d["chemical_potentials"] = tuple( - (s.as_dict(), c) for s, c in self.chemical_potentials.items() - ) - return sgce_d - - @classmethod - def from_dict(cls, d): - """Instantiate a SemiGrandEnsemble from dict representation. - - Returns: - CanonicalEnsemble - """ - chemical_potentials = {} - for spec, chem_pot in d["chemical_potentials"]: - if "oxidation_state" in spec and Element.is_valid_symbol(spec["element"]): - spec = Species.from_dict(spec) - elif "oxidation_state" in spec: - if spec["@class"] == "Vacancy": - spec = Vacancy.from_dict(spec) - else: - spec = DummySpecies.from_dict(spec) - else: - spec = Element(spec["element"]) - chemical_potentials[spec] = chem_pot - - sublatts = d.get("sublattices") # keep backwards compatibility - if sublatts is not None: - sublatts = [Sublattice.from_dict(sl_d) for sl_d in sublatts] - - return cls( - Processor.from_dict(d["processor"]), - chemical_potentials=chemical_potentials, - sublattices=sublatts, - )