def test_is_swap_possible(self):
        """Tests is_swap_possible function."""

        # swaps are possible
        for i, sl in enumerate(self.cm.sublattices):
            self.assertTrue(self.cm.is_swap_possible(i))

        # setup system with inactive sublattice
        prim = bulk('Al').repeat([2, 1, 1])
        chemical_symbols = [['Al'], ['Ag', 'Al', 'Au']]
        cs = ClusterSpace(prim, cutoffs=[0], chemical_symbols=chemical_symbols)

        supercell = prim.repeat(2)
        supercell[1].symbol = 'Ag'
        supercell[3].symbol = 'Au'
        sublattices = cs.get_sublattices(supercell)
        cm = ConfigurationManager(supercell, sublattices)

        # check both sublattices
        self.assertTrue(cm.is_swap_possible(0))
        self.assertFalse(cm.is_swap_possible(1))

        # check both sublattices when specifying allowed species
        allowed_species = [13, 47]
        self.assertTrue(cm.is_swap_possible(0,
                                            allowed_species=allowed_species))
        self.assertFalse(cm.is_swap_possible(1,
                                             allowed_species=allowed_species))
def _concentrations_fit_structure(structure: Atoms,
                                  cluster_space: ClusterSpace,
                                  concentrations: Dict[str, Dict[str, float]],
                                  tol: float = 1e-5) -> bool:
    """
    Check if specified concentrations are commensurate with a
    certain supercell (including sublattices)

    Parameters
    ----------
    structure
        atomic configuration to be checked
    cluster_space
        corresponding cluster space
    concentrations
        which concentrations, per sublattice, e.g., ``{'A': {'Ag': 0.3, 'Au': 0.7}}``
    tol
        numerical tolerance
    """
    # Check that concentrations are OK in each sublattice
    for sublattice in cluster_space.get_sublattices(structure):
        if sublattice.symbol in concentrations:
            sl_conc = concentrations[sublattice.symbol]
            for conc in sl_conc.values():
                n_symbol = conc * len(sublattice.indices)
                if abs(int(round(n_symbol)) - n_symbol) > tol:
                    return False
    return True
 def __init__(self, *args, **kwargs):
     super(TestConfigurationManager, self).__init__(*args, **kwargs)
     self.structure = bulk('Al').repeat([2, 1, 1])
     self.structure[1].symbol = 'Ag'
     self.structure = self.structure.repeat(3)
     cs = ClusterSpace(self.structure, cutoffs=[0], chemical_symbols=['Ag', 'Al'])
     self.sublattices = cs.get_sublattices(self.structure)
    def test_get_swapped_state(self):
        """Tests the getting swap indices method."""

        for _ in range(1000):
            indices, elements = self.cm.get_swapped_state(0)
            index1 = indices[0]
            index2 = indices[1]
            self.assertNotEqual(
                self.cm.occupations[index1], self.cm.occupations[index2])
            self.assertNotEqual(
                elements[0], elements[1])
            self.assertEqual(self.cm.occupations[index1], elements[1])
            self.assertEqual(self.cm.occupations[index2], elements[0])

        # set everything to Al and see that swap is not possible
        indices = [i for i in range(len(self.structure))]
        elements = [13] * len(self.structure)
        self.cm.update_occupations(indices, elements)

        with self.assertRaises(SwapNotPossibleError) as context:
            indices, elements = self.cm.get_swapped_state(0)
        self.assertTrue("Cannot swap on sublattice" in str(context.exception))
        self.assertTrue("since it is full of" in str(context.exception))

        # setup a ternary system
        prim = bulk('Al').repeat([3, 1, 1])
        chemical_symbols = ['Ag', 'Al', 'Au']
        cs = ClusterSpace(prim, cutoffs=[0], chemical_symbols=chemical_symbols)

        for i, symbol in enumerate(chemical_symbols):
            prim[i].symbol = symbol
        supercell = prim.repeat(2)
        sublattices = cs.get_sublattices(supercell)
        cm = ConfigurationManager(supercell, sublattices)

        allowed_species = [13, 47]
        for _ in range(1000):
            indices, elements = cm.get_swapped_state(
                0, allowed_species=allowed_species)
            index1 = indices[0]
            index2 = indices[1]
            self.assertNotEqual(
                cm.occupations[index1], cm.occupations[index2])
            self.assertNotEqual(
                elements[0], elements[1])
            self.assertEqual(cm.occupations[index1], elements[1])
            self.assertEqual(cm.occupations[index2], elements[0])

        # set everything to Al and see that swap is not possible
        indices = [i for i in range(len(supercell))]
        elements = [13] * len(supercell)
        cm.update_occupations(indices, elements)

        with self.assertRaises(SwapNotPossibleError) as context:
            indices, elements = cm.get_swapped_state(
                0, allowed_species=allowed_species)
        self.assertTrue("Cannot swap on sublattice" in str(context.exception))
        self.assertTrue("since it is full of" in str(context.exception))
