Exemplo n.º 1
0
    def test_distances(self):
        """Tests that the periodicity is taken into account when calculating
        distances.
        """
        scaled_positions = [[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]]
        system = System(
            scaled_positions=scaled_positions,
            symbols=["H", "H"],
            cell=[
                [5, 5, 0],
                [0, -5, -5],
                [5, 0, 5]
            ],
        )
        disp = system.get_displacement_tensor()

        # For a non-periodic system, periodicity should not be taken into
        # account even if cell is defined.
        pos = system.get_positions()
        assumed = np.array([
            [pos[0] - pos[0], pos[1] - pos[0]],
            [pos[0] - pos[1], pos[1] - pos[1]],
        ])
        self.assertTrue(np.allclose(assumed, disp))

        # For a periodic system, the nearest copy should be considered when
        # comparing distances to neighbors or to self
        system.set_pbc([True, True, True])
        disp = system.get_displacement_tensor()
        assumed = np.array([
            [[5.0, 5.0, 0.0], [5, 0, 0]],
            [[-5, 0, 0], [5.0, 5.0, 0.0]]])
        self.assertTrue(np.allclose(assumed, disp))

        # Tests that the displacement tensor is found correctly even for highly
        # non-orthorhombic systems.
        positions = np.array([
            [1.56909, 2.71871, 6.45326],
            [3.9248, 4.07536, 6.45326]
        ])
        cell = np.array([
            [4.7077, -2.718, 0.],
            [0., 8.15225, 0.],
            [0., 0., 50.]
        ])
        system = System(
            positions=positions,
            symbols=["H", "H"],
            cell=cell,
            pbc=True,
        )

        # Fully periodic with minimum image convention
        dist_mat = system.get_distance_matrix()
        distance = dist_mat[0, 1]

        # The minimum image should be within the same cell
        expected = np.linalg.norm(positions[0, :] - positions[1, :])
        self.assertTrue(np.allclose(distance, expected))
Exemplo n.º 2
0
    def test_cell_wrap(self):
        """Test that coordinates are correctly wrapped inside the cell.
        """
        system = System(
            scaled_positions=[[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]],
            symbols=["H", "H"],
            cell=[
                [1, 0, 0],
                [0, 1, 0],
                [0, 0, 1]
            ],
            pbc = [True, True, True],
        )

        #orig = np.array([[2, 1.45, -4.8]])
        orig = np.array([[0.5, 0.5, 1.5]])
        scal = system.to_scaled(orig, wrap = True)
        cart = system.to_cartesian(scal)
        self.assertFalse(np.allclose(orig, cart))


        scal2 = system.to_scaled(orig)
        cart2 = system.to_cartesian(scal2, wrap = True)
        self.assertFalse(np.allclose(orig, cart2))

        
        scal3 = system.to_scaled(orig, True)
        cart3 = system.to_cartesian(scal3, wrap = True)
        self.assertFalse(np.allclose(orig, cart3))

        scal4 = system.to_scaled(orig)
        cart4 = system.to_cartesian(scal4)
        self.assertTrue(np.allclose(orig, cart4))
       
        self.assertTrue(np.allclose(cart2, cart3))
Exemplo n.º 3
0
    def test_transformations(self):
        """Test that coordinates are correctly transformed from scaled to
        cartesian and back again.
        """
        system = System(
            scaled_positions=[[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]],
            symbols=["H", "H"],
            cell=[[5, 5, 0], [0, -5, -5], [5, 0, 5]],
        )

        orig = np.array([[2, 1.45, -4.8]])
        scal = system.to_scaled(orig)
        cart = system.to_cartesian(scal)
        self.assertTrue(np.allclose(orig, cart))
