Regions
*******

Regions are 'groups' of atoms within a ChemicalSystem. Regions are
used for some of the AMS driver and engine features, but they can also
be useful tools for bookkeeping and manipulations (see also the AMS
documentation on Regions).

Here is a simple example showing how to create and use regions:

   from scm.base import ChemicalSystem
   import numpy as np

   # Create a ChemicalSystem and define two regions (one for each water molecule)
   mol = ChemicalSystem(
   """
   System
        Atoms
             O -2.676678  0.759085  0.370636 region=water_1
             H -3.468900  0.415690  0.814339 region=water_1
             H -3.005004  1.433129 -0.246320 region=water_1
             O  0.039085  0.303872  1.265871 region=water_2
             H -0.874303  0.483757  0.975166 region=water_2
             H  0.293563 -0.534617  0.849654 region=water_2
        End
   End
   """
   )

   # Translate all the atoms in the region "water_2"
   for i_atom in mol.get_atoms_in_region("water_2"):
        mol.atoms[i_atom].coords += np.array([1, 0, 0])

Region names are case-sensitive and may not include certain
characters. Whether a string is a valid name for a region can be
checked with the "is_valid_region_name" method:

classmethod ChemicalSystem.is_valid_region_name(name: str) -> bool

   Checks if a string is a valid region name.

   Valid region names do not contain any of the following characters:

      , + - * / \ | $ & ^ < > ( ) [ ] { } " '

   Valid region names do not have leading or trailing whitespace. Note
   that region names are case-sensitive, so "A" and "a" correspond to
   different regions.

There are various methods for querying region information:

property ChemicalSystem.num_regions: int

   Returns the number of regions used in the system.

   Note that there is no such thing as an empty region. Every region
   contains at least one atom.

property ChemicalSystem.region_names: List[str]

   Returns an ASCIIbetically ordered list of the names of all regions
   in the system.

   Note that there is no such thing as an empty region. Every region
   contains at least one atom.

ChemicalSystem.has_region(region: str) -> bool

   Checks if a region exists in the system, given its region name.

   Note that region should be a valid region name, not a region
   expression. If you want to check if a region expression contains
   any atoms, use "num_atoms_in_region(...) > 0" instead.

ChemicalSystem.do_regions_intersect(regionA: str, regionB: str) -> bool

   Checks if two regions or region expressions intersect, i.e. have at
   least one atom in common.

ChemicalSystem.num_atoms_in_region(region: str) -> int

   Returns the number of atoms in a region or region expression.

   Returns zero if the region does not exist at all, or if the region
   expression evaluates to an empty set.

ChemicalSystem.num_atoms_outside_region(region: str) -> int

   Returns the number of atoms outside of a region or region
   expression.

   Returns the total number of atoms in the system if the region does
   not exist at all, or if the region expression evaluates to an empty
   set.

ChemicalSystem.get_atoms_in_region(region: str) -> ndarray[Any, dtype[int64]]

   Returns a sorted array of atom indices of all atoms in a region or
   region expression.

ChemicalSystem.get_atoms_outside_region(region: str) -> ndarray[Any, dtype[int64]]

   Returns a sorted array of atom indices of all atoms outside of a
   region or region expression.

ChemicalSystem.is_atom_in_region(atom: int | Atom, region: str) -> bool

   Checks if an atom is in a region or region expression.

ChemicalSystem.is_atom_outside_region(atom: int | Atom, region: str) -> bool

   Checks if an atom is outside of a region or region expression.

ChemicalSystem.get_regions_of_atom(atom: int | Atom) -> List[str]

   Returns an ASCIIbetically sorted list of the names of all regions
   an atom is part of.

Almost all methods for querying region information also support so-
called region expressions in addition to plain region names. Region
expressions may include region names, the parentheses "()", as well as
the operators "+" for union, "-" for set difference, and "&" for set
intersection. Additionally the wildcard "*" means all atoms, while "$"
stands for the set of selected atoms.

   # number of atoms that are either in region A or B (or both)
   num_at_AB = mol.num_atoms_in_region("A + B")

   # loop over all atoms that are not in the intersection of region A and B
   for atidx in mol.get_atoms_in_region("* - (A & B)"):
        ...

The following method can be used to extract the used regions from a
region expression:

classmethod ChemicalSystem.get_operands_in_region_expression(region_expression: str) -> List[str]

   Returns an ASCIIbetically ordered list of all unique operands used
   in a region expression.

   Operands are either names of regions, or the special operands "*"
   representing all atoms, or "$" representing the set of selected
   atoms.

The following methods can be used to change the assignment of atoms to
regions:

ChemicalSystem.set_atoms_in_region(atom_indices: ArrayLike, region: str) -> None

   Creates or sets an entire region given the indices of the atoms to
   be part of the region.

   If atom_indices is empty and the region previously existed, it will
   effectively be deleted.

ChemicalSystem.add_atoms_to_region(atom_indices: ArrayLike, region: str) -> None

   Adds multiple atoms to a region, given their atom indices.

ChemicalSystem.add_all_atoms_to_region(region: str) -> None

   Adds all of the system's atoms to a region.

ChemicalSystem.remove_atoms_from_region(atom_indices: ArrayLike, region: str) -> None

   Removes multiple atoms from a region, given their atom indices.

   No error is raised if any of the atoms is not part of the region.

ChemicalSystem.remove_region(region: str) -> None

   Removes a region. Atoms in that region will keep existing, but will
   not longer be part of that region.

ChemicalSystem.remove_all_regions() -> None

   All atoms will keep existing, but will not longer be part of any
   region.

ChemicalSystem.add_atom_to_region(atom: int | Atom, region: str) -> None

   Adds an atom to a region.

ChemicalSystem.remove_atom_from_region(atom: int | Atom, region: str) -> None

   Removes an atom from a region.

   Throws a ChemicalSystemError if the atom was not part of the region
   it should be removed from.

ChemicalSystem.remove_atom_from_all_regions(atom: int | Atom) -> None

   Removes an atom from all regions.