def occupy_structure_randomly(structure: Atoms, cluster_space: ClusterSpace,
                              target_concentrations: dict) -> None:
    """
    Occupy a structure with quasirandom order but fulfilling
    ``target_concentrations``.

    Parameters
    ----------
    structure
        ASE Atoms object that will be occupied randomly
    cluster_space
        cluster space (needed as it carries information about sublattices)
    target_concentrations
        concentration of each species in the target structure, per
        sublattice (for example ``{'Au': 0.5, 'Pd': 0.5}`` for a
        single sublattice Au-Pd structure, or
        ``{'A': {'Au': 0.5, 'Pd': 0.5}, 'B': {'H': 0.25, 'X': 0.75}}``
        for a system with two sublattices.
        The symbols defining sublattices ('A', 'B' etc) can be
        found by printing the `cluster_space`
    """
    target_concentrations = _validate_concentrations(
        cluster_space=cluster_space, concentrations=target_concentrations)

    if not _concentrations_fit_structure(structure, cluster_space,
                                         target_concentrations):
        raise ValueError('Structure with {} atoms cannot accomodate '
                         'target concentrations {}'.format(
                             len(structure), target_concentrations))

    symbols_all = [''] * len(structure)
    for sl in cluster_space.get_sublattices(structure):
        symbols = []  # type: List[str] # chemical_symbols in one sublattice
        chemical_symbols = sl.chemical_symbols
        if len(chemical_symbols) == 1:
            symbols += [chemical_symbols[0]] * len(sl.indices)
        else:
            sl_conc = target_concentrations[sl.symbol]
            for chemical_symbol in sl.chemical_symbols:
                n_symbol = int(
                    round(len(sl.indices) * sl_conc[chemical_symbol]))
                symbols += [chemical_symbol] * n_symbol

        # Should not happen but you never know
        assert len(symbols) == len(sl.indices)

        # Shuffle to introduce randomness
        random.shuffle(symbols)

        # Assign symbols to the right indices
        for symbol, lattice_site in zip(symbols, sl.indices):
            symbols_all[lattice_site] = symbol

    assert symbols_all.count('') == 0
    structure.set_chemical_symbols(symbols_all)
def _validate_concentrations(concentrations: dict,
                             cluster_space: ClusterSpace,
                             tol: float = 1e-5) -> dict:
    """
    Validates concentration specification against a cluster space
    (raises `ValueError` if they do not match).

    Parameters
    ----------
    concentrations
        concentration specification
    cluster_space
        cluster space to check against
    tol
        Numerical tolerance

    Returns
    -------
    target concentrations
        An adapted version of concentrations, which is always a dictionary
        even if there is only one sublattice
    """
    sls = cluster_space.get_sublattices(cluster_space.primitive_structure)

    if not isinstance(list(concentrations.values())[0], dict):
        concentrations = {'A': concentrations}

    # Ensure concentrations sum to 1 at each sublattice
    for sl_conc in concentrations.values():
        conc_sum = sum(list(sl_conc.values()))
        if abs(conc_sum - 1.0) > tol:
            raise ValueError(
                'Concentrations must sum up '
                'to 1 for each sublattice (not {})'.format(conc_sum))

    # Symbols need to match on each sublattice
    for sl in sls:
        if sl.symbol not in concentrations:
            if len(sl.chemical_symbols) > 1:
                raise ValueError('A sublattice ({}: {}) is missing in '
                                 'target_concentrations'.format(
                                     sl.symbol, list(sl.chemical_symbols)))
        else:
            sl_conc = concentrations[sl.symbol]
            if tuple(sorted(sl.chemical_symbols)) != tuple(
                    sorted(list(sl_conc.keys()))):
                raise ValueError(
                    'Chemical symbols on a sublattice ({}: {}) are '
                    'not the same as those in the specified '
                    'concentrations {}'.format(sl.symbol,
                                               list(sl.chemical_symbols),
                                               list(sl_conc.keys())))

    return concentrations