Exemplo n.º 4
0
    def test_set_scaled_positions(self):
        """Test the method set_scaled_positions() of the System class
        """
        scaled_positions = [[0.0, 0.0, 0.0], [0.5, 0.5, 0.5]]
        system = System(
            scaled_positions=scaled_positions,
            symbols=["H", "H"],
            cell=[
                [5, 5, 0],
                [0, -5, -5],
                [5, 0, 5]
            ],
        )


        pos = system.get_scaled_positions()

        new_pos = pos * 2
        system.set_scaled_positions(new_pos)

        new_pos = system.get_scaled_positions()
        
        self.assertTrue(np.allclose(pos * 2, new_pos))
        self.assertFalse(np.allclose(pos, new_pos))
Exemplo n.º 5
0
    def create_extended_system(self, primitive_system, centers, radial_cutoff):
        """Used to create a periodically extended system, that is as small as
        possible by rejecting atoms for which the given weighting will be below
        the given threshold.

        Modified for the local MBTR to only consider distances from the central
        atom and to enable taking the virtual sites into account.

        Args:
            primitive_system (System): The original primitive system to
                duplicate.
            radial_cutoff (float): The radial cutoff to use in constructing the
                extended system.

        Returns:
            tuple: Tuple containing the new extended system as the first entry
            and the index of the periodically repeated cell for each atom as
            the second entry. The extended system is determined is extended so that each atom can at most
            have a weight that is larger or equivalent to the given threshold.
        """
        # We need to specify that the relative positions should not be wrapped.
        # Otherwise the repeated systems may overlap with the positions taken
        # with get_positions()
        relative_pos = np.array(
            primitive_system.get_scaled_positions(wrap=False))
        numbers = np.array(primitive_system.numbers)
        cartesian_pos = np.array(primitive_system.get_positions())
        cell = np.array(primitive_system.get_cell())

        # Determine the upper limit of how many copies we need in each cell
        # vector direction. We take as many copies as needed to reach the
        # radial cutoff.
        cell_vector_lengths = np.linalg.norm(cell, axis=1)
        n_copies_axis = np.zeros(3, dtype=int)
        cell_cuts = radial_cutoff / cell_vector_lengths
        n_copies_axis = np.ceil(cell_cuts).astype(np.int)

        # Create copies of the cell but keep track of the atoms in the
        # original cell
        num_extended = []
        pos_extended = []
        num_extended.append(numbers)
        pos_extended.append(cartesian_pos)
        a = np.array([1, 0, 0])
        b = np.array([0, 1, 0])
        c = np.array([0, 0, 1])
        cell_indices = [np.zeros((len(primitive_system), 3), dtype=int)]

        for i in range(-n_copies_axis[0], n_copies_axis[0] + 1):
            for j in range(-n_copies_axis[1], n_copies_axis[1] + 1):
                for k in range(-n_copies_axis[2], n_copies_axis[2] + 1):
                    if i == 0 and j == 0 and k == 0:
                        continue

                    # Calculate the positions of the copied atoms and filter
                    # out the atoms that are farther away than the given
                    # cutoff.
                    num_copy = np.array(numbers)
                    pos_copy = np.array(relative_pos)

                    pos_shifted = pos_copy - i * a - j * b - k * c
                    pos_copy_cartesian = np.dot(pos_shifted, cell)

                    # Only distances to the atoms within the interaction limit
                    # are considered.
                    distances = cdist(pos_copy_cartesian, centers)
                    weight_mask = distances < radial_cutoff

                    # Create a boolean mask that says if the atom is within the
                    # range from at least one atom in the original cell
                    valids_mask = np.any(weight_mask, axis=1)

                    if np.any(valids_mask):
                        valid_pos = pos_copy_cartesian[valids_mask]
                        valid_num = num_copy[valids_mask]
                        valid_ind = np.tile(np.array([i, j, k], dtype=int),
                                            (len(valid_num), 1))

                        pos_extended.append(valid_pos)
                        num_extended.append(valid_num)
                        cell_indices.append(valid_ind)

        pos_extended = np.concatenate(pos_extended)
        num_extended = np.concatenate(num_extended)
        cell_indices = np.vstack(cell_indices)

        extended_system = System(positions=pos_extended,
                                 numbers=num_extended,
                                 cell=cell,
                                 pbc=False)

        return extended_system, cell_indices