Ejemplo n.º 7
0
    def test_sublattice_uniqueness(self):
        """Tests that the number of sublattices are correct
        in the case of the allowed species have duplicates in them.
        """
        structure = bulk("Al").repeat(2)

        chemical_symbols = [['H']] + [['Al', 'Ge']] * (len(structure) - 1)
        cs = ClusterSpace(structure=structure,
                          chemical_symbols=chemical_symbols,
                          cutoffs=[5])
        sublattices = cs.get_sublattices(structure)

        self.assertEqual(len(sublattices), 2)
        self.assertEqual(sublattices.allowed_species, [('Al', 'Ge'), ('H', )])
    def test_get_flip_index(self):
        """Tests the getting flip indices method."""

        for _ in range(1000):
            index, element = self.cm.get_flip_state(0)
            self.assertNotEqual(self.cm.occupations[index], element)

        # setup a ternary system
        prim = bulk('Al').repeat([3, 1, 1])
        chemical_symbols = ['Ag', 'Al', 'Au']
        cs = ClusterSpace(prim, cutoffs=[0], chemical_symbols=chemical_symbols)

        for i, symbol in enumerate(chemical_symbols):
            prim[i].symbol = symbol
        supercell = prim.repeat(2)
        sublattices = cs.get_sublattices(supercell)
        cm = ConfigurationManager(supercell, sublattices)

        allowed_species = [13, 47]
        for _ in range(1000):
            index, element = cm.get_flip_state(
                0, allowed_species=allowed_species)
            self.assertNotEqual(cm.occupations[index], element)
def _get_sqs_cluster_vector(
        cluster_space: ClusterSpace,
        target_concentrations: Dict[str, Dict[str, float]]) -> np.ndarray:
    """
    Get the SQS vector for a certain cluster space and certain
    concentration. Here SQS vector refers to the cluster vector of an
    infintely large supercell with random occupation.

    Parameters
    ----------
    cluster_space
        the kind of lattice to be occupied
    target_concentrations
        concentration of each species in the target structure,
        per sublattice (for example `{'A': {'Ag': 0.5, 'Pd': 0.5}}`)
    """
    target_concentrations = _validate_concentrations(
        concentrations=target_concentrations, cluster_space=cluster_space)

    sublattice_to_index = {
        letter: index
        for index, letter in enumerate('ABCDEFGHIJKLMNOPQRSTUVWXYZ')
    }
    all_sublattices = cluster_space.get_sublattices(
        cluster_space.primitive_structure)

    # Make a map from chemical symbol to integer, later on used
    # for evaluating cluster functions.
    # Internally, icet sorts species according to atomic numbers.
    # Also check that each symbol only occurs in one sublattice.
    symbol_to_integer_map = {}
    found_species = []  # type: List[str]
    for sublattice in all_sublattices:
        if len(sublattice.chemical_symbols) < 2:
            continue
        atomic_numbers = [
            periodic_table.index(sym) for sym in sublattice.chemical_symbols
        ]
        for i, species in enumerate(sorted(atomic_numbers)):
            found_species.append(species)
            symbol_to_integer_map[periodic_table[species]] = i

    # Target concentrations refer to all atoms, but probabilities only
    # to the sublattice.
    probabilities = {}
    for sl_conc in target_concentrations.values():
        if len(sl_conc) == 1:
            continue
        for symbol in sl_conc.keys():
            probabilities[symbol] = sl_conc[symbol]

    # For every orbit, calculate average cluster function
    cv = [1.0]
    for orbit in cluster_space.orbit_data:
        if orbit['order'] < 1:
            continue

        # What sublattices are there in this orbit?
        sublattices = [
            all_sublattices[sublattice_to_index[letter]]
            for letter in orbit['sublattices'].split('-')
        ]

        # What chemical symbols do these sublattices refer to?
        symbol_groups = [
            sublattice.chemical_symbols for sublattice in sublattices
        ]

        # How many allowed species in each of those sublattices?
        nbr_of_allowed_species = [
            len(symbol_group) for symbol_group in symbol_groups
        ]

        # Calculate contribution from every possible combination of
        # symbols weighted with their probability
        cluster_product_average = 0
        for symbols in itertools.product(*symbol_groups):
            cluster_product = 1
            for i, symbol in enumerate(symbols):
                mc_vector_component = orbit['multi_component_vector'][i]
                species_i = symbol_to_integer_map[symbol]
                prod = cluster_space.evaluate_cluster_function(
                    nbr_of_allowed_species[i], mc_vector_component, species_i)
                cluster_product *= probabilities[symbol] * prod
            cluster_product_average += cluster_product
        cv.append(cluster_product_average)
    return np.array(cv)
def generate_sqs_by_enumeration(cluster_space: ClusterSpace,
                                max_size: int,
                                target_concentrations: dict,
                                include_smaller_cells: bool = True,
                                pbc: Union[Tuple[bool, bool, bool],
                                           Tuple[int, int, int]] = None,
                                optimality_weight: float = 1.0,
                                tol: float = 1e-5) -> Atoms:
    """
    Given a ``cluster_space``, generate a special quasirandom structure
    (SQS), i.e., a structure that for a given supercell size provides
    the best possible approximation to a random alloy [ZunWeiFer90]_.

    In the present case, this means that the generated structure will
    have a cluster vector that as closely as possible matches the
    cluster vector of an infintely large randomly occupied supercell.
    Internally the function uses a simulated annealing algorithm and the
    difference between two cluster vectors is calculated with the
    measure suggested by A. van de Walle et al. in Calphad **42**, 13-18
    (2013) [WalTiwJon13]_ (for more information, see
    :class:`mchammer.calculators.TargetVectorCalculator`).

    This functions generates SQS cells by exhaustive enumeration, which
    means that the generated SQS cell is guaranteed to be optimal with
    regard to the specified measure and cell size.

    Parameters
    ----------
    cluster_space
        a cluster space defining the lattice to be occupied
    max_size
        maximum supercell size
    target_concentrations
        concentration of each species in the target structure, per
        sublattice (for example ``{'Au': 0.5, 'Pd': 0.5}`` for a
        single sublattice Au-Pd structure, or
        ``{'A': {'Au': 0.5, 'Pd': 0.5}, 'B': {'H': 0.25, 'X': 0.75}}``
        for a system with two sublattices.
        The symbols defining sublattices ('A', 'B' etc) can be
        found by printing the `cluster_space`
    include_smaller_cells
        if True, search among all supercell sizes including
        ``max_size``, else search only among those exactly matching
        ``max_size``
    pbc
        Periodic boundary conditions for each direction, e.g.,
        ``(True, True, False)``. The axes are defined by
        the cell of ``cluster_space.primitive_structure``.
        Default is periodic boundary in all directions.
    optimality_weight
        controls weighting :math:`L` of perfect correlations, see
        :class:`mchammer.calculators.TargetVectorCalculator`
    tol
        Numerical tolerance
    """
    target_concentrations = _validate_concentrations(target_concentrations,
                                                     cluster_space)
    sqs_vector = _get_sqs_cluster_vector(
        cluster_space=cluster_space,
        target_concentrations=target_concentrations)
    # Translate concentrations to the format required for concentration
    # restricted enumeration
    cr = {}  # type: Dict[str, tuple]
    sublattices = cluster_space.get_sublattices(
        cluster_space.primitive_structure)
    for sl in sublattices:
        mult_factor = len(sl.indices) / len(cluster_space.primitive_structure)
        if sl.symbol in target_concentrations:
            sl_conc = target_concentrations[sl.symbol]
        else:
            sl_conc = {sl.chemical_symbols[0]: 1.0}
        for species, value in sl_conc.items():
            c = value * mult_factor
            if species in cr:
                cr[species] = (cr[species][0] + c, cr[species][1] + c)
            else:
                cr[species] = (c, c)

    # Check to be sure...
    c_sum = sum(c[0] for c in cr.values())
    assert abs(c_sum - 1) < tol  # Should never happen, but...

    orbit_data = cluster_space.orbit_data
    best_score = 1e9

    if include_smaller_cells:
        sizes = list(range(1, max_size + 1))
    else:
        sizes = [max_size]

    # Prepare primitive structure with the right boundary conditions
    prim = cluster_space.primitive_structure
    if pbc is None:
        pbc = (True, True, True)
    prim.set_pbc(pbc)

    # Enumerate and calculate score for each structuer
    for structure in enumerate_structures(prim,
                                          sizes,
                                          cluster_space.chemical_symbols,
                                          concentration_restrictions=cr):
        cv = cluster_space.get_cluster_vector(structure)
        score = compare_cluster_vectors(cv_1=cv,
                                        cv_2=sqs_vector,
                                        orbit_data=orbit_data,
                                        optimality_weight=optimality_weight,
                                        tol=tol)

        if score < best_score:
            best_score = score
            best_structure = structure
    return best_structure