Exemplo n.º 6
0
    def create_single(
        self,
        system,
        positions,
    ):
        """Return the local many-body tensor representation for the given
        system and positions.

        Args:
            system (:class:`ase.Atoms` | :class:`.System`): Input system.
            positions (iterable): Positions or atom index of points, from
                which local_mbtr is created. Can be a list of integer numbers
                or a list of xyz-coordinates. If integers provided, the atoms
                at that index are used as centers. If positions provided, new
                atoms are added at that position.

        Returns:
            1D ndarray: The local many-body tensor representations of given
            positions, for k terms, as an array. These are ordered as given in
            positions.
        """
        # Transform the input system into the internal System-object
        system = self.get_system(system)

        # Check that the system does not have elements that are not in the list
        # of atomic numbers
        atomic_number_set = set(system.get_atomic_numbers())
        self.check_atomic_numbers(atomic_number_set)
        self._interaction_limit = len(system)
        system_positions = system.get_positions()
        system_atomic_numbers = system.get_atomic_numbers()

        # Ensure that the atomic number 0 is not present in the system
        if 0 in atomic_number_set:
            raise ValueError(
                "Please do not use the atomic number 0 in local MBTR as it "
                "is reserved to mark the atoms use as analysis centers.")

        # Form a list of indices, positions and atomic numbers for the local
        # centers. k=3 and k=2 use a slightly different approach, so two
        # versions are built
        i_new = len(system)
        indices_k2 = []
        new_pos_k2 = []
        new_atomic_numbers_k2 = []
        indices_k3 = []
        new_pos_k3 = []
        new_atomic_numbers_k3 = []
        n_atoms = len(system)
        if positions is not None:
            n_loc = len(positions)

            # Check validity of position definitions and create final cartesian
            # position list
            if len(positions) == 0:
                raise ValueError(
                    "The argument 'positions' should contain a non-empty set of"
                    " atomic indices or cartesian coordinates with x, y and z "
                    "components.")
            for i in positions:
                if np.issubdtype(type(i), np.integer):
                    i_len = len(system)
                    if i >= i_len or i < 0:
                        raise ValueError(
                            "The provided index {} is not valid for the system "
                            "with {} atoms.".format(i, i_len))
                    indices_k2.append(i)
                    indices_k3.append(i)
                    new_pos_k2.append(system_positions[i])
                    new_atomic_numbers_k2.append(system_atomic_numbers[i])
                elif isinstance(i, (list, tuple, np.ndarray)):
                    if len(i) != 3:
                        raise ValueError(
                            "The argument 'positions' should contain a "
                            "non-empty set of atomic indices or cartesian "
                            "coordinates with x, y and z components.")
                    new_pos_k2.append(np.array(i))
                    new_pos_k3.append(np.array(i))
                    new_atomic_numbers_k2.append(0)
                    new_atomic_numbers_k3.append(0)
                    i_new += 1
                else:
                    raise ValueError(
                        "Create method requires the argument 'positions', a "
                        "list of atom indices and/or positions.")
        # If positions are not supplied, it is assumed that each atom is used
        # as a center
        else:
            n_loc = n_atoms
            indices_k2 = np.arange(n_atoms)
            indices_k3 = np.arange(n_atoms)
            new_pos_k2 = system.get_positions()
            new_atomic_numbers_k2 = system.get_atomic_numbers()

        # Calculate the "raw" outputs for each term.
        mbtr = {}
        if self.k2 is not None:
            new_system_k2 = System(
                symbols=new_atomic_numbers_k2,
                positions=new_pos_k2,
            )
            mbtr["k2"] = self._get_k2(system, new_system_k2, indices_k2)

        if self.k3 is not None:
            new_system_k3 = System(
                symbols=new_atomic_numbers_k3,
                positions=new_pos_k3,
            )
            mbtr["k3"] = self._get_k3(system, new_system_k3, indices_k3)

        # Handle normalization
        if self.normalization == "l2_each":
            if self.flatten is True:
                for key, value in mbtr.items():
                    value_normalized = normalize(value, norm='l2', axis=1)
                    mbtr[key] = value_normalized
            else:
                for key, value in mbtr.items():
                    for array in value:
                        i_data = array.ravel()
                        i_norm = np.linalg.norm(i_data)
                        array /= i_norm

        # Flatten output if requested
        if self.flatten:
            length = 0

            datas = []
            rows = []
            cols = []
            for key in sorted(mbtr.keys()):
                tensor = mbtr[key]
                size = tensor.shape[1]
                coo = tensor.tocoo()
                datas.append(coo.data)
                rows.append(coo.row)
                cols.append(coo.col + length)
                length += size

            datas = np.concatenate(datas)
            rows = np.concatenate(rows)
            cols = np.concatenate(cols)
            result = coo_matrix((datas, (rows, cols)),
                                shape=[n_loc, length],
                                dtype=np.float32)

            # Make into a dense array if requested
            if not self.sparse:
                result = result.toarray()
        # Otherwise return a list of dictionaries, each dictionary containing
        # the requested unflattened tensors
        else:
            result = np.empty((n_loc), dtype='object')
            for i_loc in range(n_loc):
                i_dict = {}
                for key in mbtr.keys():
                    tensor = mbtr[key]
                    i_dict[key] = tensor[i_loc]
                result[i_loc] = i_dict

        return result
Exemplo n.º 7
0
    def create_extended_system(self, primitive_system, term_number):
        """Used to create a periodically extended system, that is as small as
        possible by rejecting atoms for which the given weighting will be below
        the given threshold.

        Modified for the local MBTR to only consider distances from the central
        atom and to enable taking the virtual sites into account.

        Args:
            primitive_system (System): The original primitive system to
                duplicate.
            term_number (int): The term number of the tensor. For k=2, the max
                distance is x, for k>2, the distance is given by 2*x.

        Returns:
            System: The new system that is extended so that each atom can at
            most have a weight that is larger or equivalent to the given
            threshold.
        """

        # We need to speciy that the relative positions should not be wrapped.
        # Otherwise the repeated systems may overlap with the positions taken
        # with get_positions()
        relative_pos = np.array(
            primitive_system.get_scaled_positions(wrap=False))
        numbers = np.array(primitive_system.numbers)
        cartesian_pos = np.array(primitive_system.get_positions())
        cell = np.array(primitive_system.get_cell())

        # Determine the upper limit of how many copies we need in each cell
        # vector direction. We take as many copies as needed for the
        # exponential weight to come down to the given threshold.
        cell_vector_lengths = np.linalg.norm(cell, axis=1)
        n_copies_axis = np.zeros(3, dtype=int)
        weight_info = self.weighting["k{}".format(term_number)]
        weighting_function = weight_info["function"]
        cutoff = self.weighting["k{}".format(term_number)]["cutoff"]

        if weighting_function == "exponential":
            scale = weight_info["scale"]
            function = lambda x: np.exp(-scale * x)

        for i_axis, axis_length in enumerate(cell_vector_lengths):
            limit_found = False
            n_copies = -1
            while (not limit_found):
                n_copies += 1
                distance = n_copies * cell_vector_lengths[0]

                # For terms k>2 we double the distances to take into
                # account the "loop" that is required.
                if term_number > 2:
                    distance = 2 * distance

                weight = function(distance)
                if weight < cutoff:
                    n_copies_axis[i_axis] = n_copies
                    limit_found = True

        # Create copies of the cell but keep track of the atoms in the
        # original cell
        num_extended = []
        pos_extended = []
        num_extended.append(numbers)
        pos_extended.append(cartesian_pos)
        a = np.array([1, 0, 0])
        b = np.array([0, 1, 0])
        c = np.array([0, 0, 1])
        for i in range(-n_copies_axis[0], n_copies_axis[0] + 1):
            for j in range(-n_copies_axis[1], n_copies_axis[1] + 1):
                for k in range(-n_copies_axis[2], n_copies_axis[2] + 1):
                    if i == 0 and j == 0 and k == 0:
                        continue

                    # Calculate the positions of the copied atoms and filter
                    # out the atoms that are farther away than the given
                    # cutoff.

                    # If the given position is virtual and does not correspond
                    # to a physical atom, the position is not repeated in the
                    # copies.
                    if self.virtual_positions and self._interaction_limit == 1:
                        num_copy = np.array(numbers)[1:]
                        pos_copy = np.array(relative_pos)[1:]

                    # If the given position is not virtual and corresponds to
                    # an actual physical atom, the ghost atom is repeated in
                    # the extended system.
                    else:
                        num_copy = np.array(numbers)
                        pos_copy = np.array(relative_pos)

                    pos_shifted = pos_copy - i * a - j * b - k * c
                    pos_copy_cartesian = np.dot(pos_shifted, cell)

                    # Only distances to the atoms within the interaction limit
                    # are considered.
                    positions_to_consider = cartesian_pos[0:self.
                                                          _interaction_limit]
                    distances = cdist(pos_copy_cartesian,
                                      positions_to_consider)

                    # For terms above k==2 we double the distances to take into
                    # account the "loop" that is required.
                    if term_number > 2:
                        distances *= 2

                    weights = function(distances)
                    weight_mask = weights >= cutoff

                    # Create a boolean mask that says if the atom is within the
                    # range from at least one atom in the original cell
                    valids_mask = np.any(weight_mask, axis=1)

                    valid_pos = pos_copy_cartesian[valids_mask]
                    valid_num = num_copy[valids_mask]

                    pos_extended.append(valid_pos)
                    num_extended.append(valid_num)

        pos_extended = np.concatenate(pos_extended)
        num_extended = np.concatenate(num_extended)

        extended_system = System(positions=pos_extended,
                                 numbers=num_extended,
                                 cell=cell,
                                 pbc=False)

        return extended_system