Ejemplo n.º 11
0
class TestGroundStateFinderInactiveSublatticeSameSpecies(unittest.TestCase):
    """Container for test of the class functionality for a system with an
    inactive sublattice occupied by a species found on the active
    sublattice."""
    def __init__(self, *args, **kwargs):
        super(TestGroundStateFinderInactiveSublatticeSameSpecies,
              self).__init__(*args, **kwargs)
        self.chemical_symbols = [['Ag', 'Au'], ['Ag']]
        self.cutoffs = [4.3]
        a = 4.0
        structure_prim = bulk(self.chemical_symbols[0][1], a=a)
        structure_prim.append(
            Atom(self.chemical_symbols[1][0], position=(a / 2, a / 2, a / 2)))
        self.structure_prim = structure_prim
        self.cs = ClusterSpace(self.structure_prim, self.cutoffs,
                               self.chemical_symbols)
        self.ce = ClusterExpansion(self.cs, [0, 0, 0.1, -0.02])
        self.all_possible_structures = []
        self.supercell = self.structure_prim.repeat(2)
        sublattices = self.cs.get_sublattices(self.supercell)
        self.n_active_sites = [
            len(subl.indices) for subl in sublattices.active_sublattices
        ]
        for i, sym in enumerate(self.supercell.get_chemical_symbols()):
            if sym not in self.chemical_symbols[0]:
                continue
            structure = self.supercell.copy()
            structure.symbols[i] = self.chemical_symbols[0][0]
            self.all_possible_structures.append(structure)

    def shortDescription(self):
        """Silences unittest from printing the docstrings in test cases."""
        return None

    def setUp(self):
        """Setup before each test."""
        self.gsf = icet.tools.ground_state_finder.GroundStateFinder(
            self.ce, self.supercell, verbose=False)

    def test_init(self):
        """Tests that initialization of tested class work."""
        # initialize from ClusterExpansion instance
        gsf = icet.tools.ground_state_finder.GroundStateFinder(self.ce,
                                                               self.supercell,
                                                               verbose=False)
        self.assertIsInstance(gsf,
                              icet.tools.ground_state_finder.GroundStateFinder)

    def test_get_ground_state(self):
        """Tests get_ground_state functionality."""
        target_val = min([
            self.ce.predict(structure)
            for structure in self.all_possible_structures
        ])

        # Provide counts for first species
        species_count = {self.chemical_symbols[0][0]: 1}
        ground_state = self.gsf.get_ground_state(species_count=species_count,
                                                 threads=1)
        predicted_species0 = self.ce.predict(ground_state)
        self.assertEqual(predicted_species0, target_val)

        species_count = {
            self.chemical_symbols[0][1]: self.n_active_sites[0] - 1
        }
        ground_state = self.gsf.get_ground_state(species_count=species_count,
                                                 threads=1)
        predicted_species1 = self.ce.predict(ground_state)
        self.assertEqual(predicted_species0, predicted_species1)

    def test_get_ground_state_fails_for_faulty_species_to_count(self):
        """Tests that get_ground_state fails if species_to_count is faulty."""
        # Check that get_ground_state fails if counts are provided for multiple species
        species_count = {
            self.chemical_symbols[0][0]: 1,
            self.chemical_symbols[0][1]: self.n_active_sites[0] - 1
        }
        with self.assertRaises(ValueError) as cm:
            self.gsf.get_ground_state(species_count=species_count)
        self.assertTrue(
            'Provide counts for at most one of the species on each active sublattice '
            '({}), not {}!'.format(self.gsf._active_species,
                                   list(species_count.keys())) in str(
                                       cm.exception))

        # Check that get_ground_state fails if counts are provided for a
        # species not found on the active sublattice
        species_count = {'H': 1}
        with self.assertRaises(ValueError) as cm:
            self.gsf.get_ground_state(species_count=species_count)
        self.assertTrue(
            'The species {} is not present on any of the active sublattices'
            ' ({})'.format(
                list(species_count.keys())[0],
                self.gsf._active_species) in str(cm.exception))

        # Check that get_ground_state fails if the count exceeds the number sites on the active
        # sublattice
        faulty_species = self.chemical_symbols[0][0]
        faulty_count = len(self.supercell)
        species_count = {faulty_species: faulty_count}
        n_active_sites = len([
            sym for sym in self.supercell.get_chemical_symbols()
            if sym == self.chemical_symbols[0][1]
        ])
        with self.assertRaises(ValueError) as cm:
            self.gsf.get_ground_state(species_count=species_count)
        self.assertTrue(
            'The count for species {} ({}) must be a positive integer and cannot '
            'exceed the number of sites on the active sublattice '
            '({})'.format(faulty_species, faulty_count, n_active_sites) in str(
                cm.exception))

    def test_create_cluster_maps(self):
        """Tests _create_cluster_maps functionality """
        gsf = icet.tools.ground_state_finder.GroundStateFinder(self.ce,
                                                               self.supercell,
                                                               verbose=False)
        gsf._create_cluster_maps(self.structure_prim)

        # Test cluster to sites map
        target = [[0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0],
                  [0, 0], [0, 0]]
        self.assertEqual(target, gsf._cluster_to_sites_map)

        # Test cluster to orbit map
        target = [0, 1, 1, 1, 1, 1, 1, 2, 2, 2]
        self.assertEqual(target, gsf._cluster_to_orbit_map)

        # Test ncluster per orbit map
        target = [1, 1, 6, 3]
        self.assertEqual(target, gsf._nclusters_per_orbit)