Exemplo n.º 8
0
    def create(self, system, positions, scaled_positions=False):
        """Return the local many-body tensor representation for the given
        system and positions.

        Args:
            system (:class:`ase.Atoms` | :class:`.System`): Input system.
            positions (iterable): Positions or atom index of points, from
                which local_mbtr is created. Can be a list of integer numbers
                or a list of xyz-coordinates.
            scaled_positions (boolean): Controls whether the given positions
                are given as scaled to the unit cell basis or not. Scaled
                positions require that a cell is available for the system.

        Returns:
            1D ndarray: The local many-body tensor representations of given
                positions, for k terms, as an array. These are ordered as given
                in positions.
        """
        # Transform the input system into the internal System-object
        system = self.get_system(system)

        # Ensure that the atomic number 0 is not present in the system
        if 0 in system.get_atomic_numbers():
            raise ValueError(
                "Please do not use the atomic number 0 in local MBTR "
                ", as it is reserved for the ghost atom used by the "
                "implementation.")

        # Ensuring self is updated
        self.update()

        # Checking scaled position
        if scaled_positions:
            if np.linalg.norm(system.get_cell()) == 0:
                raise ValueError(
                    "System doesn't have cell to justify scaled positions.")

        # Figure out the atom index or atom location from the given positions
        systems = []

        # If virtual positions requested, create new atoms with atomic number 0
        # at the requested position.
        for i_pos in positions:
            if self.virtual_positions:
                if not isinstance(i_pos, (list, tuple, np.ndarray)):
                    raise ValueError(
                        "The given position of type '{}' could not be "
                        "interpreted as a valid location. If you wish to use "
                        "existing atoms as centers, please set "
                        "'virtual_positions' to False.".format(type(i_pos)))
                if scaled_positions:
                    i_pos = np.dot(i_pos, system.get_cell())
                else:
                    i_pos = np.array(i_pos)

                i_pos = np.expand_dims(i_pos, axis=0)
                new_system = System('X', positions=i_pos)
                new_system += system
            else:
                if not np.issubdtype(type(i_pos), np.integer):
                    raise ValueError(
                        "The given position of type '{}' could not be "
                        "interpreted as a valid index. If you wish to use "
                        "custom locations as centers, please set "
                        "'virtual_positions' to True.".format(type(i_pos)))
                new_system = Atoms()
                center_atom = system[i_pos]
                new_system += center_atom
                new_system.set_atomic_numbers([0])
                system_copy = system.copy()
                del system_copy[i_pos]
                new_system += system_copy

            # Set the periodicity and cell to match the original system, as
            # they are lost in the system concatenation
            new_system.set_cell(system.get_cell())
            new_system.set_pbc(system.get_pbc())

            systems.append(new_system)

        # Request MBTR for each position. Return type depends on flattening and
        # whether a spares of dense result is requested.
        n_pos = len(positions)
        n_features = self.get_number_of_features()
        if self._flatten and self._sparse:
            data = []
            cols = []
            rows = []
            row_offset = 0
            for i, i_system in enumerate(systems):
                i_res = super().create(i_system)
                data.append(i_res.data)
                rows.append(i_res.row + row_offset)
                cols.append(i_res.col)

                # Increase the row offset
                row_offset += 1

            # Saves the descriptors as a sparse matrix
            data = np.concatenate(data)
            rows = np.concatenate(rows)
            cols = np.concatenate(cols)
            desc = coo_matrix((data, (rows, cols)),
                              shape=(n_pos, n_features),
                              dtype=np.float32)
        else:
            if self._flatten and not self._sparse:
                desc = np.empty((n_pos, n_features), dtype=np.float32)
            else:
                desc = np.empty((n_pos), dtype='object')
            for i, i_system in enumerate(systems):
                i_desc = super().create(i_system)
                desc[i] = i_desc

        return desc
Exemplo n.º 9
0
from dscribe.core import System

import matplotlib.pyplot as mpl

import ase.data

# Define the system under study: NaCl in a conventional cell.
NaCl_conv = System(
    cell=[
        [5.6402, 0.0, 0.0],
        [0.0, 5.6402, 0.0],
        [0.0, 0.0, 5.6402]
    ],
    scaled_positions=[
        [0.0, 0.5, 0.0],
        [0.0, 0.5, 0.5],
        [0.0, 0.0, 0.5],
        [0.0, 0.0, 0.0],
        [0.5, 0.5, 0.5],
        [0.5, 0.5, 0.0],
        [0.5, 0.0, 0.0],
        [0.5, 0.0, 0.5]
    ],
    symbols=["Na", "Cl", "Na", "Cl", "Na", "Cl", "Na", "Cl"],
)
# view(NaCl_conv)

# Create a local MBTR desciptor around the atomic index 6 corresponding to a Na
# atom
decay_factor = 0.5
mbtr = LMBTR(
    species=[11, 17],
Exemplo n.º 10
0
    def create_single(
        self,
        system,
        positions,
    ):
        """Return the local many-body tensor representation for the given
        system and positions.

        Args:
            system (:class:`ase.Atoms` | :class:`.System`): Input system.
            positions (iterable): Positions or atom index of points, from
                which local_mbtr is created. Can be a list of integer numbers
                or a list of xyz-coordinates.

        Returns:
            1D ndarray: The local many-body tensor representations of given
            positions, for k terms, as an array. These are ordered as given in
            positions.
        """
        # Transform the input system into the internal System-object
        system = self.get_system(system)

        # Check that the system does not have elements that are not in the list
        # of atomic numbers
        atomic_number_set = set(system.get_atomic_numbers())
        self.check_atomic_numbers(atomic_number_set)

        # Ensure that the atomic number 0 is not present in the system
        if 0 in atomic_number_set:
            raise ValueError(
                "Please do not use the atomic number 0 in local MBTR"
                ", as it is reserved for the ghost atom used by the "
                "implementation.")

        # Figure out the atom index or atom location from the given positions
        systems = []

        # Positions specified, use them
        if positions is not None:

            # Check validity of position definitions and create final cartesian
            # position list
            list_positions = []
            if len(positions) == 0:
                raise ValueError(
                    "The argument 'positions' should contain a non-empty set of"
                    " atomic indices or cartesian coordinates with x, y and z "
                    "components.")
            for i in positions:
                if np.issubdtype(type(i), np.integer):
                    i_len = len(system)
                    if i >= i_len or i < 0:
                        raise ValueError(
                            "The provided index {} is not valid for the system "
                            "with {} atoms.".format(i, i_len))
                    list_positions.append(system.get_positions()[i])
                elif isinstance(i, (list, tuple, np.ndarray)):
                    if len(i) != 3:
                        raise ValueError(
                            "The argument 'positions' should contain a "
                            "non-empty set of atomic indices or cartesian "
                            "coordinates with x, y and z components.")
                    list_positions.append(i)
                else:
                    raise ValueError(
                        "Create method requires the argument 'positions', a "
                        "list of atom indices and/or positions.")

        for i_pos in positions:
            # Position designated as cartesian position, add a new atom at that
            # location with the chemical element X and place is as the first
            # atom in the system. The interaction limit makes sure that only
            # interactions of this first atom to every other atom are
            # considered.
            if isinstance(i_pos, (list, tuple, np.ndarray)):
                if len(i_pos) != 3:
                    raise ValueError(
                        "The argument 'positions' should contain a "
                        "non-empty set of atomic indices or cartesian "
                        "coordinates with x, y and z components.")
                i_pos = np.array(i_pos)
                i_pos = np.expand_dims(i_pos, axis=0)
                new_system = System('X', positions=i_pos)
                new_system += system
            # Position designated as integer, use the atom at that index as
            # center. For the calculation this central atoms is shifted to be
            # the first atom in the system, and the interaction limit makes
            # sure that only interactions of this first atom to every other
            # atom are considered.
            elif np.issubdtype(type(i_pos), np.integer):
                new_system = Atoms()
                center_atom = system[i_pos]
                new_system += center_atom
                new_system.set_atomic_numbers([0])
                system_copy = system.copy()
                del system_copy[i_pos]
                new_system += system_copy
            else:
                raise ValueError(
                    "Create method requires the argument 'positions', a "
                    "list of atom indices and/or positions.")

            # Set the periodicity and cell to match the original system, as
            # they are lost in the system concatenation
            new_system.set_cell(system.get_cell())
            new_system.set_pbc(system.get_pbc())

            systems.append(new_system)

        # Request MBTR for each position. Return type depends on flattening and
        # whether a spares of dense result is requested.
        n_pos = len(positions)
        n_features = self.get_number_of_features()
        if self._flatten and self._sparse:
            data = []
            cols = []
            rows = []
            row_offset = 0
            for i, i_system in enumerate(systems):
                i_res = super().create_single(i_system)
                data.append(i_res.data)
                rows.append(i_res.row + row_offset)
                cols.append(i_res.col)

                # Increase the row offset
                row_offset += 1

            # Saves the descriptors as a sparse matrix
            data = np.concatenate(data)
            rows = np.concatenate(rows)
            cols = np.concatenate(cols)
            desc = coo_matrix((data, (rows, cols)),
                              shape=(n_pos, n_features),
                              dtype=np.float32)
        else:
            if self._flatten and not self._sparse:
                desc = np.empty((n_pos, n_features), dtype=np.float32)
            else:
                desc = np.empty((n_pos), dtype='object')
            for i, i_system in enumerate(systems):
                i_desc = super().create_single(i_system)
                desc[i] = i_desc

        return desc