Ejemplo n.º 12
0
class BinaryShortRangeOrderObserver(BaseObserver):
    """
    This class represents a short range order (SRO) observer for a
    binary system.


    Parameters
    ----------
    cluster_space : icet.ClusterSpace
        cluster space used for initialization
    structure : ase.Atoms
        defines the lattice which the observer will work on
    interval : int
        the observation interval, defaults to None meaning that if the
        observer is used in a Monte Carlo simulations, then the Ensemble object
        will set the interval.
    radius : float
        the maximum radius  for the neigbhor shells considered

    Attributes
    ----------
    tag : str
        human readable observer name (`BinaryShortRangeOrderObserver`)
    interval : int
        observation interval

    Example
    -------
    The following snippet illustrate how to use the short-range order (SRO)
    observer in a Monte Carlo simulation of a bulk supercell. Here, the
    parameters of the cluster expansion are set to emulate a simple Ising model
    in order to obtain an example that can be run without modification. In
    practice, one should of course use a proper cluster expansion::

        >>> from ase.build import bulk
        >>> from icet import ClusterExpansion, ClusterSpace
        >>> from mchammer.calculators import ClusterExpansionCalculator
        >>> from mchammer.ensembles import CanonicalEnsemble
        >>> from mchammer.observers import BinaryShortRangeOrderObserver

        >>> # prepare cluster expansion
        >>> # the setup emulates a second nearest-neighbor (NN) Ising model
        >>> # (zerolet and singlet ECIs are zero; only first and second neighbor
        >>> # pairs are included)
        >>> prim = bulk('Au')
        >>> cs = ClusterSpace(prim, cutoffs=[4.3], chemical_symbols=['Ag', 'Au'])
        >>> ce = ClusterExpansion(cs, [0, 0, 0.1, -0.02])

        >>> # prepare initial configuration
        >>> nAg = 10
        >>> structure = prim.repeat(3)
        >>> structure.set_chemical_symbols(nAg * ['Ag'] + (len(structure) - nAg) * ['Au'])

        >>> # set up MC simulation
        >>> calc = ClusterExpansionCalculator(structure, ce)
        >>> mc = CanonicalEnsemble(structure=structure, calculator=calc, temperature=600,
        ...                        dc_filename='myrun_sro.dc')

        # set up observer and attach it to the MC simulation
        sro = BinaryShortRangeOrderObserver(cs, structure, interval=len(structure),
                                            radius=4.3)
        mc.attach_observer(sro)

        # run 1000 trial steps
        mc.run(1000)

    After having run this snippet one can access the SRO parameters via the
    data container::

        print(mc.data_container.data)
    """
    def __init__(self,
                 cluster_space,
                 structure: Atoms,
                 radius: float,
                 interval: int = None) -> None:
        super().__init__(interval=interval,
                         return_type=dict,
                         tag='BinaryShortRangeOrderObserver')

        self._structure = structure

        self._cluster_space = ClusterSpace(
            structure=cluster_space.primitive_structure,
            cutoffs=[radius],
            chemical_symbols=cluster_space.chemical_symbols)
        self._cluster_count_observer = ClusterCountObserver(
            cluster_space=self._cluster_space,
            structure=structure,
            interval=interval)

        self._sublattices = self._cluster_space.get_sublattices(structure)
        binary_sublattice_counts = 0
        for symbols in self._sublattices.allowed_species:
            if len(symbols) == 2:
                binary_sublattice_counts += 1
                self._symbols = sorted(symbols)
            elif len(symbols) > 2:
                raise ValueError('Cluster space has more than two allowed'
                                 ' species on a sublattice. '
                                 'Allowed species: {}'.format(symbols))
        if binary_sublattice_counts != 1:
            raise ValueError('Number of binary sublattices must equal one,'
                             ' not {}'.format(binary_sublattice_counts))

    def get_observable(self, structure: Atoms) -> Dict[str, float]:
        """Returns the value of the property from a cluster expansion
        model for a given atomic configurations.

        Parameters
        ----------
        structure
            input atomic structure
        """

        self._cluster_count_observer._generate_counts(structure)
        df = self._cluster_count_observer.count_frame

        symbol_counts = self._get_atom_count(structure)
        conc_B = self._get_concentrations(structure)[self._symbols[0]]

        pair_orbit_indices = set(
            df.loc[df['order'] == 2]['orbit_index'].tolist())
        N = symbol_counts[self._symbols[0]] + symbol_counts[self._symbols[1]]
        sro_parameters = {}
        for k, orbit_index in enumerate(sorted(pair_orbit_indices)):
            orbit_df = df.loc[df['orbit_index'] == orbit_index]
            A_B_pair_count = 0
            total_count = 0
            total_A_count = 0
            for i, row in orbit_df.iterrows():
                total_count += row.cluster_count
                if self._symbols[0] in row.occupation:
                    total_A_count += row.cluster_count
                if self._symbols[0] in row.occupation and \
                        self._symbols[1] in row.occupation:
                    A_B_pair_count += row.cluster_count

            key = 'sro_{}_{}'.format(self._symbols[0], k + 1)
            Z_tot = symbol_counts[self._symbols[0]] * 2 * total_count / N
            if conc_B == 1 or Z_tot == 0:
                value = 0
            else:
                value = 1 - A_B_pair_count / (Z_tot * (1 - conc_B))
            sro_parameters[key] = value

        return sro_parameters

    def _get_concentrations(self, structure: Atoms) -> Dict[str, float]:
        """Returns concentrations for each species relative its
        sublattice.

        Parameters
        ----------
        structure
            the configuration that will be analyzed
        """
        occupation = np.array(structure.get_chemical_symbols())
        concentrations = {}
        for sublattice in self._sublattices:
            if len(sublattice.chemical_symbols) == 1:
                continue
            for symbol in sublattice.chemical_symbols:
                symbol_count = occupation[sublattice.indices].tolist().count(
                    symbol)
                concentration = symbol_count / len(sublattice.indices)
                concentrations[symbol] = concentration
        return concentrations

    def _get_atom_count(self, structure: Atoms) -> Dict[str, float]:
        """Returns atom counts for each species relative its
        sublattice.

        Parameters
        ----------
        structure
            the configuration that will be analyzed
        """
        occupation = np.array(structure.get_chemical_symbols())
        counts = {}
        for sublattice in self._sublattices:
            if len(sublattice.chemical_symbols) == 1:
                continue
            for symbol in sublattice.chemical_symbols:
                symbol_count = occupation[sublattice.indices].tolist().count(
                    symbol)
                counts[symbol] = symbol_count
        return counts