From 313b8ecb0f93d20e9d388e90fd65c3e83f1e3b65 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 23 Nov 2024 19:21:09 +0000 Subject: [PATCH] Deployed d848e9e to 0.4 with MkDocs 1.6.1 and mike 2.1.3 --- 0.4/api/atomcell/index.html | 351 ++++++++++++++++++--- 0.4/api/atoms/index.html | 179 ++++++++++- 0.4/api/bbox/index.html | 12 +- 0.4/api/cell/index.html | 65 +++- 0.4/api/defect/index.html | 125 ++++++-- 0.4/api/elem/index.html | 585 +++++++++++++++++++++++------------ 0.4/api/expr/index.html | 159 ++++++++-- 0.4/api/index.html | 461 +++++++++++++++++++++++---- 0.4/api/io/cfg/index.html | 15 + 0.4/api/io/cif/index.html | 32 +- 0.4/api/io/index.html | 58 +++- 0.4/api/io/lmp/index.html | 20 ++ 0.4/api/io/mp_api/index.html | 5 + 0.4/api/io/mslice/index.html | 5 + 0.4/api/io/qe/index.html | 17 +- 0.4/api/io/xsf/index.html | 15 + 0.4/api/io/xyz/index.html | 17 +- 0.4/api/make/index.html | 206 +++++++++--- 0.4/api/mixins/index.html | 47 ++- 0.4/api/testing/index.html | 7 +- 0.4/api/transform/index.html | 102 ++++++ 0.4/api/types/index.html | 13 +- 0.4/api/util/index.html | 15 + 0.4/api/vec/index.html | 251 +++++++++------ 0.4/api/visualize/index.html | 15 + 0.4/assets/_mkdocstrings.css | 26 +- 0.4/index.html | 36 +-- 0.4/objects.inv | Bin 8467 -> 8460 bytes 0.4/search/search_index.json | 2 +- 0.4/sitemap.xml.gz | Bin 127 -> 127 bytes 30 files changed, 2298 insertions(+), 543 deletions(-) diff --git a/0.4/api/atomcell/index.html b/0.4/api/atomcell/index.html index 7472984..627540d 100644 --- a/0.4/api/atomcell/index.html +++ b/0.4/api/atomcell/index.html @@ -7290,6 +7290,11 @@

atomlib.atomcell

+ + + + +
@@ -7315,6 +7320,11 @@

Bases: HasAtoms, HasCell, ABC

+ + + + +
Source code in atomlib/atomcell.py
- +
 55
@@ -9170,6 +9180,18 @@ 

+
+
change_transform(
+    transform: AffineTransform3D,
+    frame_to: Optional[CoordinateFrame] = None,
+    frame_from: Optional[CoordinateFrame] = None,
+) -> AffineTransform3D
+
change_transform(
+    transform: Transform3D,
+    frame_to: Optional[CoordinateFrame] = None,
+    frame_from: Optional[CoordinateFrame] = None,
+) -> Transform3D
+
change_transform(
     transform: Transform3D,
     frame_to: Optional[CoordinateFrame] = None,
@@ -9489,6 +9511,22 @@ 

+
+
partition_by(
+    by: Union[str, Sequence[str]],
+    *more_by: str,
+    maintain_order: bool = True,
+    include_key: bool = True,
+    as_dict: Literal[False] = False
+) -> List[Self]
+
partition_by(
+    by: Union[str, Sequence[str]],
+    *more_by: str,
+    maintain_order: bool = True,
+    include_key: bool = True,
+    as_dict: Literal[True] = ...
+) -> Dict[Any, Self]
+
partition_by(
     by: Union[str, Sequence[str]],
     *more_by: str,
@@ -9946,6 +9984,25 @@ 

+
+
pos(
+    x: Sequence[Optional[float]],
+    /,
+    *,
+    y: None = None,
+    z: None = None,
+    tol: float = 1e-06,
+    **kwargs: Any,
+) -> Expr
+
pos(
+    x: Optional[float] = None,
+    y: Optional[float] = None,
+    z: Optional[float] = None,
+    *,
+    tol: float = 1e-06,
+    **kwargs: Any
+) -> Expr
+
pos(
     x: Union[Sequence[Optional[float]], float, None] = None,
     y: Optional[float] = None,
@@ -10465,12 +10522,12 @@ 

crop(
-    x_min: float = -numpy.inf,
-    x_max: float = numpy.inf,
-    y_min: float = -numpy.inf,
-    y_max: float = numpy.inf,
-    z_min: float = -numpy.inf,
-    z_max: float = numpy.inf,
+    x_min: float = -inf,
+    x_max: float = inf,
+    y_min: float = -inf,
+    y_max: float = inf,
+    z_min: float = -inf,
+    z_max: float = inf,
     *,
     frame: CoordinateFrame = "local"
 ) -> Self
@@ -10529,12 +10586,12 @@ 

crop_atoms(
-    x_min: float = -numpy.inf,
-    x_max: float = numpy.inf,
-    y_min: float = -numpy.inf,
-    y_max: float = numpy.inf,
-    z_min: float = -numpy.inf,
-    z_max: float = numpy.inf,
+    x_min: float = -inf,
+    x_max: float = inf,
+    y_min: float = -inf,
+    y_max: float = inf,
+    z_min: float = -inf,
+    z_max: float = inf,
     *,
     frame: CoordinateFrame = "local"
 ) -> Self
@@ -11138,7 +11195,9 @@ 

percentiles + percentiles +

List of percentiles/quantiles to include. Defaults to 25% (first quartile), @@ -12025,6 +12084,28 @@

+
+
add_atom(
+    elem: Union[int, str],
+    x: ArrayLike,
+    /,
+    *,
+    y: None = None,
+    z: None = None,
+    frame: Optional[CoordinateFrame] = None,
+    **kwargs: Any,
+) -> Self
+
add_atom(
+    elem: Union[int, str],
+    /,
+    x: float,
+    y: float,
+    z: float,
+    *,
+    frame: Optional[CoordinateFrame] = None,
+    **kwargs: Any,
+) -> Self
+
add_atom(
     elem: Union[int, str],
     /,
@@ -12509,6 +12590,11 @@ 

Cell of atoms with known size and periodic boundary conditions.

+ + + + +
Source code in atomlib/atomcell.py
- + - + - +
612
@@ -13720,12 +13806,12 @@ 

crop(
-    x_min: float = -numpy.inf,
-    x_max: float = numpy.inf,
-    y_min: float = -numpy.inf,
-    y_max: float = numpy.inf,
-    z_min: float = -numpy.inf,
-    z_max: float = numpy.inf,
+    x_min: float = -inf,
+    x_max: float = inf,
+    y_min: float = -inf,
+    y_max: float = inf,
+    z_min: float = -inf,
+    z_max: float = inf,
     *,
     frame: CoordinateFrame = "local"
 ) -> Self
@@ -13783,6 +13869,18 @@ 

+
+
change_transform(
+    transform: AffineTransform3D,
+    frame_to: Optional[CoordinateFrame] = None,
+    frame_from: Optional[CoordinateFrame] = None,
+) -> AffineTransform3D
+
change_transform(
+    transform: Transform3D,
+    frame_to: Optional[CoordinateFrame] = None,
+    frame_from: Optional[CoordinateFrame] = None,
+) -> Transform3D
+
change_transform(
     transform: Transform3D,
     frame_to: Optional[CoordinateFrame] = None,
@@ -13851,7 +13949,9 @@ 

percentiles + percentiles +

List of percentiles/quantiles to include. Defaults to 25% (first quartile), @@ -14625,6 +14725,22 @@

+
+
partition_by(
+    by: Union[str, Sequence[str]],
+    *more_by: str,
+    maintain_order: bool = True,
+    include_key: bool = True,
+    as_dict: Literal[False] = False
+) -> List[Self]
+
partition_by(
+    by: Union[str, Sequence[str]],
+    *more_by: str,
+    maintain_order: bool = True,
+    include_key: bool = True,
+    as_dict: Literal[True] = ...
+) -> Dict[Any, Self]
+
partition_by(
     by: Union[str, Sequence[str]],
     *more_by: str,
@@ -15120,12 +15236,12 @@ 

crop_atoms(
-    x_min: float = -numpy.inf,
-    x_max: float = numpy.inf,
-    y_min: float = -numpy.inf,
-    y_max: float = numpy.inf,
-    z_min: float = -numpy.inf,
-    z_max: float = numpy.inf,
+    x_min: float = -inf,
+    x_max: float = inf,
+    y_min: float = -inf,
+    y_max: float = inf,
+    z_min: float = -inf,
+    z_max: float = inf,
     *,
     frame: CoordinateFrame = "local"
 ) -> Self
@@ -15567,6 +15683,28 @@ 

+
+
add_atom(
+    elem: Union[int, str],
+    x: ArrayLike,
+    /,
+    *,
+    y: None = None,
+    z: None = None,
+    frame: Optional[CoordinateFrame] = None,
+    **kwargs: Any,
+) -> Self
+
add_atom(
+    elem: Union[int, str],
+    /,
+    x: float,
+    y: float,
+    z: float,
+    *,
+    frame: Optional[CoordinateFrame] = None,
+    **kwargs: Any,
+) -> Self
+
add_atom(
     elem: Union[int, str],
     /,
@@ -15630,6 +15768,25 @@ 

+
+
pos(
+    x: Sequence[Optional[float]],
+    /,
+    *,
+    y: None = None,
+    z: None = None,
+    tol: float = 1e-06,
+    **kwargs: Any,
+) -> Expr
+
pos(
+    x: Optional[float] = None,
+    y: Optional[float] = None,
+    z: Optional[float] = None,
+    *,
+    tol: float = 1e-06,
+    **kwargs: Any
+) -> Expr
+
pos(
     x: Union[Sequence[Optional[float]], float, None] = None,
     y: Optional[float] = None,
@@ -16677,6 +16834,12 @@ 

+
+
read(path: FileOrPath, ty: FileType) -> HasAtomsT
+
read(
+    path: Union[str, Path, TextIO], ty: Literal[None] = None
+) -> HasAtomsT
+
read(
     path: FileOrPath, ty: Optional[FileType] = None
 ) -> HasAtomsT
@@ -17048,6 +17211,12 @@ 

+
+
write(path: FileOrPath, ty: FileType)
+
write(
+    path: Union[str, Path, TextIO], ty: Literal[None] = None
+)
+
write(path: FileOrPath, ty: Optional[FileType] = None)
 
@@ -17203,7 +17372,9 @@

f + f +

File or path to write to

@@ -17217,7 +17388,9 @@

pseudo + pseudo +

Mapping from atom symbol

@@ -17633,6 +17806,11 @@

Bases: AtomCell

+ + + + +
Source code in atomlib/atomcell.py
- + - + - +
747
@@ -18614,12 +18792,12 @@ 

crop(
-    x_min: float = -numpy.inf,
-    x_max: float = numpy.inf,
-    y_min: float = -numpy.inf,
-    y_max: float = numpy.inf,
-    z_min: float = -numpy.inf,
-    z_max: float = numpy.inf,
+    x_min: float = -inf,
+    x_max: float = inf,
+    y_min: float = -inf,
+    y_max: float = inf,
+    z_min: float = -inf,
+    z_max: float = inf,
     *,
     frame: CoordinateFrame = "local"
 ) -> Self
@@ -18677,6 +18855,18 @@ 

+
+
change_transform(
+    transform: AffineTransform3D,
+    frame_to: Optional[CoordinateFrame] = None,
+    frame_from: Optional[CoordinateFrame] = None,
+) -> AffineTransform3D
+
change_transform(
+    transform: Transform3D,
+    frame_to: Optional[CoordinateFrame] = None,
+    frame_from: Optional[CoordinateFrame] = None,
+) -> Transform3D
+
change_transform(
     transform: Transform3D,
     frame_to: Optional[CoordinateFrame] = None,
@@ -18839,7 +19029,9 @@ 

percentiles + percentiles +

List of percentiles/quantiles to include. Defaults to 25% (first quartile), @@ -19641,6 +19833,22 @@

+
+
partition_by(
+    by: Union[str, Sequence[str]],
+    *more_by: str,
+    maintain_order: bool = True,
+    include_key: bool = True,
+    as_dict: Literal[False] = False
+) -> List[Self]
+
partition_by(
+    by: Union[str, Sequence[str]],
+    *more_by: str,
+    maintain_order: bool = True,
+    include_key: bool = True,
+    as_dict: Literal[True] = ...
+) -> Dict[Any, Self]
+
partition_by(
     by: Union[str, Sequence[str]],
     *more_by: str,
@@ -20136,12 +20344,12 @@ 

crop_atoms(
-    x_min: float = -numpy.inf,
-    x_max: float = numpy.inf,
-    y_min: float = -numpy.inf,
-    y_max: float = numpy.inf,
-    z_min: float = -numpy.inf,
-    z_max: float = numpy.inf,
+    x_min: float = -inf,
+    x_max: float = inf,
+    y_min: float = -inf,
+    y_max: float = inf,
+    z_min: float = -inf,
+    z_max: float = inf,
     *,
     frame: CoordinateFrame = "local"
 ) -> Self
@@ -20583,6 +20791,28 @@ 

+
+
add_atom(
+    elem: Union[int, str],
+    x: ArrayLike,
+    /,
+    *,
+    y: None = None,
+    z: None = None,
+    frame: Optional[CoordinateFrame] = None,
+    **kwargs: Any,
+) -> Self
+
add_atom(
+    elem: Union[int, str],
+    /,
+    x: float,
+    y: float,
+    z: float,
+    *,
+    frame: Optional[CoordinateFrame] = None,
+    **kwargs: Any,
+) -> Self
+
add_atom(
     elem: Union[int, str],
     /,
@@ -20646,6 +20876,25 @@ 

+
+
pos(
+    x: Sequence[Optional[float]],
+    /,
+    *,
+    y: None = None,
+    z: None = None,
+    tol: float = 1e-06,
+    **kwargs: Any,
+) -> Expr
+
pos(
+    x: Optional[float] = None,
+    y: Optional[float] = None,
+    z: Optional[float] = None,
+    *,
+    tol: float = 1e-06,
+    **kwargs: Any
+) -> Expr
+
pos(
     x: Union[Sequence[Optional[float]], float, None] = None,
     y: Optional[float] = None,
@@ -21721,6 +21970,12 @@ 

+
+
read(path: FileOrPath, ty: FileType) -> HasAtomsT
+
read(
+    path: Union[str, Path, TextIO], ty: Literal[None] = None
+) -> HasAtomsT
+
read(
     path: FileOrPath, ty: Optional[FileType] = None
 ) -> HasAtomsT
@@ -22092,6 +22347,12 @@ 

+
+
write(path: FileOrPath, ty: FileType)
+
write(
+    path: Union[str, Path, TextIO], ty: Literal[None] = None
+)
+
write(path: FileOrPath, ty: Optional[FileType] = None)
 
@@ -22247,7 +22508,9 @@

f + f +

File or path to write to

@@ -22261,7 +22524,9 @@

pseudo + pseudo +

Mapping from atom symbol

diff --git a/0.4/api/atoms/index.html b/0.4/api/atoms/index.html index 0b04cfd..54b3172 100644 --- a/0.4/api/atoms/index.html +++ b/0.4/api/atoms/index.html @@ -3877,6 +3877,11 @@

atomlib.atoms

+ + + + +
@@ -4142,6 +4147,11 @@

Abstract class representing any (possibly compound) collection of atoms.

+ + + + +
Source code in atomlib/atoms.py
- + - + - + - +
179
@@ -6012,7 +6022,9 @@ 

frame + frame +

Coordinate frame to return atoms in. For a plain HasAtoms, @@ -6102,7 +6114,9 @@

atoms + atoms +

HasAtoms to replace these with.

@@ -6116,7 +6130,9 @@

frame + frame +

Coordinate frame inside atoms are in. For a plain HasAtoms, @@ -6210,7 +6226,9 @@

percentiles + percentiles +

List of percentiles/quantiles to include. Defaults to 25% (first quartile), @@ -7002,6 +7020,22 @@

+
+
partition_by(
+    by: Union[str, Sequence[str]],
+    *more_by: str,
+    maintain_order: bool = True,
+    include_key: bool = True,
+    as_dict: Literal[False] = False
+) -> List[Self]
+
partition_by(
+    by: Union[str, Sequence[str]],
+    *more_by: str,
+    maintain_order: bool = True,
+    include_key: bool = True,
+    as_dict: Literal[True] = ...
+) -> Dict[Any, Self]
+
partition_by(
     by: Union[str, Sequence[str]],
     *more_by: str,
@@ -7493,12 +7527,12 @@ 

crop(
-    x_min: float = -numpy.inf,
-    x_max: float = numpy.inf,
-    y_min: float = -numpy.inf,
-    y_max: float = numpy.inf,
-    z_min: float = -numpy.inf,
-    z_max: float = numpy.inf,
+    x_min: float = -inf,
+    x_max: float = inf,
+    y_min: float = -inf,
+    y_max: float = inf,
+    z_min: float = -inf,
+    z_max: float = inf,
 ) -> Self
 
@@ -7942,6 +7976,25 @@

+
+
add_atom(
+    elem: Union[int, str],
+    x: ArrayLike,
+    /,
+    *,
+    y: None = None,
+    z: None = None,
+    **kwargs: Any,
+) -> Self
+
add_atom(
+    elem: Union[int, str],
+    /,
+    x: float,
+    y: float,
+    z: float,
+    **kwargs: Any,
+) -> Self
+
add_atom(
     elem: Union[int, str],
     /,
@@ -8027,6 +8080,25 @@ 

+
+
pos(
+    x: Sequence[Optional[float]],
+    /,
+    *,
+    y: None = None,
+    z: None = None,
+    tol: float = 1e-06,
+    **kwargs: Any,
+) -> Expr
+
pos(
+    x: Optional[float] = None,
+    y: Optional[float] = None,
+    z: Optional[float] = None,
+    *,
+    tol: float = 1e-06,
+    **kwargs: Any
+) -> Expr
+
pos(
     x: Union[Sequence[Optional[float]], float, None] = None,
     y: Optional[float] = None,
@@ -8708,6 +8780,11 @@ 

  • type: Numeric atom type, as used by programs like LAMMPS
  • + + + + +
    Source code in atomlib/atoms.py
    - +
     968
    @@ -9243,7 +9320,9 @@ 

    percentiles + percentiles +

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), @@ -10035,6 +10114,22 @@

    +
    +
    partition_by(
    +    by: Union[str, Sequence[str]],
    +    *more_by: str,
    +    maintain_order: bool = True,
    +    include_key: bool = True,
    +    as_dict: Literal[False] = False
    +) -> List[Self]
    +
    partition_by(
    +    by: Union[str, Sequence[str]],
    +    *more_by: str,
    +    maintain_order: bool = True,
    +    include_key: bool = True,
    +    as_dict: Literal[True] = ...
    +) -> Dict[Any, Self]
    +
    partition_by(
         by: Union[str, Sequence[str]],
         *more_by: str,
    @@ -10526,12 +10621,12 @@ 

    crop(
    -    x_min: float = -numpy.inf,
    -    x_max: float = numpy.inf,
    -    y_min: float = -numpy.inf,
    -    y_max: float = numpy.inf,
    -    z_min: float = -numpy.inf,
    -    z_max: float = numpy.inf,
    +    x_min: float = -inf,
    +    x_max: float = inf,
    +    y_min: float = -inf,
    +    y_max: float = inf,
    +    z_min: float = -inf,
    +    z_max: float = inf,
     ) -> Self
     
    @@ -10975,6 +11070,25 @@

    +
    +
    add_atom(
    +    elem: Union[int, str],
    +    x: ArrayLike,
    +    /,
    +    *,
    +    y: None = None,
    +    z: None = None,
    +    **kwargs: Any,
    +) -> Self
    +
    add_atom(
    +    elem: Union[int, str],
    +    /,
    +    x: float,
    +    y: float,
    +    z: float,
    +    **kwargs: Any,
    +) -> Self
    +
    add_atom(
         elem: Union[int, str],
         /,
    @@ -11060,6 +11174,25 @@ 

    +
    +
    pos(
    +    x: Sequence[Optional[float]],
    +    /,
    +    *,
    +    y: None = None,
    +    z: None = None,
    +    tol: float = 1e-06,
    +    **kwargs: Any,
    +) -> Expr
    +
    pos(
    +    x: Optional[float] = None,
    +    y: Optional[float] = None,
    +    z: Optional[float] = None,
    +    *,
    +    tol: float = 1e-06,
    +    **kwargs: Any
    +) -> Expr
    +
    pos(
         x: Union[Sequence[Optional[float]], float, None] = None,
         y: Optional[float] = None,
    @@ -11711,6 +11844,12 @@ 

    +
    +
    read(path: FileOrPath, ty: FileType) -> HasAtomsT
    +
    read(
    +    path: Union[str, Path, TextIO], ty: Literal[None] = None
    +) -> HasAtomsT
    +
    read(
         path: FileOrPath, ty: Optional[FileType] = None
     ) -> HasAtomsT
    @@ -12082,6 +12221,12 @@ 

    +
    +
    write(path: FileOrPath, ty: FileType)
    +
    write(
    +    path: Union[str, Path, TextIO], ty: Literal[None] = None
    +)
    +
    write(path: FileOrPath, ty: Optional[FileType] = None)
     
    diff --git a/0.4/api/bbox/index.html b/0.4/api/bbox/index.html index 91877e7..7f53c03 100644 --- a/0.4/api/bbox/index.html +++ b/0.4/api/bbox/index.html @@ -1468,6 +1468,11 @@

    atomlib.bbox

    + + + + +
    @@ -1493,6 +1498,11 @@

    3D axis-aligned bounding box, with corners min and max.

    + + + + +
    Source code in atomlib/bbox.py
     18
    @@ -1734,7 +1744,7 @@ 

    -
    inner: ndarray = stack((min, max), axis=-1)
    +
    inner: ndarray = stack((min, max), axis=-1)
     
    diff --git a/0.4/api/cell/index.html b/0.4/api/cell/index.html index 949a386..afe44d5 100644 --- a/0.4/api/cell/index.html +++ b/0.4/api/cell/index.html @@ -2329,6 +2329,11 @@

    atomlib.cell

    + + + + +
    @@ -2396,7 +2401,7 @@

    -
    HasCellT = TypeVar('HasCellT', bound='HasCell')
    +
    HasCellT = TypeVar('HasCellT', bound='HasCell')
     
    @@ -2419,6 +2424,11 @@

    + + + + +
    Source code in atomlib/cell.py
     53
    @@ -3762,12 +3772,12 @@ 

    crop(
    -    x_min: float = -numpy.inf,
    -    x_max: float = numpy.inf,
    -    y_min: float = -numpy.inf,
    -    y_max: float = numpy.inf,
    -    z_min: float = -numpy.inf,
    -    z_max: float = numpy.inf,
    +    x_min: float = -inf,
    +    x_max: float = inf,
    +    y_min: float = -inf,
    +    y_max: float = inf,
    +    z_min: float = -inf,
    +    z_max: float = inf,
         *,
         frame: CoordinateFrame = "local"
     ) -> HasCellT
    @@ -3850,6 +3860,18 @@ 

    +
    +
    change_transform(
    +    transform: AffineTransform3D,
    +    frame_to: Optional[CoordinateFrame] = None,
    +    frame_from: Optional[CoordinateFrame] = None,
    +) -> AffineTransform3D
    +
    change_transform(
    +    transform: Transform3D,
    +    frame_to: Optional[CoordinateFrame] = None,
    +    frame_from: Optional[CoordinateFrame] = None,
    +) -> Transform3D
    +
    change_transform(
         transform: Transform3D,
         frame_to: Optional[CoordinateFrame] = None,
    @@ -3955,6 +3977,11 @@ 

    an orthogonal coordinate system. Finally, affine contains any remaining transformations to the local coordinate system, which atoms are stored in.

    + + + + +
    Source code in atomlib/cell.py
    - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - +
    342
    @@ -4868,12 +4895,12 @@ 

    crop(
    -    x_min: float = -numpy.inf,
    -    x_max: float = numpy.inf,
    -    y_min: float = -numpy.inf,
    -    y_max: float = numpy.inf,
    -    z_min: float = -numpy.inf,
    -    z_max: float = numpy.inf,
    +    x_min: float = -inf,
    +    x_max: float = inf,
    +    y_min: float = -inf,
    +    y_max: float = inf,
    +    z_min: float = -inf,
    +    z_max: float = inf,
         *,
         frame: CoordinateFrame = "local"
     ) -> HasCellT
    @@ -4956,6 +4983,18 @@ 

    +
    +
    change_transform(
    +    transform: AffineTransform3D,
    +    frame_to: Optional[CoordinateFrame] = None,
    +    frame_from: Optional[CoordinateFrame] = None,
    +) -> AffineTransform3D
    +
    change_transform(
    +    transform: Transform3D,
    +    frame_to: Optional[CoordinateFrame] = None,
    +    frame_from: Optional[CoordinateFrame] = None,
    +) -> Transform3D
    +
    change_transform(
         transform: Transform3D,
         frame_to: Optional[CoordinateFrame] = None,
    diff --git a/0.4/api/defect/index.html b/0.4/api/defect/index.html
    index 33a1ba6..a08369f 100644
    --- a/0.4/api/defect/index.html
    +++ b/0.4/api/defect/index.html
    @@ -1330,6 +1330,11 @@ 

    atomlib.defect

    + + + + +
    @@ -1457,7 +1462,9 @@

    atoms + atoms +

    Structure to add fault to

    @@ -1471,7 +1478,9 @@

    pos + pos +

    Position on fault plane

    @@ -1485,7 +1494,9 @@

    shift + shift +

    Vector to shift by

    @@ -1499,7 +1510,9 @@

    plane + plane +

    Normal to fault plane

    @@ -1709,7 +1722,9 @@

    atoms + atoms +

    Structure to add edge dislocation to

    @@ -1723,7 +1738,9 @@

    center + center +

    Position on dislocation line

    @@ -1737,7 +1754,9 @@

    b + b +

    Burgers vector of dislocation (FS/RH convention)

    @@ -1751,7 +1770,9 @@

    t + t +

    Tangent vector of dislocation

    @@ -1765,7 +1786,9 @@

    cut + cut +

    Cut plane to create dislocation on

    @@ -1783,7 +1806,9 @@

    poisson + poisson +

    Poisson ratio of material

    @@ -2123,7 +2148,9 @@

    atoms + atoms +

    Structure to add screw dislocation to

    @@ -2137,7 +2164,9 @@

    center + center +

    Position on dislocation line

    @@ -2151,7 +2180,9 @@

    b + b +

    Burgers vector of dislocation (FS/RH convention)

    @@ -2165,7 +2196,9 @@

    cut + cut +

    Cut plane to create dislocation on

    @@ -2183,7 +2216,9 @@

    sign + sign +

    Sign of screw dislocation (True means positive)

    @@ -2407,7 +2442,9 @@

    atoms + atoms +

    Structure to add dislocation loop to

    @@ -2421,7 +2458,9 @@

    center + center +

    Center of dislocation loop

    @@ -2435,7 +2474,9 @@

    b + b +

    Burgers vector of dislocation (defined so travelling upwards leads to a plastic displacment of b).

    @@ -2449,7 +2490,9 @@

    loop_r + loop_r +

    Radius of dislocation loop

    @@ -2463,7 +2506,9 @@

    poisson + poisson +

    Poisson ratio of material

    @@ -2664,7 +2709,9 @@

    atoms + atoms +

    Structure to add dislocation loop to

    @@ -2678,7 +2725,9 @@

    center + center +

    Center of dislocation loop

    @@ -2692,7 +2741,9 @@

    b + b +

    Burgers vector of dislocation (defined so travelling upwards leads to a plastic displacment of b).

    @@ -2706,7 +2757,9 @@

    loop_r + loop_r +

    Radius (side length/2) of dislocation loop

    @@ -2720,7 +2773,9 @@

    poisson + poisson +

    Poisson ratio of material

    @@ -2856,7 +2911,9 @@

    atoms + atoms +

    Structure to add dislocation loop to

    @@ -2870,7 +2927,9 @@

    b + b +

    Burgers vector of dislocation (defined so travelling upwards leads to a plastic displacment of b).

    @@ -2884,7 +2943,9 @@

    poly + poly +

    2D polygon defining dislocation line. It is placed in the plane z=center[2].

    @@ -2898,7 +2959,9 @@

    center + center +

    Center of dislocation loop (defaults to zero, applying no displacement to the gven polygon).

    @@ -2916,7 +2979,9 @@

    poisson + poisson +

    Poisson ratio of material

    diff --git a/0.4/api/elem/index.html b/0.4/api/elem/index.html index 8f10ac7..10cb1f0 100644 --- a/0.4/api/elem/index.html +++ b/0.4/api/elem/index.html @@ -682,15 +682,6 @@ - - -
  • - - - ELEMENT_SYMBOLS_POLARS - - -
  • @@ -709,6 +700,21 @@ + +
  • @@ -718,6 +724,21 @@ + +
  • @@ -727,6 +748,21 @@ + +
  • @@ -1260,15 +1296,6 @@ -
  • - -
  • - - - ELEMENT_SYMBOLS_POLARS - - -
  • @@ -1287,6 +1314,21 @@ + +
  • @@ -1296,6 +1338,21 @@ + +
  • @@ -1305,6 +1362,21 @@ + +
  • @@ -1364,6 +1436,11 @@

    atomlib.elem

    + + + + +
    @@ -1656,29 +1733,6 @@

    -

    - ELEMENT_SYMBOLS_POLARS - - - - module-attribute - - -

    -
    ELEMENT_SYMBOLS_POLARS = Series(
    -    [ELEMENT_SYMBOLS], dtype=List(Utf8)
    -)
    -
    - -
    -
    - -
    - -
    - - -

    DATA_PATH @@ -1706,60 +1760,114 @@

    +
    +
    get_elem(sym: ElemLike) -> int
    +
    get_elem(sym: Series) -> Series
    +
    get_elem(sym: Union[int, str, Series])
     
    +

    Get the atomic number corresponding to a given symbol.

    +

    Examples

    +
    >>> get_elem("Gd")
    +62
    +>>> get_elem(polars.Series(["Gd", "Ce", "O"]))
    +shape: (3,)
    +Series: 'elem' [i8]
    +[
    +    64
    +    58
    +    8
    +]
    +
    +
    Source code in atomlib/elem.py -
    74
    -75
    -76
    -77
    -78
    -79
    -80
    -81
    -82
    -83
    -84
    -85
    -86
    -87
    -88
    -89
    -90
    -91
    -92
    -93
    -94
    -95
    -96
    -97
    def get_elem(sym: t.Union[int, str, polars.Series]):
    -    if isinstance(sym, int):
    -        if not 0 < sym < len(ELEMENTS):
    -            raise ValueError(f"Invalid atomic number {sym}")
    -        return sym
    -
    -    if isinstance(sym, polars.Series):
    -        # TODO: this is a mess
    -        elem = sym.cast(polars.Utf8).str.extract(_SYM_RE, 0).str.to_lowercase() \
    -            .replace_strict(
    -                old=list(ELEMENTS.keys()), new=list(ELEMENTS.values()),
    -                default=None, return_dtype=polars.Int8
    -            ).alias('elem')
    -
    -        if (invalid := sym.filter(sym.is_not_null() & elem.is_null()).to_list()):
    -            raise ValueError(f"Invalid element symbol(s) '{', '.join(map(str, invalid))}'")
    -
    -        return elem
    -
    -    sym_s = re.search(_SYM_RE, str(sym))
    -    try:
    -        return ELEMENTS[sym_s[0].lower()]  # type: ignore
    -    except (KeyError, IndexError):
    -        raise ValueError(f"Invalid element symbol '{sym}'")
    +              
    34
    +35
    +36
    +37
    +38
    +39
    +40
    +41
    +42
    +43
    +44
    +45
    +46
    +47
    +48
    +49
    +50
    +51
    +52
    +53
    +54
    +55
    +56
    +57
    +58
    +59
    +60
    +61
    +62
    +63
    +64
    +65
    +66
    +67
    +68
    +69
    +70
    +71
    +72
    +73
    +74
    +75
    def get_elem(sym: t.Union[int, str, polars.Series]):
    +    """
    +    Get the atomic number corresponding to a given symbol.
    +
    +    # Examples
    +    ```python
    +    >>> get_elem("Gd")
    +    62
    +    >>> get_elem(polars.Series(["Gd", "Ce", "O"]))
    +    shape: (3,)
    +    Series: 'elem' [i8]
    +    [
    +        64
    +        58
    +        8
    +    ]
    +    ```
    +    """
    +
    +    if isinstance(sym, int):
    +        if not 0 < sym < len(ELEMENTS):
    +            raise ValueError(f"Invalid atomic number {sym}")
    +        return sym
    +
    +    if isinstance(sym, polars.Series):
    +        # TODO: this is a mess
    +        elem = sym.cast(polars.Utf8).str.extract(_SYM_RE, 0).str.to_lowercase() \
    +            .replace_strict(
    +                old=list(ELEMENTS.keys()), new=list(ELEMENTS.values()),
    +                default=None, return_dtype=polars.Int8
    +            ).alias('elem')
    +
    +        if (invalid := sym.filter(sym.is_not_null() & elem.is_null()).to_list()):
    +            raise ValueError(f"Invalid element symbol(s) '{', '.join(map(str, invalid))}'")
    +
    +        return elem
    +
    +    sym_s = re.search(_SYM_RE, str(sym))
    +    try:
    +        return ELEMENTS[sym_s[0].lower()]  # type: ignore
    +    except (KeyError, IndexError):
    +        raise ValueError(f"Invalid element symbol '{sym}'")
     
    @@ -1779,9 +1887,39 @@

    +

    Get the elements and quantities corresponding to a formula unit.

    +

    Examples

    +
    >>> get_elems("AlN")
    +[(13, 1.0), (7, 1.0)]
    +>>> get_elems("Al0.93Sc0.07N")
    +[(13, 0.93), (21, 0.07), (7, 1.0)]
    +
    +
    Source code in atomlib/elem.py -
    100
    +              
     78
    + 79
    + 80
    + 81
    + 82
    + 83
    + 84
    + 85
    + 86
    + 87
    + 88
    + 89
    + 90
    + 91
    + 92
    + 93
    + 94
    + 95
    + 96
    + 97
    + 98
    + 99
    +100
     101
     102
     103
    @@ -1804,50 +1942,52 @@ 

    120 121 122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133

    def get_elems(sym: ElemsLike) -> t.List[t.Tuple[int, float]]:
    -    if not isinstance(sym, str):
    -        if isinstance(sym, int):
    -            return [(sym, 1.0)]
    -        return [
    -            (get_elem(v[0]), float(v[1]))  # type: ignore
    -                if (hasattr(v, '__len__') and not isinstance(v, str))
    -                else (get_elem(v), 1.)  # type: ignore
    -            for v in sym
    -        ]
    +123
    def get_elems(sym: ElemsLike) -> t.List[t.Tuple[int, float]]:
    +    """
    +    Get the elements and quantities corresponding to a formula unit.
    +
    +    # Examples
    +    ```python
    +    >>> get_elems("AlN")
    +    [(13, 1.0), (7, 1.0)]
    +    >>> get_elems("Al0.93Sc0.07N")
    +    [(13, 0.93), (21, 0.07), (7, 1.0)]
    +    ```
    +    """
    +
    +    if not isinstance(sym, str):
    +        if isinstance(sym, int):
    +            return [(sym, 1.0)]
    +        return [
    +            (get_elem(v[0]), float(v[1]))  # type: ignore
    +                if (hasattr(v, '__len__') and not isinstance(v, str))
    +                else (get_elem(v), 1.)  # type: ignore
    +            for v in sym
    +        ]
    +
    +    if len(sym) > 0:
    +        sym = sym[0].upper() + sym[1:]
    +    segments = [
    +        (match[1], match[2]) for match in re.finditer(r'([A-Z][a-z]*)([0-9\.]*[+-]?)', str(sym))
    +    ]
    +    if len(segments) == 0:
    +        raise ValueError(f"Invalid compound '{sym}'")
    +
    +    elems = [ELEMENTS.get(seg[0].lower()) for seg in segments]
     
    -    if len(sym) > 0:
    -        sym = sym[0].upper() + sym[1:]
    -    segments = [
    -        (match[1], match[2]) for match in re.finditer(r'([A-Z][a-z]*)([0-9\.]*[+-]?)', str(sym))
    -    ]
    -    if len(segments) == 0:
    -        raise ValueError(f"Invalid compound '{sym}'")
    -
    -    elems = [ELEMENTS.get(seg[0].lower()) for seg in segments]
    +    out = []
    +    for ((elem_sym, num), elem) in zip(segments, elems):
    +        if elem is None:
    +            raise ValueError(f"Unknown element '{elem_sym}' in '{sym}'. Compounds are case-sensitive.")
    +
    +        try:
    +            num = float(num) if len(num) and num[-1] not in ('+', '-') else 1.
    +        except ValueError:
    +            raise ValueError(f"Unknown occupancy '{num}' for elem '{elem_sym}' in compound '{sym}'")
     
    -    out = []
    -    for ((elem_sym, num), elem) in zip(segments, elems):
    -        if elem is None:
    -            raise ValueError(f"Unknown element '{elem_sym}' in '{sym}'. Compounds are case-sensitive.")
    -
    -        try:
    -            num = float(num) if len(num) and num[-1] not in ('+', '-') else 1.
    -        except ValueError:
    -            raise ValueError(f"Unknown occupancy '{num}' for elem '{elem_sym}' in compound '{sym}'")
    -
    -        out.append((elem, num))
    -
    -    return out
    +        out.append((elem, num))
    +
    +    return out
     
    @@ -1862,14 +2002,34 @@

    +
    +
    get_sym(elem: int) -> str
    +
    get_sym(elem: Series) -> Series
    +
    get_sym(elem: Union[int, Series])
     
    +

    Get the symbol corresponding to an atomic number.

    +

    Examples

    +
    >>> get_sym(5)
    +"B"
    +
    +
    Source code in atomlib/elem.py -
    144
    +              
    134
    +135
    +136
    +137
    +138
    +139
    +140
    +141
    +142
    +143
    +144
     145
     146
     147
    @@ -1882,22 +2042,30 @@ 

    154 155 156 -157 -158

    def get_sym(elem: t.Union[int, polars.Series]):
    -    if isinstance(elem, polars.Series):
    -        sym = elem.cast(polars.Int64).replace_strict(
    -            list(range(1, len(ELEMENT_SYMBOLS)+1)),
    -            ELEMENT_SYMBOLS,
    -            default=None,
    -            return_dtype=polars.Utf8,
    -        ).alias('symbol')
    -
    -        if (invalid := elem.filter(elem.is_not_null() & sym.is_null()).unique().to_list()):
    -            raise ValueError(f"Invalid atomic number(s) {', '.join(map(str, invalid))}")
    -
    -        return sym
    -
    -    return _get_sym(elem)
    +157
    def get_sym(elem: t.Union[int, polars.Series]):
    +    """
    +    Get the symbol corresponding to an atomic number.
    +
    +    # Examples
    +    ```python
    +    >>> get_sym(5)
    +    "B"
    +    ```
    +    """
    +    if isinstance(elem, polars.Series):
    +        sym = elem.cast(polars.Int64).replace_strict(
    +            list(range(1, len(ELEMENT_SYMBOLS)+1)),
    +            ELEMENT_SYMBOLS,
    +            default=None,
    +            return_dtype=polars.Utf8,
    +        ).alias('symbol')
    +
    +        if (invalid := elem.filter(elem.is_not_null() & sym.is_null()).unique().to_list()):
    +            raise ValueError(f"Invalid atomic number(s) {', '.join(map(str, invalid))}")
    +
    +        return sym
    +
    +    return _get_sym(elem)
     
    @@ -1912,17 +2080,24 @@

    +
    +
    get_mass(elem: int) -> float
    +
    get_mass(elem: Series) -> Series
    +
    get_mass(elem: Union[ndarray, Sequence[int]]) -> ndarray
    +
    get_mass(elem: Union[int, Sequence[int], ndarray, Series])
     
    -

    Get the standard atomic mass for the given element.

    -

    Follows the 2021 table of the IUPAC Commission on Isotopic Abundances and Atomic Weights <https://doi.org/10.1515/pac-2019-0603>_.

    +

    Get the standard atomic mass for the given element. +Follows the 2021 IUPAC definitions [1].

    +

    [1] 2021 table of the IUPAC Commission on Isotopic Abundances and Atomic Weights https://doi.org/10.1515/pac-2019-0603

    Source code in atomlib/elem.py -
    173
    +              
    172
    +173
     174
     175
     176
    @@ -1939,11 +2114,12 @@ 

    187 188 189 -190

    def get_mass(elem: t.Union[int, t.Sequence[int], numpy.ndarray, polars.Series]):
    -    """
    -    Get the standard atomic mass for the given element.
    +190
    def get_mass(elem: t.Union[int, t.Sequence[int], numpy.ndarray, polars.Series]):
    +    """
    +    Get the standard atomic mass for the given element.
    +    Follows the 2021 IUPAC definitions [1].
     
    -    Follows the `2021 table of the IUPAC Commission on Isotopic Abundances and Atomic Weights <https://doi.org/10.1515/pac-2019-0603>`_.
    +    [1] 2021 table of the IUPAC Commission on Isotopic Abundances and Atomic Weights <https://doi.org/10.1515/pac-2019-0603>
         """
         global _ELEMENT_MASSES
     
    @@ -1976,8 +2152,9 @@ 

    -

    Get crystal ionic radius in angstroms for elem in charge state charge.

    -

    Follows R.D. Shannon, Acta Cryst. A32 (1976) <https://doi.org/10.1107/S0567739476001551>.

    +

    Get crystal ionic radius in angstroms for elem in charge state charge. +Follows the values in [2].

    +

    [2] R.D. Shannon, Acta Cryst. A32 (1976) https://doi.org/10.1107/S0567739476001551

    Source code in atomlib/elem.py @@ -2001,27 +2178,29 @@

    210 211 212 -213

    def get_ionic_radius(elem: int, charge: int) -> float:
    +213
    +214
    def get_ionic_radius(elem: int, charge: int) -> float:
         """
    -    Get crystal ionic radius in angstroms for ``elem`` in charge state ``charge``.
    -
    -    Follows `R.D. Shannon, Acta Cryst. A32 (1976) <https://doi.org/10.1107/S0567739476001551>`.
    -    """
    -    global _ION_RADII
    -
    -    import json
    -
    -    if _ION_RADII is None:
    -        with _open_text_data('ion_radii.json') as f:
    -            _ION_RADII = json.load(f)
    -        assert _ION_RADII is not None
    -
    -    s = f"{get_sym(elem)}{charge:+d}"
    -
    -    try:
    -        return _ION_RADII[s]
    -    except KeyError:
    -        raise ValueError(f"Unknown radius for ion '{s}'") from None
    +    Get crystal ionic radius in angstroms for `elem` in charge state `charge`.
    +    Follows the values in [2].
    +
    +    [2] R.D. Shannon, Acta Cryst. A32 (1976) <https://doi.org/10.1107/S0567739476001551>
    +    """
    +    global _ION_RADII
    +
    +    import json
    +
    +    if _ION_RADII is None:
    +        with _open_text_data('ion_radii.json') as f:
    +            _ION_RADII = json.load(f)
    +        assert _ION_RADII is not None
    +
    +    s = f"{get_sym(elem)}{charge:+d}"
    +
    +    try:
    +        return _ION_RADII[s]
    +    except KeyError:
    +        raise ValueError(f"Unknown radius for ion '{s}'") from None
     
    @@ -2036,6 +2215,11 @@

    +
    +
    get_radius(elem: int) -> float
    +
    get_radius(elem: Series) -> Series
    +
    get_radius(elem: Union[ndarray, Sequence[int]]) -> ndarray
    +
    get_radius(
         elem: Union[int, Sequence[int], ndarray, Series]
     )
    @@ -2043,13 +2227,13 @@ 

    -

    Get the neutral atomic radius for the given element(s), in angstroms.

    -

    Follows the calculated values in E. Clementi et. al, J. Chem. Phys. 47 (1967) <https://doi.org/10.1063/1.1712084>_.

    +

    Get the neutral atomic radius for the given element(s), in angstroms. +Follows the values in [3].

    +

    [3] E. Clementi et. al, J. Chem. Phys. 47 (1967) https://doi.org/10.1063/1.1712084

    Source code in atomlib/elem.py -
    228
    -229
    +              
    229
     230
     231
     232
    @@ -2065,24 +2249,27 @@ 

    242 243 244 -245

    def get_radius(elem: t.Union[int, t.Sequence[int], numpy.ndarray, polars.Series]):
    -    """
    -    Get the neutral atomic radius for the given element(s), in angstroms.
    -
    -    Follows the calculated values in `E. Clementi et. al, J. Chem. Phys. 47 (1967) <https://doi.org/10.1063/1.1712084>`_.
    -    """
    -    global _ELEMENT_RADII
    -
    -    if _ELEMENT_RADII is None:
    -        with _open_binary_data('radii.npy') as f:
    -            _ELEMENT_RADII = numpy.load(f, allow_pickle=False)
    -
    -    if isinstance(elem, polars.Series):
    -        return polars.Series(values=_ELEMENT_RADII)[elem-1]
    -
    -    if isinstance(elem, (int, numpy.ndarray)):
    -        return _ELEMENT_RADII[elem-1]  # type: ignore
    -    return _ELEMENT_RADII[[e-1 for e in elem]]  # type: ignore
    +245
    +246
    +247
    def get_radius(elem: t.Union[int, t.Sequence[int], numpy.ndarray, polars.Series]):
    +    """
    +    Get the neutral atomic radius for the given element(s), in angstroms.
    +    Follows the values in [3].
    +
    +    [3] E. Clementi et. al, J. Chem. Phys. 47 (1967) <https://doi.org/10.1063/1.1712084>
    +    """
    +    global _ELEMENT_RADII
    +
    +    if _ELEMENT_RADII is None:
    +        with _open_binary_data('radii.npy') as f:
    +            _ELEMENT_RADII = numpy.load(f, allow_pickle=False)
    +
    +    if isinstance(elem, polars.Series):
    +        return polars.Series(values=_ELEMENT_RADII)[elem-1]
    +
    +    if isinstance(elem, (int, numpy.ndarray)):
    +        return _ELEMENT_RADII[elem-1]  # type: ignore
    +    return _ELEMENT_RADII[[e-1 for e in elem]]  # type: ignore
     
    diff --git a/0.4/api/expr/index.html b/0.4/api/expr/index.html index 2c3b9f2..8396f7a 100644 --- a/0.4/api/expr/index.html +++ b/0.4/api/expr/index.html @@ -3716,6 +3716,11 @@

    atomlib.expr

    + + + + +
    @@ -3882,7 +3887,7 @@

    VECTOR_OPS: Sequence[Op[ndarray]] = [
         *cast(Op[ndarray], op)
         for op in NUMERIC_OPS,
    -    NaryOp([","], call=stack, precedence=3),
    +    NaryOp([","], call=stack, precedence=3),
     ]
     
    @@ -3931,6 +3936,11 @@

    Bases: Protocol, Generic[V]

    + + + + +
    Source code in atomlib/expr.py
    22
    @@ -3981,6 +3991,11 @@ 

    Bases: ABC, Generic[V]

    + + + + +
    Source code in atomlib/expr.py
    27
    @@ -4048,7 +4063,7 @@ 

    -
    call: Callable = field(repr=False)
    +
    call: Callable = field(repr=False)
     
    @@ -4108,6 +4123,11 @@

    Bases: Op[V]

    + + + + +
    Source code in atomlib/expr.py
    38
    @@ -4187,7 +4207,7 @@ 

    -
    call: VariadicCallable[V] = field(repr=False)
    +
    call: VariadicCallable[V] = field(repr=False)
     
    @@ -4301,6 +4321,11 @@

    Bases: Op[V]

    + + + + +
    Source code in atomlib/expr.py
    55
    @@ -4390,7 +4415,7 @@ 

    -
    call: Callable[[V, V], V] = field(repr=False)
    +
    call: Callable[[V, V], V] = field(repr=False)
     
    @@ -4537,6 +4562,11 @@

    Bases: Op[V]

    + + + + +
    Source code in atomlib/expr.py
    77
    @@ -4600,7 +4630,7 @@ 

    -
    call: Callable[[V], V] = field(repr=False)
    +
    call: Callable[[V], V] = field(repr=False)
     
    @@ -4660,6 +4690,11 @@

    Bases: BinaryOp[V], UnaryOp[V]

    + + + + +
    Source code in atomlib/expr.py
    86
    @@ -4787,7 +4822,7 @@ 

    -
    call: Callable[[V, Optional[V]], V] = field(repr=False)
    +
    call: Callable[[V, Optional[V]], V] = field(repr=False)
     
    @@ -4868,6 +4903,11 @@

    Bases: ABC, Generic[T_co, V]

    + + + + +
    Source code in atomlib/expr.py
     94
    @@ -4991,6 +5031,11 @@ 

    Bases: Token[T_co, V]

    + + + + +
    Source code in atomlib/expr.py
    104
    @@ -5125,6 +5170,11 @@ 

    Bases: Token

    + + + + +
    Source code in atomlib/expr.py
    109
    @@ -5238,6 +5288,11 @@ 

    Bases: Token

    + + + + +
    Source code in atomlib/expr.py
    114
    @@ -5351,6 +5406,11 @@ 

    Bases: Token

    + + + + +
    Source code in atomlib/expr.py
    119
    @@ -5462,6 +5522,11 @@ 

    Bases: Token[T_co, V]

    + + + + +
    Source code in atomlib/expr.py
    123
    @@ -5592,6 +5657,11 @@ 

    Bases: ABC, Generic[T_co, V]

    + + + + +
    Source code in atomlib/expr.py
    128
    @@ -5685,8 +5755,8 @@ 

    format(
         format_scalar: Callable[
             [ValueToken[T_co, V]], str
    -    ] = str,
    -    format_op: Callable[[OpToken[T_co, V]], str] = str,
    +    ] = str,
    +    format_op: Callable[[OpToken[T_co, V]], str] = str,
     ) -> str
     
    @@ -5737,6 +5807,11 @@

    Bases: Expr[T_co, V]

    + + + + +
    Source code in atomlib/expr.py
    146
    @@ -5824,7 +5899,7 @@ 

    -
    op: UnaryOp[V] = field(init=False)
    +
    op: UnaryOp[V] = field(init=False)
     
    @@ -5912,8 +5987,8 @@

    format(
         format_scalar: Callable[
             [ValueToken[T_co, V]], str
    -    ] = str,
    -    format_op: Callable[[OpToken[T_co, V]], str] = str,
    +    ] = str,
    +    format_op: Callable[[OpToken[T_co, V]], str] = str,
     ) -> str
     
    @@ -5962,6 +6037,11 @@

    Bases: Expr[T_co, V]

    + + + + +
    Source code in atomlib/expr.py
    167
    @@ -6049,7 +6129,7 @@ 

    -
    op: BinaryOp[V] = field(init=False)
    +
    op: BinaryOp[V] = field(init=False)
     
    @@ -6136,8 +6216,8 @@

    format(
         format_scalar: Callable[
             [ValueToken[T_co, V]], str
    -    ] = str,
    -    format_op: Callable[[OpToken[T_co, V]], str] = str,
    +    ] = str,
    +    format_op: Callable[[OpToken[T_co, V]], str] = str,
     ) -> str
     
    @@ -6186,6 +6266,11 @@

    Bases: Expr[T_co, V]

    + + + + +
    Source code in atomlib/expr.py
    188
    @@ -6289,7 +6374,7 @@ 

    -
    op: NaryOp[V] = field(init=False)
    +
    op: NaryOp[V] = field(init=False)
     
    @@ -6377,8 +6462,8 @@

    format(
         format_scalar: Callable[
             [ValueToken[T_co, V]], str
    -    ] = str,
    -    format_op: Callable[[OpToken[T_co, V]], str] = str,
    +    ] = str,
    +    format_op: Callable[[OpToken[T_co, V]], str] = str,
     ) -> str
     
    @@ -6433,6 +6518,11 @@

    Bases: Expr[T_co, V]

    + + + + +
    Source code in atomlib/expr.py
    217
    @@ -6621,8 +6711,8 @@ 

    format(
         format_scalar: Callable[
             [ValueToken[T_co, V]], str
    -    ] = str,
    -    format_op: Callable[[OpToken[T_co, V]], str] = str,
    +    ] = str,
    +    format_op: Callable[[OpToken[T_co, V]], str] = str,
     ) -> str
     
    @@ -6671,6 +6761,11 @@

    Bases: Expr[T_co, V]

    + + + + +
    Source code in atomlib/expr.py
    234
    @@ -6813,8 +6908,8 @@ 

    format(
         format_scalar: Callable[
             [ValueToken[T_co, V]], str
    -    ] = str,
    -    format_op: Callable[[OpToken[T_co, V]], str] = str,
    +    ] = str,
    +    format_op: Callable[[OpToken[T_co, V]], str] = str,
     ) -> str
     
    @@ -6863,6 +6958,11 @@

    Bases: Generic[T_co, V]

    + + + + +
    Source code in atomlib/expr.py
    249
    @@ -7107,7 +7207,7 @@ 

    -
    ops: Dict[str, Op[V]] = {k: _efor (k, v) in match_list if v is not None}
    +
    ops: Dict[str, Op[V]] = {k: _Lfor (k, v) in match_list if v is not None}
     
    @@ -7197,6 +7297,11 @@

    Bases: Generic[T_co, V]

    + + + + +
    Source code in atomlib/expr.py
    328
    @@ -8068,6 +8173,11 @@ 

    Bases: Protocol

    + + + + +
    Source code in atomlib/expr.py
    515
    @@ -8132,6 +8242,11 @@ 

    Bases: Protocol

    + + + + +
    Source code in atomlib/expr.py
    529
    diff --git a/0.4/api/index.html b/0.4/api/index.html
    index 4897ea7..1decaa0 100644
    --- a/0.4/api/index.html
    +++ b/0.4/api/index.html
    @@ -4897,6 +4897,11 @@ 

    Index

    + + + + +
    @@ -5015,6 +5020,11 @@

  • type: Numeric atom type, as used by programs like LAMMPS
  • + + + + +
    Source code in atomlib/atoms.py
    - +
     968
    @@ -5550,7 +5560,9 @@ 

    percentiles + percentiles +

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), @@ -6342,6 +6354,22 @@

    +
    +
    partition_by(
    +    by: Union[str, Sequence[str]],
    +    *more_by: str,
    +    maintain_order: bool = True,
    +    include_key: bool = True,
    +    as_dict: Literal[False] = False
    +) -> List[Self]
    +
    partition_by(
    +    by: Union[str, Sequence[str]],
    +    *more_by: str,
    +    maintain_order: bool = True,
    +    include_key: bool = True,
    +    as_dict: Literal[True] = ...
    +) -> Dict[Any, Self]
    +
    partition_by(
         by: Union[str, Sequence[str]],
         *more_by: str,
    @@ -6833,12 +6861,12 @@ 

    crop(
    -    x_min: float = -numpy.inf,
    -    x_max: float = numpy.inf,
    -    y_min: float = -numpy.inf,
    -    y_max: float = numpy.inf,
    -    z_min: float = -numpy.inf,
    -    z_max: float = numpy.inf,
    +    x_min: float = -inf,
    +    x_max: float = inf,
    +    y_min: float = -inf,
    +    y_max: float = inf,
    +    z_min: float = -inf,
    +    z_max: float = inf,
     ) -> Self
     
    @@ -7282,6 +7310,25 @@

    +
    +
    add_atom(
    +    elem: Union[int, str],
    +    x: ArrayLike,
    +    /,
    +    *,
    +    y: None = None,
    +    z: None = None,
    +    **kwargs: Any,
    +) -> Self
    +
    add_atom(
    +    elem: Union[int, str],
    +    /,
    +    x: float,
    +    y: float,
    +    z: float,
    +    **kwargs: Any,
    +) -> Self
    +
    add_atom(
         elem: Union[int, str],
         /,
    @@ -7367,6 +7414,25 @@ 

    +
    +
    pos(
    +    x: Sequence[Optional[float]],
    +    /,
    +    *,
    +    y: None = None,
    +    z: None = None,
    +    tol: float = 1e-06,
    +    **kwargs: Any,
    +) -> Expr
    +
    pos(
    +    x: Optional[float] = None,
    +    y: Optional[float] = None,
    +    z: Optional[float] = None,
    +    *,
    +    tol: float = 1e-06,
    +    **kwargs: Any
    +) -> Expr
    +
    pos(
         x: Union[Sequence[Optional[float]], float, None] = None,
         y: Optional[float] = None,
    @@ -8018,6 +8084,12 @@ 

    +
    +
    read(path: FileOrPath, ty: FileType) -> HasAtomsT
    +
    read(
    +    path: Union[str, Path, TextIO], ty: Literal[None] = None
    +) -> HasAtomsT
    +
    read(
         path: FileOrPath, ty: Optional[FileType] = None
     ) -> HasAtomsT
    @@ -8389,6 +8461,12 @@ 

    +
    +
    write(path: FileOrPath, ty: FileType)
    +
    write(
    +    path: Union[str, Path, TextIO], ty: Literal[None] = None
    +)
    +
    write(path: FileOrPath, ty: Optional[FileType] = None)
     
    @@ -8579,6 +8657,11 @@

    Abstract class representing any (possibly compound) collection of atoms.

    + + + + +
    Source code in atomlib/atoms.py
    - + - + - + - +
    179
    @@ -10449,7 +10532,9 @@ 

    frame + frame +

    Coordinate frame to return atoms in. For a plain HasAtoms, @@ -10539,7 +10624,9 @@

    atoms + atoms +

    HasAtoms to replace these with.

    @@ -10553,7 +10640,9 @@

    frame + frame +

    Coordinate frame inside atoms are in. For a plain HasAtoms, @@ -10647,7 +10736,9 @@

    percentiles + percentiles +

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), @@ -11439,6 +11530,22 @@

    +
    +
    partition_by(
    +    by: Union[str, Sequence[str]],
    +    *more_by: str,
    +    maintain_order: bool = True,
    +    include_key: bool = True,
    +    as_dict: Literal[False] = False
    +) -> List[Self]
    +
    partition_by(
    +    by: Union[str, Sequence[str]],
    +    *more_by: str,
    +    maintain_order: bool = True,
    +    include_key: bool = True,
    +    as_dict: Literal[True] = ...
    +) -> Dict[Any, Self]
    +
    partition_by(
         by: Union[str, Sequence[str]],
         *more_by: str,
    @@ -11930,12 +12037,12 @@ 

    crop(
    -    x_min: float = -numpy.inf,
    -    x_max: float = numpy.inf,
    -    y_min: float = -numpy.inf,
    -    y_max: float = numpy.inf,
    -    z_min: float = -numpy.inf,
    -    z_max: float = numpy.inf,
    +    x_min: float = -inf,
    +    x_max: float = inf,
    +    y_min: float = -inf,
    +    y_max: float = inf,
    +    z_min: float = -inf,
    +    z_max: float = inf,
     ) -> Self
     
    @@ -12379,6 +12486,25 @@

    +
    +
    add_atom(
    +    elem: Union[int, str],
    +    x: ArrayLike,
    +    /,
    +    *,
    +    y: None = None,
    +    z: None = None,
    +    **kwargs: Any,
    +) -> Self
    +
    add_atom(
    +    elem: Union[int, str],
    +    /,
    +    x: float,
    +    y: float,
    +    z: float,
    +    **kwargs: Any,
    +) -> Self
    +
    add_atom(
         elem: Union[int, str],
         /,
    @@ -12464,6 +12590,25 @@ 

    +
    +
    pos(
    +    x: Sequence[Optional[float]],
    +    /,
    +    *,
    +    y: None = None,
    +    z: None = None,
    +    tol: float = 1e-06,
    +    **kwargs: Any,
    +) -> Expr
    +
    pos(
    +    x: Optional[float] = None,
    +    y: Optional[float] = None,
    +    z: Optional[float] = None,
    +    *,
    +    tol: float = 1e-06,
    +    **kwargs: Any
    +) -> Expr
    +
    pos(
         x: Union[Sequence[Optional[float]], float, None] = None,
         y: Optional[float] = None,
    @@ -13140,6 +13285,11 @@ 

    an orthogonal coordinate system. Finally, affine contains any remaining transformations to the local coordinate system, which atoms are stored in.

    + + + + +
    Source code in atomlib/cell.py
    342
    @@ -14053,12 +14203,12 @@ 

    crop(
    -    x_min: float = -numpy.inf,
    -    x_max: float = numpy.inf,
    -    y_min: float = -numpy.inf,
    -    y_max: float = numpy.inf,
    -    z_min: float = -numpy.inf,
    -    z_max: float = numpy.inf,
    +    x_min: float = -inf,
    +    x_max: float = inf,
    +    y_min: float = -inf,
    +    y_max: float = inf,
    +    z_min: float = -inf,
    +    z_max: float = inf,
         *,
         frame: CoordinateFrame = "local"
     ) -> HasCellT
    @@ -14141,6 +14291,18 @@ 

    +
    +
    change_transform(
    +    transform: AffineTransform3D,
    +    frame_to: Optional[CoordinateFrame] = None,
    +    frame_from: Optional[CoordinateFrame] = None,
    +) -> AffineTransform3D
    +
    change_transform(
    +    transform: Transform3D,
    +    frame_to: Optional[CoordinateFrame] = None,
    +    frame_from: Optional[CoordinateFrame] = None,
    +) -> Transform3D
    +
    change_transform(
         transform: Transform3D,
         frame_to: Optional[CoordinateFrame] = None,
    @@ -14404,6 +14566,11 @@ 

    + + + + +
    Source code in atomlib/cell.py
     53
    @@ -15747,12 +15914,12 @@ 

    crop(
    -    x_min: float = -numpy.inf,
    -    x_max: float = numpy.inf,
    -    y_min: float = -numpy.inf,
    -    y_max: float = numpy.inf,
    -    z_min: float = -numpy.inf,
    -    z_max: float = numpy.inf,
    +    x_min: float = -inf,
    +    x_max: float = inf,
    +    y_min: float = -inf,
    +    y_max: float = inf,
    +    z_min: float = -inf,
    +    z_max: float = inf,
         *,
         frame: CoordinateFrame = "local"
     ) -> HasCellT
    @@ -15835,6 +16002,18 @@ 

    +
    +
    change_transform(
    +    transform: AffineTransform3D,
    +    frame_to: Optional[CoordinateFrame] = None,
    +    frame_from: Optional[CoordinateFrame] = None,
    +) -> AffineTransform3D
    +
    change_transform(
    +    transform: Transform3D,
    +    frame_to: Optional[CoordinateFrame] = None,
    +    frame_from: Optional[CoordinateFrame] = None,
    +) -> Transform3D
    +
    change_transform(
         transform: Transform3D,
         frame_to: Optional[CoordinateFrame] = None,
    @@ -15933,6 +16112,11 @@ 

    Cell of atoms with known size and periodic boundary conditions.

    + + + + +
    Source code in atomlib/atomcell.py
    - + - + - +
    612
    @@ -17144,12 +17328,12 @@ 

    crop(
    -    x_min: float = -numpy.inf,
    -    x_max: float = numpy.inf,
    -    y_min: float = -numpy.inf,
    -    y_max: float = numpy.inf,
    -    z_min: float = -numpy.inf,
    -    z_max: float = numpy.inf,
    +    x_min: float = -inf,
    +    x_max: float = inf,
    +    y_min: float = -inf,
    +    y_max: float = inf,
    +    z_min: float = -inf,
    +    z_max: float = inf,
         *,
         frame: CoordinateFrame = "local"
     ) -> Self
    @@ -17207,6 +17391,18 @@ 

    +
    +
    change_transform(
    +    transform: AffineTransform3D,
    +    frame_to: Optional[CoordinateFrame] = None,
    +    frame_from: Optional[CoordinateFrame] = None,
    +) -> AffineTransform3D
    +
    change_transform(
    +    transform: Transform3D,
    +    frame_to: Optional[CoordinateFrame] = None,
    +    frame_from: Optional[CoordinateFrame] = None,
    +) -> Transform3D
    +
    change_transform(
         transform: Transform3D,
         frame_to: Optional[CoordinateFrame] = None,
    @@ -17275,7 +17471,9 @@ 

    percentiles + percentiles +

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), @@ -18049,6 +18247,22 @@

    +
    +
    partition_by(
    +    by: Union[str, Sequence[str]],
    +    *more_by: str,
    +    maintain_order: bool = True,
    +    include_key: bool = True,
    +    as_dict: Literal[False] = False
    +) -> List[Self]
    +
    partition_by(
    +    by: Union[str, Sequence[str]],
    +    *more_by: str,
    +    maintain_order: bool = True,
    +    include_key: bool = True,
    +    as_dict: Literal[True] = ...
    +) -> Dict[Any, Self]
    +
    partition_by(
         by: Union[str, Sequence[str]],
         *more_by: str,
    @@ -18544,12 +18758,12 @@ 

    crop_atoms(
    -    x_min: float = -numpy.inf,
    -    x_max: float = numpy.inf,
    -    y_min: float = -numpy.inf,
    -    y_max: float = numpy.inf,
    -    z_min: float = -numpy.inf,
    -    z_max: float = numpy.inf,
    +    x_min: float = -inf,
    +    x_max: float = inf,
    +    y_min: float = -inf,
    +    y_max: float = inf,
    +    z_min: float = -inf,
    +    z_max: float = inf,
         *,
         frame: CoordinateFrame = "local"
     ) -> Self
    @@ -18991,6 +19205,28 @@ 

    +
    +
    add_atom(
    +    elem: Union[int, str],
    +    x: ArrayLike,
    +    /,
    +    *,
    +    y: None = None,
    +    z: None = None,
    +    frame: Optional[CoordinateFrame] = None,
    +    **kwargs: Any,
    +) -> Self
    +
    add_atom(
    +    elem: Union[int, str],
    +    /,
    +    x: float,
    +    y: float,
    +    z: float,
    +    *,
    +    frame: Optional[CoordinateFrame] = None,
    +    **kwargs: Any,
    +) -> Self
    +
    add_atom(
         elem: Union[int, str],
         /,
    @@ -19054,6 +19290,25 @@ 

    +
    +
    pos(
    +    x: Sequence[Optional[float]],
    +    /,
    +    *,
    +    y: None = None,
    +    z: None = None,
    +    tol: float = 1e-06,
    +    **kwargs: Any,
    +) -> Expr
    +
    pos(
    +    x: Optional[float] = None,
    +    y: Optional[float] = None,
    +    z: Optional[float] = None,
    +    *,
    +    tol: float = 1e-06,
    +    **kwargs: Any
    +) -> Expr
    +
    pos(
         x: Union[Sequence[Optional[float]], float, None] = None,
         y: Optional[float] = None,
    @@ -20101,6 +20356,12 @@ 

    +
    +
    read(path: FileOrPath, ty: FileType) -> HasAtomsT
    +
    read(
    +    path: Union[str, Path, TextIO], ty: Literal[None] = None
    +) -> HasAtomsT
    +
    read(
         path: FileOrPath, ty: Optional[FileType] = None
     ) -> HasAtomsT
    @@ -20472,6 +20733,12 @@ 

    +
    +
    write(path: FileOrPath, ty: FileType)
    +
    write(
    +    path: Union[str, Path, TextIO], ty: Literal[None] = None
    +)
    +
    write(path: FileOrPath, ty: Optional[FileType] = None)
     
    @@ -20627,7 +20894,9 @@

    f + f +

    File or path to write to

    @@ -20641,7 +20910,9 @@

    pseudo + pseudo +

    Mapping from atom symbol

    @@ -21053,6 +21324,11 @@

    Bases: HasAtoms, HasCell, ABC

    + + + + +
    Source code in atomlib/atomcell.py
    - +
     55
    @@ -22908,6 +23184,18 @@ 

    +
    +
    change_transform(
    +    transform: AffineTransform3D,
    +    frame_to: Optional[CoordinateFrame] = None,
    +    frame_from: Optional[CoordinateFrame] = None,
    +) -> AffineTransform3D
    +
    change_transform(
    +    transform: Transform3D,
    +    frame_to: Optional[CoordinateFrame] = None,
    +    frame_from: Optional[CoordinateFrame] = None,
    +) -> Transform3D
    +
    change_transform(
         transform: Transform3D,
         frame_to: Optional[CoordinateFrame] = None,
    @@ -23227,6 +23515,22 @@ 

    +
    +
    partition_by(
    +    by: Union[str, Sequence[str]],
    +    *more_by: str,
    +    maintain_order: bool = True,
    +    include_key: bool = True,
    +    as_dict: Literal[False] = False
    +) -> List[Self]
    +
    partition_by(
    +    by: Union[str, Sequence[str]],
    +    *more_by: str,
    +    maintain_order: bool = True,
    +    include_key: bool = True,
    +    as_dict: Literal[True] = ...
    +) -> Dict[Any, Self]
    +
    partition_by(
         by: Union[str, Sequence[str]],
         *more_by: str,
    @@ -23684,6 +23988,25 @@ 

    +
    +
    pos(
    +    x: Sequence[Optional[float]],
    +    /,
    +    *,
    +    y: None = None,
    +    z: None = None,
    +    tol: float = 1e-06,
    +    **kwargs: Any,
    +) -> Expr
    +
    pos(
    +    x: Optional[float] = None,
    +    y: Optional[float] = None,
    +    z: Optional[float] = None,
    +    *,
    +    tol: float = 1e-06,
    +    **kwargs: Any
    +) -> Expr
    +
    pos(
         x: Union[Sequence[Optional[float]], float, None] = None,
         y: Optional[float] = None,
    @@ -24203,12 +24526,12 @@ 

    crop(
    -    x_min: float = -numpy.inf,
    -    x_max: float = numpy.inf,
    -    y_min: float = -numpy.inf,
    -    y_max: float = numpy.inf,
    -    z_min: float = -numpy.inf,
    -    z_max: float = numpy.inf,
    +    x_min: float = -inf,
    +    x_max: float = inf,
    +    y_min: float = -inf,
    +    y_max: float = inf,
    +    z_min: float = -inf,
    +    z_max: float = inf,
         *,
         frame: CoordinateFrame = "local"
     ) -> Self
    @@ -24267,12 +24590,12 @@ 

    crop_atoms(
    -    x_min: float = -numpy.inf,
    -    x_max: float = numpy.inf,
    -    y_min: float = -numpy.inf,
    -    y_max: float = numpy.inf,
    -    z_min: float = -numpy.inf,
    -    z_max: float = numpy.inf,
    +    x_min: float = -inf,
    +    x_max: float = inf,
    +    y_min: float = -inf,
    +    y_max: float = inf,
    +    z_min: float = -inf,
    +    z_max: float = inf,
         *,
         frame: CoordinateFrame = "local"
     ) -> Self
    @@ -24876,7 +25199,9 @@ 

    percentiles + percentiles +

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), @@ -25763,6 +26088,28 @@

    +
    +
    add_atom(
    +    elem: Union[int, str],
    +    x: ArrayLike,
    +    /,
    +    *,
    +    y: None = None,
    +    z: None = None,
    +    frame: Optional[CoordinateFrame] = None,
    +    **kwargs: Any,
    +) -> Self
    +
    add_atom(
    +    elem: Union[int, str],
    +    /,
    +    x: float,
    +    y: float,
    +    z: float,
    +    *,
    +    frame: Optional[CoordinateFrame] = None,
    +    **kwargs: Any,
    +) -> Self
    +
    add_atom(
         elem: Union[int, str],
         /,
    diff --git a/0.4/api/io/cfg/index.html b/0.4/api/io/cfg/index.html
    index 3cd77f7..5d8dd6f 100644
    --- a/0.4/api/io/cfg/index.html
    +++ b/0.4/api/io/cfg/index.html
    @@ -1570,6 +1570,11 @@ 

    atomlib.io.cfg

    + + + + +
    @@ -1645,6 +1650,11 @@

    + + + + +
    Source code in atomlib/io/cfg.py
    22
    @@ -2147,6 +2157,11 @@ 

    + + + + +
    Source code in atomlib/io/cfg.py
    100
    diff --git a/0.4/api/io/cif/index.html b/0.4/api/io/cif/index.html
    index db20c07..ef13d92 100644
    --- a/0.4/api/io/cif/index.html
    +++ b/0.4/api/io/cif/index.html
    @@ -2020,6 +2020,11 @@ 

    atomlib.io.cif

    + + + + +
    @@ -2097,6 +2102,11 @@

    + + + + +
    Source code in atomlib/io/cif.py
    36
    @@ -2318,6 +2328,11 @@ 

    + + + + +
    Source code in atomlib/io/cif.py
     68
    @@ -2775,7 +2790,7 @@ 

    data_dict: Dict[str, Union[List[Value], Value]] = field(
    -    init=False
    +    init=False
     )
     
    @@ -3269,6 +3284,11 @@

    + + + + +
    Source code in atomlib/io/cif.py
    263
    @@ -3354,6 +3374,11 @@ 

    + + + + +
    Source code in atomlib/io/cif.py
    298
    @@ -3601,6 +3626,11 @@ 

    + + + + +
    Source code in atomlib/io/cif.py
    365
    diff --git a/0.4/api/io/index.html b/0.4/api/io/index.html
    index e678d6e..b73b2c2 100644
    --- a/0.4/api/io/index.html
    +++ b/0.4/api/io/index.html
    @@ -1750,6 +1750,11 @@ 

    atomlib.io

    + + + + +
    @@ -1842,6 +1847,11 @@

    + + + + +
    Source code in atomlib/io/cif.py
    36
    @@ -2063,6 +2073,11 @@ 

    + + + + +
    Source code in atomlib/io/xyz.py
     63
    @@ -2364,7 +2379,7 @@ 

    -
    params: Dict[str, str] = field(default_factory=dict)
    +
    params: Dict[str, str] = field(default_factory=dict)
     
    @@ -2718,6 +2733,11 @@

    + + + + +
    Source code in atomlib/io/xsf.py
     57
    @@ -3310,6 +3330,11 @@ 

    + + + + +
    Source code in atomlib/io/cfg.py
    22
    @@ -3816,6 +3841,11 @@ 

    + + + + +
    Source code in atomlib/io/lmp.py
    - + - + - +
     22
    @@ -5246,7 +5276,9 @@ 

    atomcell + atomcell +

    Structure to write

    @@ -5260,7 +5292,9 @@

    f + f +

    File or path to write to

    @@ -5274,7 +5308,9 @@

    pseudo + pseudo +

    Mapping from atom symbol

    @@ -5932,6 +5968,12 @@

    +
    +
    read(path: FileOrPath, ty: FileType) -> HasAtoms
    +
    read(
    +    path: Union[str, Path, TextIO], ty: Literal[None] = None
    +) -> HasAtoms
    +
    read(
         path: FileOrPath, ty: Optional[FileType] = None
     ) -> HasAtoms
    @@ -6024,6 +6066,14 @@ 

    +
    +
    write(atoms: HasAtoms, path: FileOrPath, ty: FileType)
    +
    write(
    +    atoms: HasAtoms,
    +    path: Union[str, Path, TextIO],
    +    ty: Literal[None] = None,
    +)
    +
    write(
         atoms: HasAtoms,
         path: FileOrPath,
    diff --git a/0.4/api/io/lmp/index.html b/0.4/api/io/lmp/index.html
    index a7d2b09..a458eff 100644
    --- a/0.4/api/io/lmp/index.html
    +++ b/0.4/api/io/lmp/index.html
    @@ -1636,6 +1636,11 @@ 

    atomlib.io.lmp

    + + + + +
    @@ -1663,6 +1668,11 @@

    + + + + +
    Source code in atomlib/io/lmp.py
     22
    @@ -2686,6 +2696,11 @@ 

    + + + + +
    Source code in atomlib/io/lmp.py
    231
    @@ -2822,6 +2837,11 @@ 

    + + + + +
    Source code in atomlib/io/lmp.py
    - + - + - +
    239
    diff --git a/0.4/api/io/mp_api/index.html b/0.4/api/io/mp_api/index.html
    index 67042c8..af3b660 100644
    --- a/0.4/api/io/mp_api/index.html
    +++ b/0.4/api/io/mp_api/index.html
    @@ -1258,6 +1258,11 @@ 

    atomlib.io.mp_api

    + + + + +
    diff --git a/0.4/api/io/mslice/index.html b/0.4/api/io/mslice/index.html index 7dee3a4..b001f6a 100644 --- a/0.4/api/io/mslice/index.html +++ b/0.4/api/io/mslice/index.html @@ -1421,6 +1421,11 @@

    atomlib.io.mslice

    + + + + +
    diff --git a/0.4/api/io/qe/index.html b/0.4/api/io/qe/index.html index 570b7ff..ecc145c 100644 --- a/0.4/api/io/qe/index.html +++ b/0.4/api/io/qe/index.html @@ -1204,6 +1204,11 @@

    atomlib.io.qe

    + + + + +
    @@ -1243,7 +1248,9 @@

    atomcell + atomcell +

    Structure to write

    @@ -1257,7 +1264,9 @@

    f + f +

    File or path to write to

    @@ -1271,7 +1280,9 @@

    pseudo + pseudo +

    Mapping from atom symbol

    diff --git a/0.4/api/io/xsf/index.html b/0.4/api/io/xsf/index.html index a73fdc6..7a29f13 100644 --- a/0.4/api/io/xsf/index.html +++ b/0.4/api/io/xsf/index.html @@ -1642,6 +1642,11 @@

    atomlib.io.xsf

    + + + + +
    @@ -1692,6 +1697,11 @@

    + + + + +
    Source code in atomlib/io/xsf.py
     57
    @@ -2280,6 +2290,11 @@ 

    + + + + +
    Source code in atomlib/io/xsf.py
    150
    diff --git a/0.4/api/io/xyz/index.html b/0.4/api/io/xyz/index.html
    index 0485971..2a8b8fc 100644
    --- a/0.4/api/io/xyz/index.html
    +++ b/0.4/api/io/xyz/index.html
    @@ -1515,6 +1515,11 @@ 

    atomlib.io.xyz

    + + + + +
    @@ -1563,6 +1568,11 @@

    + + + + +
    Source code in atomlib/io/xyz.py
     63
    @@ -1864,7 +1874,7 @@ 

    -
    params: Dict[str, str] = field(default_factory=dict)
    +
    params: Dict[str, str] = field(default_factory=dict)
     
    @@ -2214,6 +2224,11 @@

    + + + + +
    Source code in atomlib/io/xyz.py
    - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - +
    229
    diff --git a/0.4/api/make/index.html b/0.4/api/make/index.html
    index ccd462b..f8d6894 100644
    --- a/0.4/api/make/index.html
    +++ b/0.4/api/make/index.html
    @@ -1420,6 +1420,11 @@ 

    atomlib.make

    + + + + +
    @@ -1486,7 +1491,9 @@

    elem + elem +

    Element to add (e.g. 'Al' or 13)

    @@ -1500,7 +1507,9 @@

    a + a +

    Lattice parameter (Angstrom)

    @@ -1514,7 +1523,9 @@

    cell + cell +

    Cell type to return ('conv', 'prim', or 'ortho')

    @@ -1532,7 +1543,9 @@

    additional + additional +

    Additional atoms to add to the structure.

    @@ -1716,7 +1729,9 @@

    elems + elems +

    Elements to add (e.g. 'AlN' or ('Al', 'N'))

    @@ -1730,7 +1745,9 @@

    a + a +

    Lattice parameter (Angstrom)

    @@ -1744,7 +1761,9 @@

    c + c +

    Vertical lattice parameter (Angstrom)

    @@ -1762,7 +1781,9 @@

    d + d +

    Vertical distance between the two sublattices (fractional)

    @@ -1780,7 +1801,9 @@

    cell + cell +

    Cell type to return ('conv', 'prim', or 'ortho')

    @@ -2054,7 +2077,9 @@

    elems + elems +

    Elements to add (e.g. 'NaCl' or ('Na', 'Cl'))

    @@ -2068,7 +2093,9 @@

    a + a +

    Lattice parameter (Angstrom)

    @@ -2082,7 +2109,9 @@

    cell + cell +

    Cell type to return ('conv', 'prim', or 'ortho'). Returns @@ -2249,7 +2278,9 @@

    elems + elems +

    Elements to add (e.g. 'ZnS' or ('Zn', 'S'))

    @@ -2263,7 +2294,9 @@

    a + a +

    Lattice parameter (Angstrom)

    @@ -2277,7 +2310,9 @@

    cell + cell +

    Cell type to return ('conv', 'prim', or 'ortho'). Returns @@ -2427,6 +2462,20 @@

    +
    +
    diamond(
    +    elem: None = None,
    +    a: Optional[Num] = None,
    +    *,
    +    cell: CellType = "conv"
    +) -> AtomCell
    +
    diamond(
    +    elem: Optional[ElemLike],
    +    a: Num,
    +    *,
    +    cell: CellType = "conv"
    +) -> AtomCell
    +
    diamond(
         elem: Optional[ElemLike] = None,
         a: Optional[Num] = None,
    @@ -2451,7 +2500,9 @@ 

    elem + elem +

    Element to add (e.g. 'C')

    @@ -2469,7 +2520,9 @@

    a + a +

    Lattice parameter (Angstrom)

    @@ -2487,7 +2540,9 @@

    cell + cell +

    Cell type to return ('conv', 'prim', or 'ortho'). Returns @@ -2626,7 +2681,9 @@

    elems + elems +

    Elements to add (e.g. 'CaF' or ('Ca', 'F'))

    @@ -2640,7 +2697,9 @@

    a + a +

    Lattice parameter (Angstrom)

    @@ -2654,7 +2713,9 @@

    cell + cell +

    Cell type to return ('conv', 'prim', or 'ortho'). Returns @@ -2800,6 +2861,29 @@

    +
    +
    cesium_chloride(
    +    elems: Literal["CsCl"] = "CsCl",
    +    a: None = None,
    +    *,
    +    d: None = None,
    +    cell: CellType = "conv"
    +) -> AtomCell
    +
    cesium_chloride(
    +    elems: ElemsLike,
    +    a: Num,
    +    *,
    +    d: None = None,
    +    cell: CellType = "conv"
    +) -> AtomCell
    +
    cesium_chloride(
    +    elems: ElemsLike = "CsCl",
    +    a: None = None,
    +    *,
    +    d: Num,
    +    cell: CellType = "conv"
    +) -> AtomCell
    +
    cesium_chloride(
         elems: ElemsLike = "CsCl",
         a: Optional[Num] = None,
    @@ -2825,7 +2909,9 @@ 

    elems + elems +

    Elements to add (e.g. 'CsCl' or ('Cs', 'Cl'))

    @@ -2843,7 +2929,9 @@

    a + a +

    Lattice parameter (Angstrom)

    @@ -2861,7 +2949,9 @@

    d + d +

    Nearest-neighbor bond distance (Angstrom)

    @@ -2879,7 +2969,9 @@

    cell + cell +

    Cell type to return ('conv', 'prim', or 'ortho'). @@ -3050,7 +3142,9 @@

    elems + elems +

    Elements to add (e.g. 'CaTiO' or ('Ca', 'Ti', 'O'))

    @@ -3064,7 +3158,9 @@

    cell_size + cell_size +

    Lattice parameters (e.g. 3.0 (cubic), [3.0, 5.0] @@ -3079,7 +3175,9 @@

    cell + cell +

    Cell type to return ('conv', 'prim', or 'ortho'). @@ -3255,7 +3353,9 @@

    elems + elems +

    Elements to add (e.g. 'C', 6, or SiO2)

    @@ -3269,7 +3369,9 @@

    density + density +

    Mean mass density to target (g/cm^3)

    @@ -3283,7 +3385,9 @@

    seed + seed +

    Deterministic random seed to add (any object)

    @@ -3301,7 +3405,9 @@

    extra_cols + extra_cols +

    Extra parameters to add to each atom

    @@ -3470,7 +3576,9 @@

    atoms + atoms +

    Input structure

    @@ -3484,7 +3592,9 @@

    zone + zone +

    Zone to align with the +z-axis

    @@ -3502,7 +3612,9 @@

    horz + horz +

    Zone to align with the +x-axis

    @@ -3520,7 +3632,9 @@

    max_n + max_n +

    Maximum number of unit cells to search

    @@ -3538,7 +3652,9 @@

    tol + tol +

    Maximum strain tolerance

    @@ -3758,7 +3874,9 @@

    layer + layer +

    Layer to stack into a stacking sequence. Will be stacked along the c axis.

    @@ -3772,7 +3890,9 @@

    sequence + sequence +

    Stacking sequence. Each layer should be "A", "B", or "C" (in the common case @@ -3787,7 +3907,9 @@

    shift_vector + shift_vector +

    Shift to apply, in fractional coordinates. The shift between each layer @@ -3807,7 +3929,9 @@

    n_layers + n_layers +

    Number of layers which corresponds to a shift of a complete lattice vector. diff --git a/0.4/api/mixins/index.html b/0.4/api/mixins/index.html index 3202ece..8d4b68e 100644 --- a/0.4/api/mixins/index.html +++ b/0.4/api/mixins/index.html @@ -1712,6 +1712,11 @@

    atomlib.mixins

    + + + + +
    @@ -1740,6 +1745,11 @@

    Mix-in to add IO methods to HasAtoms.

    All concrete subclasses of HasAtoms should also subclass this.

    + + + + +
    Source code in atomlib/mixins.py
     45
    @@ -1966,6 +1976,12 @@ 

    +
    +
    read(path: FileOrPath, ty: FileType) -> HasAtomsT
    +
    read(
    +    path: Union[str, Path, TextIO], ty: Literal[None] = None
    +) -> HasAtomsT
    +
    read(
         path: FileOrPath, ty: Optional[FileType] = None
     ) -> HasAtomsT
    @@ -2337,6 +2353,12 @@ 

    +
    +
    write(path: FileOrPath, ty: FileType)
    +
    write(
    +    path: Union[str, Path, TextIO], ty: Literal[None] = None
    +)
    +
    write(path: FileOrPath, ty: Optional[FileType] = None)
     
    @@ -2398,6 +2420,11 @@

    Mix-in to add IO methods to HasAtomCell.

    All concrete subclasses of HasAtomCell should also subclass this.

    + + + + +
    Source code in atomlib/mixins.py
    - + - +
    146
    @@ -2514,6 +2541,12 @@ 

    +
    +
    read(path: FileOrPath, ty: FileType) -> HasAtomsT
    +
    read(
    +    path: Union[str, Path, TextIO], ty: Literal[None] = None
    +) -> HasAtomsT
    +
    read(
         path: FileOrPath, ty: Optional[FileType] = None
     ) -> HasAtomsT
    @@ -2885,6 +2918,12 @@ 

    +
    +
    write(path: FileOrPath, ty: FileType)
    +
    write(
    +    path: Union[str, Path, TextIO], ty: Literal[None] = None
    +)
    +
    write(path: FileOrPath, ty: Optional[FileType] = None)
     
    @@ -3040,7 +3079,9 @@

    f + f +

    File or path to write to

    @@ -3054,7 +3095,9 @@

    pseudo + pseudo +

    Mapping from atom symbol

    diff --git a/0.4/api/testing/index.html b/0.4/api/testing/index.html index abeead7..dd967ce 100644 --- a/0.4/api/testing/index.html +++ b/0.4/api/testing/index.html @@ -1364,6 +1364,11 @@

    atomlib.testing

    + + + + +
    @@ -1385,7 +1390,7 @@

    -
    CallableT = TypeVar('CallableT', bound=Callable)
    +
    CallableT = TypeVar('CallableT', bound=Callable)
     
    diff --git a/0.4/api/transform/index.html b/0.4/api/transform/index.html index d2192fe..568316d 100644 --- a/0.4/api/transform/index.html +++ b/0.4/api/transform/index.html @@ -2384,6 +2384,11 @@

    atomlib.transform

    + + + + +
    @@ -2445,6 +2450,11 @@

    Alternatively, points can be transformed using functional notation: transformed = transform(points).

    + + + + +
    Source code in atomlib/transform.py
     29
    @@ -2754,6 +2764,10 @@ 

    +
    +
    transform(points: BBox3D) -> BBox3D
    +
    transform(points: ArrayLike) -> NDArray[floating]
    +
    transform(
         points: Pts3DLike,
     ) -> Union[BBox3D, NDArray[floating]]
    @@ -2834,6 +2848,11 @@ 

    Transformation which applies a function to the given points.

    + + + + +
    Source code in atomlib/transform.py
    110
    @@ -3067,6 +3086,10 @@ 

    +
    +
    transform(points: BBox3D) -> BBox3D
    +
    transform(points: ArrayLike) -> NDArray[floating]
    +
    transform(
         points: Pts3DLike,
     ) -> Union[BBox3D, NDArray[floating]]
    @@ -3147,6 +3170,11 @@ 

    Bases: Transform3D

    + + + + +
    Source code in atomlib/transform.py
    147
    @@ -4068,6 +4096,12 @@ 

    +
    +
    translate(x: VecLike) -> AffineTransform3D
    +
    translate(
    +    x: Num = 0.0, y: Num = 0.0, z: Num = 0.0
    +) -> AffineTransform3D
    +
    translate(
         x: Union[Num, VecLike] = 0.0, y: Num = 0.0, z: Num = 0.0
     ) -> AffineTransform3D
    @@ -4131,6 +4165,16 @@ 

    +
    +
    scale(x: VecLike) -> AffineTransform3D
    +
    scale(
    +    x: Num = 1.0,
    +    y: Num = 1.0,
    +    z: Num = 1.0,
    +    *,
    +    all: Num = 1.0
    +) -> AffineTransform3D
    +
    scale(
         x: Union[Num, VecLike] = 1.0,
         y: Num = 1.0,
    @@ -4267,6 +4311,10 @@ 

    +
    +
    mirror(a: VecLike) -> AffineTransform3D
    +
    mirror(a: Num, b: Num, c: Num) -> AffineTransform3D
    +
    mirror(
         a: Union[Num, VecLike],
         b: Optional[Num] = None,
    @@ -4452,6 +4500,10 @@ 

    +
    +
    transform(points: BBox3D) -> BBox3D
    +
    transform(points: ArrayLike) -> NDArray[floating]
    +
    transform(
         points: Pts3DLike,
     ) -> Union[BBox3D, NDArray[floating]]
    @@ -4544,6 +4596,10 @@ 

    +
    +
    compose(other: Transform3DT) -> Transform3DT
    +
    compose(other: Transform3D) -> Transform3D
     
    @@ -4590,6 +4646,12 @@

    +
    +
    conjugate(
    +    transform: AffineTransform3D,
    +) -> AffineTransform3D
    +
    conjugate(transform: Transform3DT) -> Transform3DT
    +
    conjugate(transform: Transform3D) -> Transform3D
     
    @@ -4643,6 +4705,11 @@

    Bases: AffineTransform3D

    + + + + +
    Source code in atomlib/transform.py
    435
    @@ -5586,6 +5653,12 @@ 

    +
    +
    translate(x: VecLike) -> AffineTransform3D
    +
    translate(
    +    x: Num = 0.0, y: Num = 0.0, z: Num = 0.0
    +) -> AffineTransform3D
    +
    translate(
         x: Union[Num, VecLike] = 0.0, y: Num = 0.0, z: Num = 0.0
     ) -> AffineTransform3D
    @@ -5934,6 +6007,10 @@ 

    +
    +
    mirror(a: VecLike) -> LinearTransform3D
    +
    mirror(a: Num, b: Num, c: Num) -> LinearTransform3D
    +
    mirror(
         a: Union[Num, VecLike],
         b: Optional[Num] = None,
    @@ -6264,6 +6341,17 @@ 

    +
    +
    align_to(
    +    v1: VecLike,
    +    v2: VecLike,
    +    p1: Literal[None] = None,
    +    p2: Literal[None] = None,
    +) -> LinearTransform3D
    +
    align_to(
    +    v1: VecLike, v2: VecLike, p1: VecLike, p2: VecLike
    +) -> LinearTransform3D
    +
    align_to(
         v1: VecLike,
         v2: VecLike,
    @@ -6431,6 +6519,16 @@ 

    +
    +
    scale(x: VecLike) -> LinearTransform3D
    +
    scale(
    +    x: Num = 1.0,
    +    y: Num = 1.0,
    +    z: Num = 1.0,
    +    *,
    +    all: Num = 1.0
    +) -> LinearTransform3D
    +
    scale(
         x: Union[Num, VecLike] = 1.0,
         y: Num = 1.0,
    @@ -6577,6 +6675,10 @@ 

    +
    +
    transform(points: BBox3D) -> BBox3D
    +
    transform(points: ArrayLike) -> NDArray[floating]
    +
    transform(
         points: Pts3DLike,
     ) -> Union[BBox3D, NDArray[floating]]
    diff --git a/0.4/api/types/index.html b/0.4/api/types/index.html
    index 7a52bdf..423f508 100644
    --- a/0.4/api/types/index.html
    +++ b/0.4/api/types/index.html
    @@ -1328,6 +1328,11 @@ 

    atomlib.types

    + + + + +
    @@ -1489,7 +1494,7 @@

    -
    ScalarT = TypeVar('ScalarT', bound=generic)
    +
    ScalarT = TypeVar('ScalarT', bound=generic)
     
    @@ -1509,6 +1514,12 @@

    +
    +
    to_vec3(v: VecLike, dtype: None = None) -> NDArray[float64]
    +
    to_vec3(
    +    v: VecLike, dtype: Type[ScalarT]
    +) -> NDArray[ScalarT]
    +
    to_vec3(
         v: VecLike, dtype: Optional[Type[generic]] = None
     ) -> NDArray[generic]
    diff --git a/0.4/api/util/index.html b/0.4/api/util/index.html
    index 733c87d..a90e458 100644
    --- a/0.4/api/util/index.html
    +++ b/0.4/api/util/index.html
    @@ -1394,6 +1394,11 @@ 

    atomlib.util

    + + + + +
    @@ -1471,6 +1476,11 @@

    If called on the class, a default instance will be constructed before calling the wrapped function.

    + + + + +
    Source code in atomlib/util.py
    107
    @@ -1551,6 +1561,11 @@ 

    Bases: Exception

    + + + + +
    Source code in atomlib/util.py
    - + - +
    148
    diff --git a/0.4/api/vec/index.html b/0.4/api/vec/index.html
    index b5c5d03..89a4c3f 100644
    --- a/0.4/api/vec/index.html
    +++ b/0.4/api/vec/index.html
    @@ -940,6 +940,21 @@
         
       
       
    +    
    +  
     
           
             
  • @@ -1404,6 +1419,21 @@ + +
  • @@ -1474,6 +1504,11 @@

    atomlib.vec

    + + + + +
    @@ -1515,6 +1550,12 @@

    +
    +
    to_vec3(v: VecLike, dtype: None = None) -> NDArray[float64]
    +
    to_vec3(
    +    v: VecLike, dtype: Type[ScalarT]
    +) -> NDArray[ScalarT]
    +
    to_vec3(
         v: VecLike, dtype: Optional[Type[generic]] = None
     ) -> NDArray[generic]
    @@ -1791,7 +1832,9 @@ 

  • poly + poly +

    Polygon(s) to compute the angle of, array of shape (..., N, 2)

    @@ -1805,7 +1848,9 @@

    pts + pts +

    Point(s) to view the polygons from, array of shape (..., 3)

    @@ -2068,6 +2113,20 @@

    +
    +
    in_polygon(
    +    poly: ndarray,
    +    pt: Literal[None] = None,
    +    *,
    +    rule: WindingRule = "evenodd"
    +) -> Callable[[ndarray], NDArray[bool_]]
    +
    in_polygon(
    +    poly: ndarray,
    +    pt: ndarray,
    +    *,
    +    rule: WindingRule = "evenodd"
    +) -> NDArray[bool_]
    +
    in_polygon(
         poly: ndarray,
         pt: Optional[ndarray] = None,
    @@ -2147,9 +2206,13 @@ 

    -

    Reduce a crystallographic vector (int or float) to lowest common terms. -Example: reduce_vec([3, 3, 3]) = [1, 1, 1] -reduce_vec([0.25, 0.25, 0.25]) = [1, 1, 1]

    +

    Reduce a crystallographic vector (int or float) to lowest common terms.

    +

    Examples

    +
    >>> reduce_vec([3, 6, 9])
    +[1, 2, 3]
    +>>> reduce_vec([0.5, 0.25, 0.25])
    +[2, 1, 1]
    +
    Source code in atomlib/vec.py @@ -2175,29 +2238,43 @@

    210 211 212 -213

    def reduce_vec(arr: ArrayLike, max_denom: int = 10000) -> NDArray[numpy.int64]:
    +213
    +214
    +215
    +216
    +217
    +218
    +219
    +220
    def reduce_vec(arr: ArrayLike, max_denom: int = 10000) -> NDArray[numpy.int64]:
         """
         Reduce a crystallographic vector (int or float) to lowest common terms.
    -    Example: reduce_vec([3, 3, 3]) = [1, 1, 1]
    -    reduce_vec([0.25, 0.25, 0.25]) = [1, 1, 1]
    -    """
    -    a = numpy.atleast_1d(arr)
    -    if not numpy.issubdtype(a.dtype, numpy.floating):
    -        return a // numpy.gcd.reduce(a, axis=-1, keepdims=True)
    -
    -    a = a / numpy.max(numpy.abs(a))
    -
    -    n = numpy.empty(shape=a.shape, dtype=numpy.int64)
    -    d = numpy.empty(shape=a.shape, dtype=numpy.int64)
    -    with numpy.nditer([a, n, d], ['refs_ok'], [['readonly'], ['writeonly'], ['writeonly']]) as it:  # type: ignore
    -        for (v, n_, d_) in it:
    -            (n_[()], d_[()]) = Fraction(float(v)).limit_denominator(max_denom).as_integer_ratio()
    -
    -    # reduce to common denominator
    -    factors = numpy.lcm.reduce(d, axis=-1, keepdims=True) // d
    -    n *= factors
    -    # and then reduce numerators
    -    return n // numpy.gcd.reduce(n, axis=-1, keepdims=True)
    +
    +    # Examples
    +    ```python
    +    >>> reduce_vec([3, 6, 9])
    +    [1, 2, 3]
    +    >>> reduce_vec([0.5, 0.25, 0.25])
    +    [2, 1, 1]
    +    ```
    +    """
    +
    +    a = numpy.atleast_1d(arr)
    +    if not numpy.issubdtype(a.dtype, numpy.floating):
    +        return a // numpy.gcd.reduce(a, axis=-1, keepdims=True)
    +
    +    a = a / numpy.max(numpy.abs(a))
    +
    +    n = numpy.empty(shape=a.shape, dtype=numpy.int64)
    +    d = numpy.empty(shape=a.shape, dtype=numpy.int64)
    +    with numpy.nditer([a, n, d], ['refs_ok'], [['readonly'], ['writeonly'], ['writeonly']]) as it:  # type: ignore
    +        for (v, n_, d_) in it:
    +            (n_[()], d_[()]) = Fraction(float(v)).limit_denominator(max_denom).as_integer_ratio()
    +
    +    # reduce to common denominator
    +    factors = numpy.lcm.reduce(d, axis=-1, keepdims=True) // d
    +    n *= factors
    +    # and then reduce numerators
    +    return n // numpy.gcd.reduce(n, axis=-1, keepdims=True)
     

    @@ -2225,21 +2302,21 @@

    Source code in atomlib/vec.py -
    def miller_4_to_3_vec(a: NDArray[numpy.number], reduce: bool = True, max_denom: int = 10000) -> NDArray[numpy.number]:
    -    """Convert a vector in 4-axis Miller-Bravais notation to 3-axis Miller notation."""
    -    a = numpy.atleast_1d(a)
    -    assert a.shape[-1] == 4
    -    U, V, T, W = numpy.split(a, 4, axis=-1)
    -    assert numpy.allclose(-T, U + V, equal_nan=True)
    -    out = numpy.concatenate((2*U + V, 2*V + U, W), axis=-1)
    -    return reduce_vec(out, max_denom) if reduce else out
    +              
    def miller_4_to_3_vec(a: NDArray[numpy.number], reduce: bool = True, max_denom: int = 10000) -> NDArray[numpy.number]:
    +    """Convert a vector in 4-axis Miller-Bravais notation to 3-axis Miller notation."""
    +    a = numpy.atleast_1d(a)
    +    assert a.shape[-1] == 4
    +    U, V, T, W = numpy.split(a, 4, axis=-1)
    +    assert numpy.allclose(-T, U + V, equal_nan=True)
    +    out = numpy.concatenate((2*U + V, 2*V + U, W), axis=-1)
    +    return reduce_vec(out, max_denom) if reduce else out
     
    @@ -2267,25 +2344,25 @@

    Source code in atomlib/vec.py -
    226
    -227
    -228
    -229
    -230
    -231
    -232
    -233
    +              
    def miller_3_to_4_vec(a: NDArray[numpy.number], reduce: bool = True, max_denom: int = 10000) -> NDArray[numpy.number]:
    -    """Convert a vector in 3-axis Miller notation to 4-axis Miller-Bravais notation."""
    -    a = numpy.atleast_1d(a)
    -    assert a.shape[-1] == 3
    -    u, v, w = numpy.split(a, 3, axis=-1)
    -    U = 2*u - v
    -    V = 2*v - u
    -    W = 3*w
    -    out = numpy.concatenate((U, V, -(U + V), W), axis=-1)
    -    return reduce_vec(out, max_denom) if reduce else out
    +235
    +236
    +237
    +238
    +239
    +240
    +241
    +242
    def miller_3_to_4_vec(a: NDArray[numpy.number], reduce: bool = True, max_denom: int = 10000) -> NDArray[numpy.number]:
    +    """Convert a vector in 3-axis Miller notation to 4-axis Miller-Bravais notation."""
    +    a = numpy.atleast_1d(a)
    +    assert a.shape[-1] == 3
    +    u, v, w = numpy.split(a, 3, axis=-1)
    +    U = 2*u - v
    +    V = 2*v - u
    +    W = 3*w
    +    out = numpy.concatenate((U, V, -(U + V), W), axis=-1)
    +    return reduce_vec(out, max_denom) if reduce else out
     
    @@ -2313,21 +2390,21 @@

    Source code in atomlib/vec.py -
    def miller_4_to_3_plane(a: NDArray[numpy.number], reduce: bool = True, max_denom: int = 10000) -> NDArray[numpy.number]:
    -    """Convert a plane in 4-axis Miller-Bravais notation to 3-axis Miller notation."""
    -    a = numpy.atleast_1d(a)
    -    assert a.shape[-1] == 4
    -    h, k, i, l = numpy.split(a, 4, axis=-1)  # noqa: E741
    -    assert numpy.allclose(-i, h + k, equal_nan=True)
    -    out = numpy.concatenate((h, k, l), axis=-1)
    -    return reduce_vec(out, max_denom) if reduce else out
    +              
    def miller_4_to_3_plane(a: NDArray[numpy.number], reduce: bool = True, max_denom: int = 10000) -> NDArray[numpy.number]:
    +    """Convert a plane in 4-axis Miller-Bravais notation to 3-axis Miller notation."""
    +    a = numpy.atleast_1d(a)
    +    assert a.shape[-1] == 4
    +    h, k, i, l = numpy.split(a, 4, axis=-1)  # noqa: E741
    +    assert numpy.allclose(-i, h + k, equal_nan=True)
    +    out = numpy.concatenate((h, k, l), axis=-1)
    +    return reduce_vec(out, max_denom) if reduce else out
     
    @@ -2355,19 +2432,19 @@

    Source code in atomlib/vec.py -
    def miller_3_to_4_plane(a: NDArray[numpy.number], reduce: bool = True, max_denom: int = 10000) -> NDArray[numpy.number]:
    -    """Convert a plane in 3-axis Miller notation to 4-axis Miller-Bravais notation."""
    -    a = numpy.atleast_1d(a)
    -    assert a.shape[-1] == 3
    -    h, k, l = numpy.split(a, 3, axis=-1)  # noqa: E741
    -    out = numpy.concatenate((h, k, -(h + k), l), axis=-1)
    -    return reduce_vec(out, max_denom) if reduce else out
    +              
    def miller_3_to_4_plane(a: NDArray[numpy.number], reduce: bool = True, max_denom: int = 10000) -> NDArray[numpy.number]:
    +    """Convert a plane in 3-axis Miller notation to 4-axis Miller-Bravais notation."""
    +    a = numpy.atleast_1d(a)
    +    assert a.shape[-1] == 3
    +    h, k, l = numpy.split(a, 3, axis=-1)  # noqa: E741
    +    out = numpy.concatenate((h, k, -(h + k), l), axis=-1)
    +    return reduce_vec(out, max_denom) if reduce else out
     
    diff --git a/0.4/api/visualize/index.html b/0.4/api/visualize/index.html index 1c99dd2..4e64af4 100644 --- a/0.4/api/visualize/index.html +++ b/0.4/api/visualize/index.html @@ -1462,6 +1462,11 @@

    atomlib.visualize

    + + + + +
    @@ -1531,6 +1536,11 @@

    Bases: ABC

    + + + + +
    Source code in atomlib/visualize/__init__.py
    33
    @@ -1609,6 +1619,11 @@ 

    Bases: Figure, AtomImage

    + + + + +
    Source code in atomlib/visualize/__init__.py
    - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/0.4/objects.inv b/0.4/objects.inv index dec8b946d38634786af0590b9ab952f22dbd2a39..f47a1241a97059304ba7d4fcfe0f411dc1d7b5ee 100644 GIT binary patch delta 3533 zcmV;;4KnhRLX1MNViSK{NdFJFFB)tO8ia_DNyV7lbDn!o5Qb=@WFM9BAvuAhrIx`# zNRG11DO!OpA3kq*`p31|?(OB}$Mds`=<@TbUATeIXJ4+)zc863i_4cjO+O8Rc!8aI zG`a+SupVQL4v8O4@0X65pJ|r~j-C=dl>vqy&Oi#E z(UQj>)u0gi#6}_X!2x}iptlnBQ~#(dzTc%esq4P;V4U58+NJBzGLtOhCmE=pTLzH8 ztsm^`J^DiIX@#jZ9fXt6Bu-Q>>zKhE{=4fysn@(LwCsP;bwCG|vDMBv{X|or|D10Fp zd@s;3_d+(*UPwpSi@<@EefQ8i2i>)>D?mD|&Kl^cdV+uZ0`&ImF0BXnec9)@Bz0AF z%OM$8Jpy$iv_y|mFHvn)06h)Qh1WVK61GjU{3QQTO`v1^Lyb-ue9TNeS0b>e|8LYg z?n1ntuJ_2r>BuCIUKs6z!5$UuPrrgut*^qM)>m;%Ya{`W zV}uZo<0~8q`zj2CVL=p8@B|N<1cxfV{m%0hzMa3jzWl=XM>>1N9ci|*1={ss54P)( zL)-O8B29FpN)ZLL(FQ`gwh;jOw+Y1VhsyV^XSVtudV($yk^MVUQoa-Uy|(mz6ALBNm5$D%UsNNUsw96M!TPVM?MlN?GG@5a^+s%q6C4a?*vC(w z8I3_(?e_Wcs4_#Er$$`cF*y)R(WQl`H!bQ4>iri^?S~wg)<8@Cd8L3_ z{ENQC_xcq4&Fs(mjCrPiU2*yuZN(juYSVq5Jx(?f;Q_kYCQZpbZ$PUtv;hwcziodF z2*1tO2-xO(q^qyz0sU8WyX`{!x2q22L64;dk}7#rz%v2fmcPFLi_`YL|BK!Bv}sf8 zt=+~ld1jvlzuT;h;a1l(Koio4o=C^si{`f*ZHzsk_k$BY8636g^ULdORMvk_%x5ay z2D{5yxc+%{K>ge9?8}$4@6q+yKdyh)-It9q&#WERwkG95%rAk9FwVGLfUj+NS$i9E zq3)SDgEUW>U7%}b?&a{qIcDlE%<--`Fmp<3`*yE-q}tP1n_8SPhqc=&bAi5>*nxVSG#Bb>=61~Bv_XH^V%e_j z*z+~KLXD=yAkS%zMu%>jEdf9D`1Uy|rbD43qSiNRbT#Sq^4FIDnXFq!FSw;mEYZ~& zaYSUV#I-4=%|Or=nB0D89A4y5J+q5%D^*4+N1NLqpkn=q<40xt3OP%}5M5_+bu~rQ z!;%9x0w?Wlra=l%+K-Y7xQ2gg9vt1oWuQXcJS` zraE5MKErXLo|v8wwa@Qdq&2SXg6#2a7v-4oI56ux#LEnYpmmQ)!UJ2T5q{1yjqnRN z`W_}6O&+dXWcsj0D(*qcA#gQ*51aq&J7{;4A6}s6)`MopfB|Z^pd25$0DnmV()E=u z3wA7`xZOWs+2OYTVgY~Jj;Tl@l^u#a$t+P5p2iRLBEi!Jnnt!nX-C95-3%nvsvVWYvKl}ReI=Vjp^!el2_4#_j;6AM- zZ`uXGD^jnW?*>-^c3mokqMxqn|Ge*pSA=Xbfco`(KBlfYxLbeZpqd>Wv@>ul(zp_7 z+$PzfIg9+p(S#9+AvzSQlO`n6X~&7MYihpjd$fId#f!JSp@4rdzS9pT!|ostmID|V z%jcX8i7>|}oUPWzZ$sLwtpkP8+HfO(U*1UfGhAz$zba`v1M`O@4c5UMrf3ZyW9j;& zr`??Us62zZ+1!8C>dnLhWX*nXu1|m(%sbuXyQhCPneIN%%Ms?CzYQ{%pE#^-d0Q9a zEJyKyp0X4d?($R5cuMy7!s9kRa+X6E+L0bF*)QPml9{luLCz$sAGLo1H1veBvi9Q)5x1M|7oZ0V*$S!` ztgqhu`p18@*-zYswUrmFtrY4_TfA*!pk);CzU?}{z+~~l)694uE zE3sO1;npbgXuiUT`y zoj`D)s!@45mDMZMFkX{vf0L?=+btx|Nt^7O5AMy+CevYn{c!oI{$V-CWd ze7`O>l~`lb6)R>;TbQtoR^oSp!OQC>Ix~My+jvcb)ry~0CK}n=Vucy%85nui3AIw3 zCA^F$Qd_LCX>8DpX$un;x_+}#RCOQlm`yml-{&tv_v5bL-DmkdD<|~f;`7&QQ=yBK zy8L=A!MaZ(*0Dvhei)IXKvj9#SEo|RRQC63|D#1Ix@o!tTfmdA6D%-XluYF|xvPI~ z$f4T?2^Z)p8{xB% z?#zW!0=a{#wh?N={%I2++(s}t)MkYEc1=-dzn|8 zwXRkxQu?Ld?Ceh2A*x$h^s35y8_|DZN&5y;%||e|(W>%IUnnit{Bs)j72q3aIzIUt zB4=6SBb7`yYteysu-@P&WWZYRAw$bj>K5%CmjWRj_EJmQH7<36+NV`a+B)mB+~Vm^ z=e|DNk-U>CwDTRwwOFKrrcO4^D&Iye;B7I91x&=&a>9w&S}1TLwiXO|uMK}{l8Kl} zMm!NSh*5M%V%s;XWrFp`KR1+fepOp0+01$ln=4DZZiA9|R=d`c&uVR2@>#7(F?d$H z78TEG4N~G+?OH2nR%?_CoRb+eLuL&QIqAH?BAIU1q66=ssO)FNKUehT4{r6(Xl#-S znH+62Q`l2_n{Yx`@_S8wetMdiw@PB!do%b`mX-|L6#Ps}-sID9P zpTp^^T zgLG2n8cG||b#t?}*K~3Vqt(mh=*4=|xxk-EcF>LI%W1Xl-FUo1ZgS6!rb!kRdHQl^ zoJ*Mrr~he0j!CM$kLZ6*UB3MHs7Pbu8Y2d39>ZPW-)r!V=$IV}95XXdSd6yy@)5S} za$I)Vwog;Pn`v^e?tSS*pW4>O#VmuqgD{ukIsp5aur^QV6sO&8o%2_-{}&6;l<0go z#&@gz75U_j%%8yFD|{+a`QbGjbOu-b7Btfy#s|KQ^{|s2F1kf+a}cyy4*Z%N>+1VQ z$w~K>`_Vp6jnNmDLXqZGRK|xSkt)3Andqj|eg4$0U5kD{1PZ(f6*v}Y_*v8LHR2QQPmdp%ka<$H)Mze*lQPl=w& z0K*SwAcfCp$>Wb|PzZfuqY(PwfIdsmTZ#Iqf7I3D@6w#qb>Dd~&Tc{N(sfvxNtW@G z4Ajpp14!W35B7icQXEp?LBNMLa-t{Kkfu5nFO&ief28t4p?7^(qDQ*a2Z3AT0HKX> z1hKX_a{Q)v@%7XBmk;mw^P?U>=0_LU?5H>2#pNAyG`NsWr!J(EsS9*kbg`Tip%;d< z*$ZS8z7Pz)7wDLKAscEhq$BJ_;K0hhd+42m?poLtART{JXASgJJ;8kedV6-4)&u;$ z>~ma_x~jURl#Ht$fjWs=qDQHhw>GPXo`&bbYn>Ab+Xh>HlK-eC&@uj@MyCuuW~QDi z5m?m!H|iaCAzn{%1Ms3ujzJ1|11XgH0UobJ+HTpctpuipGmbxb>acE^9oxRSZ|EK9 zo)_tk5_*3r%U#p5qmp?mu1-afmleIu^OSkld*tGDWD-a(jP}7`kBauEU%{x>S7A`= zt2m}Ll7PoCLWsxl6^?{`6$ZkvAc`n>f(K24LlxhC=lKfX&fi^Me&PEgoju}?G~3_; z?Ru~W+x5ty?Rq4UCOT53hyvPZ10h}82mt-t1Y&=8aubH%(@mi633He-i4V#CoqmVn zZjmG7M*Mm8`Rv_!^yQq_R{tDcGvsbBDBzYaN%nnTg8Z8w9}fICBdOOQrYe2}W)H;iu5eE);fU^IX}DUH;5=H?K51iRKh^hLbw4={b5 zwD~@{9p5T#uK%7Rb1`5SH*Nl2--pZcb6kJjFRBqvRg#Wi{nylXrC}&HGhFF<6Su_) z4hA#qq#^`H4F#N_-Bd)ES9Ehc0(?Zl69(Bd|{tKt}BM?k$ zpe6sjGD0o>MPK53eG2|&_Gf*@Jk!6fIQ@*a;tol*=|0aMC!3J)0Nreprd*#lpw)jE z+JFa!-?j#X-{xxsZ1X+R)#men{tLX_RwMq~RfqDR$5I1Hl{_lonE-FgU*G@5X?x%Q z#cq4rw5b!%S-FGnH<0-sLP@|GYY&{%v>m<;#EB_vrfUA6M({%f^^z)(&f1lX4;Em%v3BXWTBp z*S5T@y^Xn0_e`8Yny1Vz&^0sna`@pKGj$i{cvl>lIVHhib~mS6=nOp>13oNlbu)=7 zz6(@%tV2psv-rxgafm|<=rYH3w3`rb30~m z+8}JPM$=-D=QKy7L$^hjfFF8%`S~>l-z?n)G_<>`Q=5)~&%8 z+|nkN=<19(BC=QF+7#1fAZQCrZof1RFLJ1!*~Pb&DkGJn&211+v3|tyqq2R$oF!t2 zuCuthnj-39$$=YzllC^#AccP??MF!kT*EaFj&9;IP@!&elJR08Q?DuvQ+PP#l?uC; zQB^;Z+eD>-I?V(`ETPnopr+Xf;0^2Q`=?Lm*I&My^SLgt>!-i93znHZ$%Hai5O7hN z6srIiJCP8)M^n6A*@yAp`Ub+g!(CAhJRSz`d)a9O>{h82x)0ne=gEKi=hX@8-z;%z zhqXQM;(~lr9WQI2;kZywOwWhf=XWmB8rOC~_V~7oa?E%fm~|fFWrjk~y2m8pfi2Sr zKj)c7_yrt&4-<|i4_7WSeb^!u_n_qvxEjBu&42bCw7ba~PzEu>ftyR3wqg4n>}1mZ%9&ZrRalQ^aN z%M$-Ke{I04R4U5TM-?Rpj2=EHvATE7zgU27Eg+HwUJ)kQKhEEuef@YHU7vsY{PFDi zd_7@spVpE$?I7S4sn^bTgR20$E|o&jPgnJS-gm<*LN*yd{d#{sA5+&H+%0lY&2A9d z88{YcT!}PplkCu(MSkOG!idBW9SYS+6B6mP<3!jsHQxn3+P>Q2#oOLcz&{w@=?9Zx zcaR6m0St`gQ_zM)nB()$R%_#TC2iK$fx>8QxRJjvZ>0Mft~JeHq_mxZ`6HAD>)_2+ zv<8r|bbSugZq9#wRGvZIZ0>6HX5s;|WfwU!~cKxY|?6Lgic_&`@#i;H!1 zMGnlG5cV=%tUWtCCHs5faT^~w%b^SHNROB77jSsVOjv)|AZHTR4`KltdO}%Q`*DVd z+s*b1(2t}*1=_BD)q~fiJ8RS3`E&Kx_kY>jDg(2vb>-r&=ECM9^Lit;hI#oZ7jK@E zb#1dl#as^Ou(pnc4#Zm?=Y(z1S{LLU1%0ezD085;@;N7C9lsoqYecYPi?TW|auZkk zds~uh1=W8G)>m(S{o~r~C+^_d$_v)k3?P4XO@qik8Pnj%(t!am6#iRobl~2JgGSKa ziH*woTYSGyKjdcnaNk1Z=M}p*^4(%_-$mAD3o>Z0-A2K_+4S#|os%8=Q?K+WIqAvj zeVUhvaSwk>n>i%$G0zTs;C8AfsOa9V$Irb3tunRJ4_>es52ad8jb1?{rr zk9wSkn6lo{SS`A6Ym|93U+Cx;=8zxx$K7B{@>Zv=WsHqm?OKjZX?z1%hi(kqs`T3e zlX^JyJ0|f?AUIIfs63s@>J@4juSvGQNma(}7Lw+P_vUAl=`g^4xcpTAu$*J^ zNn3xs6jZQ8=U^3M&<<5G$MR4Wa|jMrF-GP9728x4l?Z=AuDB{J6a0)kaadUCVH1_$ zM&pX-dSpn}8BkbHK;hj0h4%pz-T_ef+fU)IJ_Ww_w82YH8@%zf!Rt;Nyjv9bVr^PW z&}q&VwhcMHUl*H7tg-2e6*HzSOxQ*%@jHLP;N|rbotdX?yr#iw#m_1ejcjeP!VL8c zj6CavS}D#FUd9utE!NmHHfYAQg$WB?zga1&x{rU%CY;^x^B1A}ao6wev-}p86Z&xR z`Rlc*(8Wnze!Z4p-RBhR*dkd!s>o5GsyywhQ>kPs`+K$j(V`UHG~IzM;K|ns78rjn zN~Us~+|@Va&~1Z+3v`u@@L5b3dCptw7>Y6it|+Kfq265XF4I?`J@(jG(l5~5=f@&9 zrbt9$*}qSB=0Yif+(A{_2sL5%&W~>SF05%{ZemscBkwR)vbRldR1kW<7_^m8D&`K}kHTU2Dl_wKgsJ ztk$F$JgZ%cif6S3De zxB6!^Hc5p{jy9SpY&CRl(WifI)~Gja&OxZIP)`2uzLzdV1*&ifhOw5Mib zdVWY$*Ny#9FUz8Fiv)V6`}UAgs&1?3-??!LWI@whKmIB)?zLq>j&TuDju7=%t!$Yi zAJS#65Yo~?Iw^Avr48x2xmnw5I=O|>>g96uV!i2H;LjvG=*IKqv|9IWJl-KUx#vdH zB#VkXeYrEvrObrW|1^Ih$0XI>NA#vHU;cYkq_J_05d$@k;V$s+HTXt!%nk*PnVBan zM%#M%2-|i!F1u{or>Wn~G&xxJzI38bZENFVmOdf3Sh7u~iw2-++M zeoc;b_5Gvdr2ESKXrHIX=nG4sNb@Qx<3o~26<+g9bkpfRe`?pRMZX^c1>S@T9E&vk OENT(XnfwPtmRU!q*c#yg diff --git a/0.4/search/search_index.json b/0.4/search/search_index.json index 8ea1b3b..b784c73 100644 --- a/0.4/search/search_index.json +++ b/0.4/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"atomlib: A modern, extensible library for creating atomic structures","text":"

    atomlib is a package for creating, modifying, and controlling atomic structures. It draws heavy inspiration from previous tools like Atomsk and ASE, but attempts to provide a cleaner, more consistent interface that can be used from Python or a command line.

    atomlib has minimal dependencies: numpy, scipy, and polars are required for core atom structure manipulation, and click is required for command line functionality.

    "},{"location":"#atomic-representation-supported-properties","title":"Atomic representation & supported properties","text":"

    Atomic structures are stored as polars DataFrames, providing a clean, immutable interface that maximizes expressiveness and minimizes errors. For formats that allow arbitrary properties, these properties can be passed through transparently. atomlib has first-class support for fractional occupancy, Debye-Waller factors, atomic forces, and labels.

    Translational symmetry is stored in Cell objects, which represent a fully generic cell. Atoms can be modified in any coordinate system that makes sense (global, local real-space, cell fraction, box fraction, etc.). Support for non-translational symmetry operations is limited at this point.

    For more information, check out the example notebooks and the API documentation.

    "},{"location":"#currently-supported-file-formats","title":"Currently supported file formats","text":"

    File format support is still a work in progress. Where possible, parsers are implemented from scratch in-repo. Most formats are implemented in two steps: Parsing to an intermediate representation which preserves all format-specific information, and then conversion to the generic Atoms & AtomCell types for manipulation & display. This means you can write your own code to utilize advanced format features even if they're not supported out of the box.

    Format Ext. Read Write Notes CIF .cif CIF1 & CIF2. Isotropic B-factor only XCrysDen .xsf AtomEye CFG .cfg Currently basic format only Basic XYZ .xyz Ext. XYZ .exyz Arbitrary properties not implemented Special XYZ .sxyz To be implemented LAMMPS Data .lmp Quantum Espresso .qe pw.x format pyMultislicer .mslice Currently XML format only"},{"location":"api/","title":"Index","text":"

    Top-level exports:

    "},{"location":"api/#atomlib.AtomSelection","title":"AtomSelection module-attribute","text":"
    AtomSelection: TypeAlias = Union[\n    IntoExprColumn,\n    NDArray[bool_],\n    ArrayLike,\n    Mapping[str, Any],\n]\n

    Polars expression selecting a subset of atoms. Can be used with many Atoms methods.

    "},{"location":"api/#atomlib.CoordinateFrame","title":"CoordinateFrame module-attribute","text":"
    CoordinateFrame: TypeAlias = Literal[\n    \"cell\",\n    \"cell_frac\",\n    \"cell_box\",\n    \"ortho\",\n    \"ortho_frac\",\n    \"ortho_box\",\n    \"linear\",\n    \"local\",\n    \"global\",\n]\n

    A coordinate frame to use.

    • cell: Real-space units along crystal axes
    • cell_frac: Fraction of unit cells
    • cell_box: Fraction of cell box
    • ortho: Real-space units along orthogonal cell
    • ortho_frac: Fraction of orthogonal cell
    • ortho_box: Fraction of orthogonal box
    • linear: Angstroms in local coordinate system (without affine transformation)
    • local: Angstroms in local coordinate system (with affine transformation)
    • global: Angstroms in global coordinate system (with all transformations)

    For more information, see the documentation at Coordinate systems, or the example notebook at examples/coords.ipynb.

    "},{"location":"api/#atomlib.Atoms","title":"Atoms","text":"

    Bases: AtomsIOMixin, HasAtoms

    A collection of atoms, absent any implied coordinate system. Implemented as a wrapper around a polars.DataFrame.

    Must contain the following columns:

    • coords: array of [x, y, z] positions, float
    • elem: atomic number, int
    • symbol: atomic symbol (may contain charges)

    In addition, it commonly contains the following columns:

    • i: Initial atom number
    • wobble: Isotropic Debye-Waller mean-squared deviation (\\(\\left<u^2\\right> = B \\cdot \\frac{3}{8 \\pi^2}\\), dimensions of [Length^2])
    • frac_occupancy: Fractional occupancy, in the range [0., 1.]
    • mass: Atomic mass, in g/mol (approx. Da)
    • velocity: array of [x, y, z] velocities, float, dimensions of length/time
    • type: Numeric atom type, as used by programs like LAMMPS
    Source code in atomlib/atoms.py
    class Atoms(AtomsIOMixin, HasAtoms):\n    r\"\"\"\n    A collection of atoms, absent any implied coordinate system.\n    Implemented as a wrapper around a [`polars.DataFrame`][polars.DataFrame].\n\n    Must contain the following columns:\n\n    - coords: array of `[x, y, z]` positions, float\n    - elem: atomic number, int\n    - symbol: atomic symbol (may contain charges)\n\n    In addition, it commonly contains the following columns:\n\n    - i: Initial atom number\n    - wobble: Isotropic Debye-Waller mean-squared deviation ($\\left<u^2\\right> = B \\cdot \\frac{3}{8 \\pi^2}$, dimensions of [Length^2])\n    - frac_occupancy: Fractional occupancy, in the range [0., 1.]\n    - mass: Atomic mass, in g/mol (approx. Da)\n    - velocity: array of `[x, y, z]` velocities, float, dimensions of length/time\n    - type: Numeric atom type, as used by programs like LAMMPS\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n\n    def __init__(self, data: t.Optional[IntoAtoms] = None, columns: t.Optional[t.Sequence[str]] = None,\n                 orient: t.Union[t.Literal['row'], t.Literal['col'], None] = None,\n                 _unchecked: bool = False):\n        self._bbox: t.Optional[BBox3D] = None\n        self.inner: polars.DataFrame\n\n        if data is None:\n            assert columns is None\n            self.inner = polars.DataFrame([\n                polars.Series('coords', (), dtype=polars.Array(polars.Float64, 3)),\n                polars.Series('elem', (), dtype=polars.Int8),\n                polars.Series('symbol', (), dtype=polars.Utf8),\n            ])\n        elif isinstance(data, polars.DataFrame):\n            self.inner = data\n        elif isinstance(data, Atoms):\n            self.inner = data.inner\n            _unchecked = True\n        else:\n            self.inner = polars.DataFrame(data, schema=columns, orient=orient)\n\n        if not _unchecked:\n            # stack ('x', 'y', 'z') -> 'coords'\n            self.inner = _with_columns_stacked(self.inner, ('x', 'y', 'z'), 'coords')\n            self.inner = _with_columns_stacked(self.inner, ('v_x', 'v_y', 'v_z'), 'velocity')\n\n            missing: t.Tuple[str, ...] = tuple(set(['symbol', 'elem']) - set(self.columns))\n            if len(missing) > 1:\n                raise ValueError(\"'Atoms' missing columns 'elem' and/or 'symbol'.\")\n            # fill 'symbol' from 'elem' or vice-versa\n            if missing == ('symbol',):\n                self.inner = self.inner.with_columns(get_sym(self.inner['elem']))\n            elif missing == ('elem',):\n                # by convention, add before 'symbol' column\n                self.inner = self.inner.insert_column(\n                    self.inner.get_column_index('symbol'),\n                    get_elem(self.inner['symbol']),\n                )\n\n            # cast to standard dtypes\n            self.inner = self.inner.with_columns([\n                self.inner[col].cast(dtype)\n                for (col, dtype) in _COLUMN_DTYPES.items() if col in self.inner\n            ])\n\n            self._validate_atoms()\n\n    @staticmethod\n    def empty() -> Atoms:\n        \"\"\"\n        Return an empty Atoms with only the mandatory columns.\n        \"\"\"\n        return Atoms()\n\n    def _validate_atoms(self):\n        missing = [col for col in _REQUIRED_COLUMNS if col not in self.columns]\n        if len(missing):\n            raise ValueError(f\"'Atoms' missing column(s) {', '.join(map(repr, missing))}\")\n\n    def get_atoms(self, frame: t.Literal['local'] = 'local') -> Atoms:\n        if frame != 'local':\n            raise ValueError(f\"Atoms without a cell only support the 'local' coordinate frame, not '{frame}'.\")\n        return self\n\n    def with_atoms(self, atoms: HasAtoms, frame: t.Literal['local'] = 'local') -> Atoms:\n        if frame != 'local':\n            raise ValueError(f\"Atoms without a cell only support the 'local' coordinate frame, not '{frame}'.\")\n        return atoms.get_atoms()\n\n    @classmethod\n    def _combine_metadata(cls: t.Type[Atoms], *atoms: HasAtoms) -> Atoms:\n        return cls.empty()\n\n    def bbox(self) -> BBox3D:\n        \"\"\"Return the bounding box of all the points in `self`.\"\"\"\n        if self._bbox is None:\n            self._bbox = BBox3D.from_pts(self.coords())\n\n        return self._bbox\n\n    def __str__(self) -> str:\n        return f\"Atoms, {self.inner!s}\"\n\n    def __repr__(self) -> str:\n        buf = StringIO()\n        buf.write(\"Atoms([\\n\")\n\n        for series in self.inner.to_dict().values():\n            buf.write(f\"    Series({series.name!r}, {series.to_list()!r}, dtype={series.dtype!r}),\\n\")\n\n        buf.write(\"])\\n\")\n        return buf.getvalue()\n\n    def _repr_pretty_(self, p: t.Any, cycle: bool) -> None:\n        p.text('Atoms(...)') if cycle else p.text(str(self))\n
    "},{"location":"api/#atomlib.Atoms.columns","title":"columns property","text":"
    columns: List[str]\n

    Return the column names in self.

    RETURNS DESCRIPTION List[str]

    A sequence of column names

    "},{"location":"api/#atomlib.Atoms.dtypes","title":"dtypes property","text":"
    dtypes: List[DataType]\n

    Return the datatypes in self.

    RETURNS DESCRIPTION List[DataType]

    A sequence of column DataTypes

    "},{"location":"api/#atomlib.Atoms.schema","title":"schema property","text":"
    schema: Schema\n

    Return the schema of self.

    RETURNS DESCRIPTION Schema

    A dictionary of column names and DataTypes

    "},{"location":"api/#atomlib.Atoms.with_column","title":"with_column class-attribute instance-attribute","text":"
    with_column = with_columns\n
    "},{"location":"api/#atomlib.Atoms.transform","title":"transform class-attribute instance-attribute","text":"
    transform = transform_atoms\n
    "},{"location":"api/#atomlib.Atoms.crop_atoms","title":"crop_atoms class-attribute instance-attribute","text":"
    crop_atoms = crop\n
    "},{"location":"api/#atomlib.Atoms.unique","title":"unique class-attribute instance-attribute","text":"
    unique = deduplicate\n
    "},{"location":"api/#atomlib.Atoms.inner","title":"inner instance-attribute","text":"
    inner: DataFrame\n
    "},{"location":"api/#atomlib.Atoms.describe","title":"describe","text":"
    describe(\n    percentiles: Union[Sequence[float], float, None] = (\n        0.25,\n        0.5,\n        0.75,\n    ),\n    *,\n    interpolation: RollingInterpolationMethod = \"nearest\"\n) -> DataFrame\n

    Return summary statistics for self. See DataFrame.describe for more information.

    PARAMETER DESCRIPTION percentiles

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), 50% (median), and 75% (third quartile).

    TYPE: Union[Sequence[float], float, None] DEFAULT: (0.25, 0.5, 0.75)

    RETURNS DESCRIPTION DataFrame

    A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.describe)\ndef describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n             interpolation: RollingInterpolationMethod = 'nearest') -> polars.DataFrame:\n    \"\"\"\n    Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n    Args:\n      percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                   50% (median), and 75% (third quartile).\n\n    Returns:\n      A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.Atoms.with_columns","title":"with_columns","text":"
    with_columns(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Return a copy of self with the given columns added.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef with_columns(self,\n                 *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 **named_exprs: IntoExpr) -> polars.DataFrame:\n    \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n    return self._get_frame().with_columns(*exprs, **named_exprs)\n
    "},{"location":"api/#atomlib.Atoms.insert_column","title":"insert_column","text":"
    insert_column(index: int, column: Series) -> DataFrame\n
    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n    return self._get_frame().insert_column(index, column)\n
    "},{"location":"api/#atomlib.Atoms.get_column","title":"get_column","text":"
    get_column(name: str) -> Series\n

    Get the specified column from self, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(lambda df, name: df.get_column(name))\ndef get_column(self, name: str) -> polars.Series:\n    \"\"\"\n    Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.Atoms.get_columns","title":"get_columns","text":"
    get_columns() -> List[Series]\n

    Return all columns from self as a list of Series.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_columns)\ndef get_columns(self) -> t.List[polars.Series]:\n    \"\"\"\n    Return all columns from `self` as a list of [`Series`][polars.Series].\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.Atoms.get_column_index","title":"get_column_index","text":"
    get_column_index(name: str) -> int\n

    Get the index of a column by name, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_column_index)\ndef get_column_index(self, name: str) -> int:\n    \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.Atoms.group_by","title":"group_by","text":"
    group_by(\n    *by: Union[IntoExpr, Iterable[IntoExpr]],\n    maintain_order: bool = False,\n    **named_by: IntoExpr\n) -> GroupBy\n

    Start a group by operation. See DataFrame.group_by for more information.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.group_by)\ndef group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]], maintain_order: bool = False,\n             **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n    \"\"\"\n    Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.Atoms.pipe","title":"pipe","text":"
    pipe(\n    function: Callable[Concatenate[HasAtomsT, P], T],\n    *args: args,\n    **kwargs: kwargs\n) -> T\n

    Apply function to self (in method-call syntax).

    Source code in atomlib/atoms.py
    def pipe(self: HasAtomsT, function: t.Callable[Concatenate[HasAtomsT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n    \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n    return function(self, *args, **kwargs)\n
    "},{"location":"api/#atomlib.Atoms.clone","title":"clone","text":"
    clone() -> DataFrame\n

    Return a copy of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef clone(self) -> polars.DataFrame:\n    \"\"\"Return a copy of `self`.\"\"\"\n    return self._get_frame().clone()\n
    "},{"location":"api/#atomlib.Atoms.drop","title":"drop","text":"
    drop(\n    *columns: Union[str, Iterable[str]], strict: bool = True\n) -> DataFrame\n

    Return self with the specified columns removed.

    Source code in atomlib/atoms.py
    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n    \"\"\"Return `self` with the specified columns removed.\"\"\"\n    return self._get_frame().drop(*columns, strict=strict)\n
    "},{"location":"api/#atomlib.Atoms.filter","title":"filter","text":"
    filter(\n    *predicates: Union[\n        None,\n        IntoExprColumn,\n        Iterable[IntoExprColumn],\n        bool,\n        List[bool],\n        ndarray,\n    ],\n    **constraints: Any\n) -> Self\n

    Filter self, removing rows which evaluate to False.

    Source code in atomlib/atoms.py
    def filter(\n    self,\n    *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n    **constraints: t.Any,\n) -> Self:\n    \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n    # TODO clean up\n    preds_not_none = tuple(filter(lambda p: p is not None, predicates))\n    if not len(preds_not_none) and not len(constraints):\n        return self\n    return self.with_atoms(Atoms(self._get_frame().filter(*preds_not_none, **constraints), _unchecked=True))  # type: ignore\n
    "},{"location":"api/#atomlib.Atoms.sort","title":"sort","text":"
    sort(\n    by: Union[IntoExpr, Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: Union[bool, Sequence[bool]] = False,\n    nulls_last: bool = False\n) -> DataFrame\n

    Sort the atoms in self by the given columns/expressions.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef sort(\n    self,\n    by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: t.Union[bool, t.Sequence[bool]] = False,\n    nulls_last: bool = False,\n) -> polars.DataFrame:\n    \"\"\"\n    Sort the atoms in `self` by the given columns/expressions.\n    \"\"\"\n    return self._get_frame().sort(\n        by, *more_by, descending=descending, nulls_last=nulls_last\n    )\n
    "},{"location":"api/#atomlib.Atoms.slice","title":"slice","text":"
    slice(\n    offset: int, length: Optional[int] = None\n) -> DataFrame\n

    Return a slice of the rows in self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef slice(self, offset: int, length: t.Optional[int] = None) -> polars.DataFrame:\n    \"\"\"Return a slice of the rows in `self`.\"\"\"\n    return self._get_frame().slice(offset, length)\n
    "},{"location":"api/#atomlib.Atoms.head","title":"head","text":"
    head(n: int = 5) -> DataFrame\n

    Return the first n rows of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef head(self, n: int = 5) -> polars.DataFrame:\n    \"\"\"Return the first `n` rows of `self`.\"\"\"\n    return self._get_frame().head(n)\n
    "},{"location":"api/#atomlib.Atoms.tail","title":"tail","text":"
    tail(n: int = 5) -> DataFrame\n

    Return the last n rows of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef tail(self, n: int = 5) -> polars.DataFrame:\n    \"\"\"Return the last `n` rows of `self`.\"\"\"\n    return self._get_frame().tail(n)\n
    "},{"location":"api/#atomlib.Atoms.drop_nulls","title":"drop_nulls","text":"
    drop_nulls(\n    subset: Union[str, Collection[str], None] = None\n) -> DataFrame\n

    Drop rows that contain nulls in any of columns subset.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n    \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n    return self._get_frame().drop_nulls(subset)\n
    "},{"location":"api/#atomlib.Atoms.fill_null","title":"fill_null","text":"
    fill_null(\n    value: Any = None,\n    strategy: Optional[FillNullStrategy] = None,\n    limit: Optional[int] = None,\n    matches_supertype: bool = True,\n) -> DataFrame\n

    Fill null values in self, using the specified value or strategy.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef fill_null(\n    self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n    limit: t.Optional[int] = None, matches_supertype: bool = True,\n) -> polars.DataFrame:\n    \"\"\"Fill null values in `self`, using the specified value or strategy.\"\"\"\n    return self._get_frame().fill_null(value, strategy, limit, matches_supertype=matches_supertype)\n
    "},{"location":"api/#atomlib.Atoms.fill_nan","title":"fill_nan","text":"
    fill_nan(value: Union[Expr, int, float, None]) -> DataFrame\n

    Fill floating-point NaN values in self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef fill_nan(self, value: t.Union[polars.Expr, int, float, None]) -> polars.DataFrame:\n    \"\"\"Fill floating-point NaN values in `self`.\"\"\"\n    return self._get_frame().fill_nan(value)\n
    "},{"location":"api/#atomlib.Atoms.concat","title":"concat classmethod","text":"
    concat(\n    atoms: Union[\n        HasAtomsT,\n        IntoAtoms,\n        Iterable[Union[HasAtomsT, IntoAtoms]],\n    ],\n    *,\n    rechunk: bool = True,\n    how: ConcatMethod = \"vertical\"\n) -> HasAtomsT\n

    Concatenate multiple Atoms together, handling metadata appropriately.

    Source code in atomlib/atoms.py
    @classmethod\ndef concat(cls: t.Type[HasAtomsT],\n           atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n           rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n    \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n    # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n    # same type as ``cls``.\n    if _is_abstract(cls):\n        raise TypeError(\"concat() must be called on a concrete class.\")\n\n    if isinstance(atoms, HasAtoms):\n        atoms = (atoms,)\n    dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n    representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n    if len(dfs) == 0:\n        return representative.with_atoms(Atoms.empty(), 'local')\n\n    if how in ('vertical', 'vertical_relaxed'):\n        # get order from first member\n        cols = dfs[0].columns\n        dfs = [df.select(cols) for df in dfs]\n    elif how == 'inner':\n        cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n        schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n        if len(schema) == 0:\n            raise ValueError(\"Atoms have no columns in common\")\n\n        dfs = [_select_schema(df, schema) for df in dfs]\n        how = 'vertical'\n\n    return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n
    "},{"location":"api/#atomlib.Atoms.partition_by","title":"partition_by","text":"
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: bool = False\n) -> Union[List[Self], Dict[Any, Self]]\n

    Group by the given columns and partition into separate dataframes.

    Return the partitions as a dictionary by specifying as_dict=True.

    Source code in atomlib/atoms.py
    def partition_by(\n    self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n    maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n    \"\"\"\n    Group by the given columns and partition into separate dataframes.\n\n    Return the partitions as a dictionary by specifying `as_dict=True`.\n    \"\"\"\n    if as_dict:\n        d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n        return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n    return [\n        self.with_atoms(Atoms(df, _unchecked=True))\n        for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n    ]\n
    "},{"location":"api/#atomlib.Atoms.select","title":"select","text":"
    select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.select)\ndef select(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr,\n) -> polars.DataFrame:\n    \"\"\"\n    Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.Atoms.select_schema","title":"select_schema","text":"
    select_schema(schema: SchemaDict) -> DataFrame\n

    Select columns from self and cast to the given schema. Raises TypeError if a column is not found or if it can't be cast.

    Source code in atomlib/atoms.py
    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n    \"\"\"\n    Select columns from `self` and cast to the given schema.\n    Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n    \"\"\"\n    return _select_schema(self, schema)\n
    "},{"location":"api/#atomlib.Atoms.select_props","title":"select_props","text":"
    select_props(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Self\n

    Select exprs from self, while keeping required columns.

    RETURNS DESCRIPTION Self

    A HasAtoms filtered to contain the

    Self

    specified properties (as well as required columns).

    Source code in atomlib/atoms.py
    def select_props(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Self:\n    \"\"\"\n    Select `exprs` from `self`, while keeping required columns.\n\n    Returns:\n      A [`HasAtoms`][atomlib.atoms.HasAtoms] filtered to contain the\n      specified properties (as well as required columns).\n    \"\"\"\n    props = self._get_frame().lazy().select(*exprs, **named_exprs).drop(_REQUIRED_COLUMNS, strict=False).collect(_eager=True)\n    return self.with_atoms(\n        Atoms(self._get_frame().select(_REQUIRED_COLUMNS).hstack(props), _unchecked=False)\n    )\n
    "},{"location":"api/#atomlib.Atoms.try_select","title":"try_select","text":"
    try_select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Optional[DataFrame]\n

    Try to select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns. Returns None if any columns are missing.

    Source code in atomlib/atoms.py
    def try_select(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr,\n) -> t.Optional[polars.DataFrame]:\n    \"\"\"\n    Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns. Returns `None` if any\n    columns are missing.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    try:\n        return self._get_frame().select(*exprs, **named_exprs)\n    except polars.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/#atomlib.Atoms.try_get_column","title":"try_get_column","text":"
    try_get_column(name: str) -> Optional[Series]\n

    Try to get a column from self, returning None if it doesn't exist.

    Source code in atomlib/atoms.py
    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n    \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n    try:\n        return self.get_column(name)\n    except polars.exceptions.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/#atomlib.Atoms.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n
    Source code in atomlib/atoms.py
    def assert_equal(self, other: t.Any):\n    assert isinstance(other, HasAtoms)\n    assert dict(self.schema) == dict(other.schema)\n    for col in self.schema.keys():\n        polars.testing.assert_series_equal(self[col], other[col], check_names=False, rtol=1e-3, atol=1e-8)\n
    "},{"location":"api/#atomlib.Atoms.bbox_atoms","title":"bbox_atoms","text":"
    bbox_atoms() -> BBox3D\n

    Return the bounding box of all the atoms in self.

    Source code in atomlib/atoms.py
    def bbox_atoms(self) -> BBox3D:\n    \"\"\"Return the bounding box of all the atoms in ``self``.\"\"\"\n    return BBox3D.from_pts(self.coords())\n
    "},{"location":"api/#atomlib.Atoms.transform_atoms","title":"transform_atoms","text":"
    transform_atoms(\n    transform: IntoTransform3D,\n    selection: Optional[AtomSelection] = None,\n    *,\n    transform_velocities: bool = False\n) -> Self\n

    Transform the atoms in self by transform. If selection is given, only transform the atoms in selection.

    Source code in atomlib/atoms.py
    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                    transform_velocities: bool = False) -> Self:\n    \"\"\"\n    Transform the atoms in `self` by `transform`.\n    If `selection` is given, only transform the atoms in `selection`.\n    \"\"\"\n    transform = Transform3D.make(transform)\n    selection = _selection_to_numpy(self, selection)\n    transformed = self.with_coords(Transform3D.make(transform) @ self.coords(selection), selection)\n    # try to transform velocities as well\n    if transform_velocities and (velocities := self.velocities(selection)) is not None:\n        return transformed.with_velocity(transform.transform_vec(velocities), selection)\n    return transformed\n
    "},{"location":"api/#atomlib.Atoms.round_near_zero","title":"round_near_zero","text":"
    round_near_zero(tol: float = 1e-14) -> Self\n

    Round atom position values near zero to zero.

    Source code in atomlib/atoms.py
    def round_near_zero(self, tol: float = 1e-14) -> Self:\n    \"\"\"\n    Round atom position values near zero to zero.\n    \"\"\"\n    return self.with_columns(coords=polars.concat_list(\n        polars.when(_coord_expr(col).abs() >= tol).then(_coord_expr(col)).otherwise(polars.lit(0.))\n        for col in range(3)\n    ).list.to_array(3))\n
    "},{"location":"api/#atomlib.Atoms.crop","title":"crop","text":"
    crop(\n    x_min: float = -numpy.inf,\n    x_max: float = numpy.inf,\n    y_min: float = -numpy.inf,\n    y_max: float = numpy.inf,\n    z_min: float = -numpy.inf,\n    z_max: float = numpy.inf,\n) -> Self\n

    Crop, removing all atoms outside of the specified region, inclusive.

    Source code in atomlib/atoms.py
    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf) -> Self:\n    \"\"\"\n    Crop, removing all atoms outside of the specified region, inclusive.\n    \"\"\"\n\n    return self.filter(\n        self.x().is_between(x_min, x_max, closed='both'),\n        self.y().is_between(y_min, y_max, closed='both'),\n        self.z().is_between(z_min, z_max, closed='both'),\n    )\n
    "},{"location":"api/#atomlib.Atoms.deduplicate","title":"deduplicate","text":"
    deduplicate(\n    tol: float = 0.001,\n    subset: Iterable[str] = (\"x\", \"y\", \"z\", \"symbol\"),\n    keep: UniqueKeepStrategy = \"first\",\n    maintain_order: bool = True,\n) -> Self\n

    De-duplicate atoms in self. Atoms of the same symbol that are closer than tolerance to each other (by Euclidian distance) will be removed, leaving only the atom specified by keep (defaults to the first atom).

    If subset is specified, only those columns will be included while assessing duplicates. Floating point columns other than 'x', 'y', and 'z' will not by toleranced.

    Source code in atomlib/atoms.py
    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n    \"\"\"\n    De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n    to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n    `keep` (defaults to the first atom).\n\n    If `subset` is specified, only those columns will be included while assessing duplicates.\n    Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n    \"\"\"\n    import scipy.spatial\n\n    cols = set((subset,) if isinstance(subset, str) else subset)\n\n    indices = numpy.arange(len(self))\n\n    spatial_cols = cols.intersection(('x', 'y', 'z'))\n    cols -= spatial_cols\n    if len(spatial_cols) > 0:\n        coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n        tree = scipy.spatial.KDTree(coords)\n\n        # TODO This is a bad algorithm\n        while True:\n            changed = False\n            for (i, j) in tree.query_pairs(tol, 2.):\n                # whenever we encounter a pair, ensure their index matches\n                i_i, i_j = indices[[i, j]]\n                if i_i != i_j:\n                    indices[i] = indices[j] = min(i_i, i_j)\n                    changed = True\n            if not changed:\n                break\n\n        self = self.with_column(polars.Series('_unique_pts', indices))\n        cols.add('_unique_pts')\n\n    frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n    if len(spatial_cols) > 0:\n        frame = frame.drop('_unique_pts')\n\n    return self.with_atoms(Atoms(frame, _unchecked=True))\n
    "},{"location":"api/#atomlib.Atoms.with_bounds","title":"with_bounds","text":"
    with_bounds(\n    cell_size: Optional[VecLike] = None,\n    cell_origin: Optional[VecLike] = None,\n) -> \"AtomCell\"\n

    Return a periodic cell with the given orthogonal cell dimensions.

    If cell_size is not specified, it will be assumed (and may be incorrect).

    Source code in atomlib/atoms.py
    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n    \"\"\"\n    Return a periodic cell with the given orthogonal cell dimensions.\n\n    If cell_size is not specified, it will be assumed (and may be incorrect).\n    \"\"\"\n    # TODO: test this\n    from .atomcell import AtomCell\n\n    if cell_size is None:\n        warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n        cell_size = self.bbox().size\n        cell_origin = self.bbox().min\n\n    # TODO test this origin code\n    cell = Cell.from_unit_cell(cell_size)\n    if cell_origin is not None:\n        cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n    return AtomCell(self.get_atoms(), cell, frame='local')\n
    "},{"location":"api/#atomlib.Atoms.coords","title":"coords","text":"
    coords(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Literal[\"local\"] = \"local\"\n) -> NDArray[float64]\n

    Return a (N, 3) ndarray of atom coordinates (dtype numpy.float64).

    Source code in atomlib/atoms.py
    def coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> NDArray[numpy.float64]:\n    \"\"\"Return a `(N, 3)` ndarray of atom coordinates (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n    df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n    return df.get_column('coords').to_numpy().astype(numpy.float64)\n
    "},{"location":"api/#atomlib.Atoms.x","title":"x","text":"
    x() -> Expr\n
    Source code in atomlib/atoms.py
    def x(self) -> polars.Expr:\n    return polars.col('coords').arr.get(0).alias('x')\n
    "},{"location":"api/#atomlib.Atoms.y","title":"y","text":"
    y() -> Expr\n
    Source code in atomlib/atoms.py
    def y(self) -> polars.Expr:\n    return polars.col('coords').arr.get(1).alias('y')\n
    "},{"location":"api/#atomlib.Atoms.z","title":"z","text":"
    z() -> Expr\n
    Source code in atomlib/atoms.py
    def z(self) -> polars.Expr:\n    return polars.col('coords').arr.get(2).alias('z')\n
    "},{"location":"api/#atomlib.Atoms.velocities","title":"velocities","text":"
    velocities(\n    selection: Optional[AtomSelection] = None,\n) -> Optional[NDArray[float64]]\n

    Return a (N, 3) ndarray of atom velocities (dtype numpy.float64).

    Source code in atomlib/atoms.py
    def velocities(self, selection: t.Optional[AtomSelection] = None) -> t.Optional[NDArray[numpy.float64]]:\n    \"\"\"Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n    if 'velocity' not in self:\n        return None\n\n    df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n    return df.get_column('velocity').to_numpy().astype(numpy.float64)\n
    "},{"location":"api/#atomlib.Atoms.types","title":"types","text":"
    types() -> Optional[Series]\n

    Returns a Series of atom types (dtype polars.Int32).

    Source code in atomlib/atoms.py
    def types(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('type')\n
    "},{"location":"api/#atomlib.Atoms.masses","title":"masses","text":"
    masses() -> Optional[Series]\n

    Returns a Series of atom masses (dtype polars.Float32).

    Source code in atomlib/atoms.py
    def masses(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('mass')\n
    "},{"location":"api/#atomlib.Atoms.add_atom","title":"add_atom","text":"
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: Union[ArrayLike, float],\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    **kwargs: Any,\n) -> Self\n

    Return a copy of self with an extra atom.

    By default, all extra columns present in self must be specified as **kwargs.

    Try to avoid calling this in a loop (Use HasAtoms.concat instead).

    Source code in atomlib/atoms.py
    def add_atom(self, elem: t.Union[int, str], /,\n             x: t.Union[ArrayLike, float],\n             y: t.Optional[float] = None,\n             z: t.Optional[float] = None,\n             **kwargs: t.Any) -> Self:\n    \"\"\"\n    Return a copy of `self` with an extra atom.\n\n    By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n    Try to avoid calling this in a loop (Use [`HasAtoms.concat`][atomlib.atoms.HasAtoms.concat] instead).\n    \"\"\"\n    if isinstance(elem, int):\n        kwargs.update(elem=elem)\n    else:\n        kwargs.update(symbol=elem)\n    if hasattr(x, '__len__') and len(x) > 1:  # type: ignore\n        (x, y, z) = to_vec3(x)\n    elif y is None or z is None:\n        raise ValueError(\"Must specify vector of positions or x, y, & z.\")\n\n    sym = get_sym(elem) if isinstance(elem, int) else elem\n    d: t.Dict[str, t.Any] = {'x': x, 'y': y, 'z': z, 'symbol': sym, **kwargs}\n    return self.concat(\n        (self, Atoms(d).select_schema(self.schema)),\n        how='vertical'\n    )\n
    "},{"location":"api/#atomlib.Atoms.pos","title":"pos","text":"
    pos(\n    x: Union[Sequence[Optional[float]], float, None] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n

    Select all atoms at a given position.

    Formally, returns all atoms within a cube of radius tol centered at (x,y,z), exclusive of the cube's surface.

    Additional parameters given as kwargs will be checked as additional parameters (with strict equality).

    Source code in atomlib/atoms.py
    def pos(self,\n        x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n        y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n        tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n    \"\"\"\n    Select all atoms at a given position.\n\n    Formally, returns all atoms within a cube of radius ``tol``\n    centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n    Additional parameters given as ``kwargs`` will be checked\n    as additional parameters (with strict equality).\n    \"\"\"\n\n    if isinstance(x, t.Sequence):\n        (x, y, z) = x\n\n    tol = abs(float(tol))\n    selection = polars.lit(True)\n    if x is not None:\n        selection &= self.x().is_between(x - tol, x + tol, closed='none')\n    if y is not None:\n        selection &= self.y().is_between(y - tol, y + tol, closed='none')\n    if z is not None:\n        selection &= self.z().is_between(z - tol, z + tol, closed='none')\n    for (col, val) in kwargs.items():\n        selection &= (polars.col(col) == val)\n\n    return selection\n
    "},{"location":"api/#atomlib.Atoms.with_index","title":"with_index","text":"
    with_index(index: Optional[AtomValues] = None) -> Self\n

    Returns self with a row index added in column 'i' (dtype polars.Int64). If index is not specified, defaults to an existing index or a new index.

    Source code in atomlib/atoms.py
    def with_index(self, index: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n    If `index` is not specified, defaults to an existing index or a new index.\n    \"\"\"\n    if index is None and 'i' in self.columns:\n        return self\n    if index is None:\n        index = numpy.arange(len(self), dtype=numpy.int64)\n    return self.with_column(_values_to_expr(self, index, polars.Int64).alias('i'))\n
    "},{"location":"api/#atomlib.Atoms.with_wobble","title":"with_wobble","text":"
    with_wobble(wobble: Optional[AtomValues] = None) -> Self\n

    Return self with the given displacements in column 'wobble' (dtype polars.Float64). If wobble is not specified, defaults to the already-existing wobbles or 0.

    Source code in atomlib/atoms.py
    def with_wobble(self, wobble: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n    \"\"\"\n    if wobble is None and 'wobble' in self.columns:\n        return self\n    wobble = 0. if wobble is None else wobble\n    return self.with_column(_values_to_expr(self, wobble, polars.Float64).alias('wobble'))\n
    "},{"location":"api/#atomlib.Atoms.with_occupancy","title":"with_occupancy","text":"
    with_occupancy(\n    frac_occupancy: Optional[AtomValues] = None,\n) -> Self\n

    Return self with the given fractional occupancies (dtype polars.Float64). If frac_occupancy is not specified, defaults to the already-existing occupancies or 1.

    Source code in atomlib/atoms.py
    def with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n    \"\"\"\n    if frac_occupancy is None and 'frac_occupancy' in self.columns:\n        return self\n    frac_occupancy = 1. if frac_occupancy is None else frac_occupancy\n    return self.with_column(_values_to_expr(self, frac_occupancy, polars.Float64).alias('frac_occupancy'))\n
    "},{"location":"api/#atomlib.Atoms.apply_wobble","title":"apply_wobble","text":"
    apply_wobble(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    Displace the atoms in self by the amount in the wobble column. wobble is interpretated as a mean-squared displacement, which is distributed equally over each axis.

    Source code in atomlib/atoms.py
    def apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    Displace the atoms in `self` by the amount in the `wobble` column.\n    `wobble` is interpretated as a mean-squared displacement, which is distributed\n    equally over each axis.\n    \"\"\"\n    if 'wobble' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    stddev = self.select((polars.col('wobble') / 3.).sqrt()).to_series().to_numpy()\n    coords = self.coords()\n    coords += stddev[:, None] * rng.standard_normal(coords.shape)\n    return self.with_coords(coords)\n
    "},{"location":"api/#atomlib.Atoms.apply_occupancy","title":"apply_occupancy","text":"
    apply_occupancy(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    For each atom in self, use its frac_occupancy to randomly decide whether to remove it.

    Source code in atomlib/atoms.py
    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n    \"\"\"\n    if 'frac_occupancy' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    frac = self.select('frac_occupancy').to_series().to_numpy()\n    choice = rng.binomial(1, frac).astype(numpy.bool_)\n    return self.filter(polars.lit(choice))\n
    "},{"location":"api/#atomlib.Atoms.with_type","title":"with_type","text":"
    with_type(types: Optional[AtomValues] = None) -> Self\n

    Return self with the given atom types in column 'type'. If types is not specified, use the already existing types or auto-assign them.

    When auto-assigning, each symbol is given a unique value, case-sensitive. Values are assigned from lowest atomic number to highest. For instance: [\"Ag+\", \"Na\", \"H\", \"Ag\"] => [3, 11, 1, 2]

    Source code in atomlib/atoms.py
    def with_type(self, types: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom types in column 'type'.\n    If `types` is not specified, use the already existing types or auto-assign them.\n\n    When auto-assigning, each symbol is given a unique value, case-sensitive.\n    Values are assigned from lowest atomic number to highest.\n    For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n    \"\"\"\n    if types is not None:\n        return self.with_columns(type=_values_to_expr(self, types, polars.Int32))\n    if 'type' in self.columns:\n        return self\n\n    unique = Atoms(self._get_frame().unique(maintain_order=False, subset=['elem', 'symbol']).sort(['elem', 'symbol']), _unchecked=True)\n    new = self.with_column(polars.Series('type', values=numpy.zeros(len(self)), dtype=polars.Int32))\n\n    logging.warning(\"Auto-assigning element types\")\n    for (i, (elem, sym)) in enumerate(unique.select(('elem', 'symbol')).rows()):\n        print(f\"Assigning type {i+1} to element '{sym}'\")\n        new = new.with_column(polars.when((polars.col('elem') == elem) & (polars.col('symbol') == sym))\n                                    .then(polars.lit(i+1))\n                                    .otherwise(polars.col('type'))\n                                    .alias('type'))\n\n    assert (new.get_column('type') == 0).sum() == 0\n    return new\n
    "},{"location":"api/#atomlib.Atoms.with_mass","title":"with_mass","text":"
    with_mass(mass: Optional[ArrayLike] = None) -> Self\n

    Return self with the given atom masses in column 'mass'. If mass is not specified, use the already existing masses or auto-assign them.

    Source code in atomlib/atoms.py
    def with_mass(self, mass: t.Optional[ArrayLike] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom masses in column `'mass'`.\n    If `mass` is not specified, use the already existing masses or auto-assign them.\n    \"\"\"\n    if mass is not None:\n        return self.with_column(_values_to_expr(self, mass, polars.Float32).alias('mass'))\n    if 'mass' in self.columns:\n        return self\n\n    unique_elems = self.get_column('elem').unique()\n    new = self.with_column(polars.Series('mass', values=numpy.zeros(len(self)), dtype=polars.Float32))\n\n    logging.warning(\"Auto-assigning element masses\")\n    for elem in unique_elems:\n        new = new.with_column(polars.when(polars.col('elem') == elem)\n                                    .then(polars.lit(get_mass(elem)))\n                                    .otherwise(polars.col('mass'))\n                                    .alias('mass'))\n\n    assert (new.get_column('mass').abs() < 1e-10).sum() == 0\n    return new\n
    "},{"location":"api/#atomlib.Atoms.with_symbol","title":"with_symbol","text":"
    with_symbol(\n    symbols: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n) -> Self\n

    Return self with the given atomic symbols.

    Source code in atomlib/atoms.py
    def with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atomic symbols.\n    \"\"\"\n    if selection is not None:\n        selection = _selection_to_numpy(self, selection)\n        new_symbols = self.get_column('symbol')\n        new_symbols[selection] = polars.Series(list(numpy.broadcast_to(symbols, len(selection))), dtype=polars.Utf8)\n        symbols = new_symbols\n\n    # TODO better cast here\n    symbols = polars.Series('symbol', list(numpy.broadcast_to(symbols, len(self))), dtype=polars.Utf8)\n    return self.with_columns((symbols, get_elem(symbols)))\n
    "},{"location":"api/#atomlib.Atoms.with_coords","title":"with_coords","text":"
    with_coords(\n    pts: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Literal[\"local\"] = \"local\"\n) -> Self\n

    Return self replaced with the given atomic positions.

    Source code in atomlib/atoms.py
    def with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic positions.\n    \"\"\"\n    if selection is not None:\n        selection = _selection_to_numpy(self, selection)\n        new_pts = self.coords()\n        pts = numpy.atleast_2d(pts)\n        assert pts.shape[-1] == 3\n        new_pts[selection] = pts\n        pts = new_pts\n\n    # https://github.com/pola-rs/polars/issues/18369\n    pts = numpy.broadcast_to(pts, (len(self), 3)) if len(self) else []\n    return self.with_columns(polars.Series('coords', pts, polars.Array(polars.Float64, 3)))\n
    "},{"location":"api/#atomlib.Atoms.with_velocity","title":"with_velocity","text":"
    with_velocity(\n    pts: Optional[ArrayLike] = None,\n    selection: Optional[AtomSelection] = None,\n) -> Self\n

    Return self replaced with the given atomic velocities. If pts is not specified, use the already existing velocities or zero.

    Source code in atomlib/atoms.py
    def with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                  selection: t.Optional[AtomSelection] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic velocities.\n    If `pts` is not specified, use the already existing velocities or zero.\n    \"\"\"\n    if pts is None:\n        if 'velocity' in self:\n            return self\n        all_pts = numpy.zeros((len(self), 3))\n    else:\n        all_pts = self['velocity'].to_numpy()\n\n    if selection is None:\n        all_pts = pts or all_pts\n    elif pts is not None:\n        selection = _selection_to_numpy(self, selection)\n        all_pts = numpy.require(all_pts, requirements=['WRITEABLE'])\n        pts = numpy.atleast_2d(pts)\n        assert pts.shape[-1] == 3\n        all_pts[selection] = pts\n\n    all_pts = numpy.broadcast_to(all_pts, (len(self), 3))\n    return self.with_columns(polars.Series('velocity', all_pts, polars.Array(polars.Float64, 3)))\n
    "},{"location":"api/#atomlib.Atoms.read","title":"read classmethod","text":"
    read(\n    path: FileOrPath, ty: Optional[FileType] = None\n) -> HasAtomsT\n

    Read a structure from a file.

    Supported types can be found in the io module. If no ty is specified, it is inferred from the file's extension.

    Source code in atomlib/mixins.py
    @classmethod\ndef read(cls: t.Type[HasAtomsT], path: FileOrPath, ty: t.Optional[FileType] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a file.\n\n    Supported types can be found in the [io][atomlib.io] module.\n    If no `ty` is specified, it is inferred from the file's extension.\n    \"\"\"\n    from .io import read\n    return _cast_atoms(read(path, ty), cls)  # type: ignore\n
    "},{"location":"api/#atomlib.Atoms.read_cif","title":"read_cif classmethod","text":"
    read_cif(\n    f: Union[FileOrPath, CIF, CIFDataBlock],\n    block: Union[int, str, None] = None,\n) -> HasAtomsT\n

    Read a structure from a CIF file.

    If block is specified, read data from the given block of the CIF file (index or name).

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cif(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CIF, CIFDataBlock], block: t.Union[int, str, None] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a CIF file.\n\n    If `block` is specified, read data from the given block of the CIF file (index or name).\n    \"\"\"\n    from .io import read_cif\n    return _cast_atoms(read_cif(f, block), cls)\n
    "},{"location":"api/#atomlib.Atoms.read_xyz","title":"read_xyz classmethod","text":"
    read_xyz(f: Union[FileOrPath, XYZ]) -> HasAtomsT\n

    Read a structure from an XYZ file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xyz(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XYZ]) -> HasAtomsT:\n    \"\"\"Read a structure from an XYZ file.\"\"\"\n    from .io import read_xyz\n    return _cast_atoms(read_xyz(f), cls)\n
    "},{"location":"api/#atomlib.Atoms.read_xsf","title":"read_xsf classmethod","text":"
    read_xsf(f: Union[FileOrPath, XSF]) -> HasAtomsT\n

    Read a structure from an XSF file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xsf(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XSF]) -> HasAtomsT:\n    \"\"\"Read a structure from an XSF file.\"\"\"\n    from .io import read_xsf\n    return _cast_atoms(read_xsf(f), cls)\n
    "},{"location":"api/#atomlib.Atoms.read_cfg","title":"read_cfg classmethod","text":"
    read_cfg(f: Union[FileOrPath, CFG]) -> HasAtomsT\n

    Read a structure from a CFG file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cfg(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CFG]) -> HasAtomsT:\n    \"\"\"Read a structure from a CFG file.\"\"\"\n    from .io import read_cfg\n    return _cast_atoms(read_cfg(f), cls)\n
    "},{"location":"api/#atomlib.Atoms.read_lmp","title":"read_lmp classmethod","text":"
    read_lmp(\n    f: Union[FileOrPath, LMP],\n    type_map: Optional[Dict[int, Union[str, int]]] = None,\n) -> HasAtomsT\n

    Read a structure from a LAAMPS data file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_lmp(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, LMP], type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> HasAtomsT:\n    \"\"\"Read a structure from a LAAMPS data file.\"\"\"\n    from .io import read_lmp\n    return _cast_atoms(read_lmp(f, type_map=type_map), cls)\n
    "},{"location":"api/#atomlib.Atoms.write_cif","title":"write_cif","text":"
    write_cif(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cif(self, f: FileOrPath):\n    from .io import write_cif\n    write_cif(self, f)\n
    "},{"location":"api/#atomlib.Atoms.write_xyz","title":"write_xyz","text":"
    write_xyz(f: FileOrPath, fmt: XYZFormat = 'exyz')\n
    Source code in atomlib/mixins.py
    def write_xyz(self, f: FileOrPath, fmt: XYZFormat = 'exyz'):\n    from .io import write_xyz\n    write_xyz(self, f, fmt)\n
    "},{"location":"api/#atomlib.Atoms.write_xsf","title":"write_xsf","text":"
    write_xsf(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_xsf(self, f: FileOrPath):\n    from .io import write_xsf\n    write_xsf(self, f)\n
    "},{"location":"api/#atomlib.Atoms.write_cfg","title":"write_cfg","text":"
    write_cfg(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cfg(self, f: FileOrPath):\n    from .io import write_cfg\n    write_cfg(self, f)\n
    "},{"location":"api/#atomlib.Atoms.write_lmp","title":"write_lmp","text":"
    write_lmp(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_lmp(self, f: FileOrPath):\n    from .io import write_lmp\n    write_lmp(self, f)\n
    "},{"location":"api/#atomlib.Atoms.write","title":"write","text":"
    write(path: FileOrPath, ty: Optional[FileType] = None)\n

    Write this structure to a file.

    A file type may be specified using ty. If no ty is specified, it is inferred from the path's extension.

    Source code in atomlib/mixins.py
    def write(self, path: FileOrPath, ty: t.Optional[FileType] = None):\n    \"\"\"\n    Write this structure to a file.\n\n    A file type may be specified using `ty`.\n    If no `ty` is specified, it is inferred from the path's extension.\n    \"\"\"\n    from .io import write\n    write(self, path, ty)  # type: ignore\n
    "},{"location":"api/#atomlib.Atoms.empty","title":"empty staticmethod","text":"
    empty() -> Atoms\n

    Return an empty Atoms with only the mandatory columns.

    Source code in atomlib/atoms.py
    @staticmethod\ndef empty() -> Atoms:\n    \"\"\"\n    Return an empty Atoms with only the mandatory columns.\n    \"\"\"\n    return Atoms()\n
    "},{"location":"api/#atomlib.Atoms.get_atoms","title":"get_atoms","text":"
    get_atoms(frame: Literal['local'] = 'local') -> Atoms\n
    Source code in atomlib/atoms.py
    def get_atoms(self, frame: t.Literal['local'] = 'local') -> Atoms:\n    if frame != 'local':\n        raise ValueError(f\"Atoms without a cell only support the 'local' coordinate frame, not '{frame}'.\")\n    return self\n
    "},{"location":"api/#atomlib.Atoms.with_atoms","title":"with_atoms","text":"
    with_atoms(\n    atoms: HasAtoms, frame: Literal[\"local\"] = \"local\"\n) -> Atoms\n
    Source code in atomlib/atoms.py
    def with_atoms(self, atoms: HasAtoms, frame: t.Literal['local'] = 'local') -> Atoms:\n    if frame != 'local':\n        raise ValueError(f\"Atoms without a cell only support the 'local' coordinate frame, not '{frame}'.\")\n    return atoms.get_atoms()\n
    "},{"location":"api/#atomlib.Atoms.bbox","title":"bbox","text":"
    bbox() -> BBox3D\n

    Return the bounding box of all the points in self.

    Source code in atomlib/atoms.py
    def bbox(self) -> BBox3D:\n    \"\"\"Return the bounding box of all the points in `self`.\"\"\"\n    if self._bbox is None:\n        self._bbox = BBox3D.from_pts(self.coords())\n\n    return self._bbox\n
    "},{"location":"api/#atomlib.HasAtoms","title":"HasAtoms","text":"

    Bases: ABC

    Abstract class representing any (possibly compound) collection of atoms.

    Source code in atomlib/atoms.py
    class HasAtoms(abc.ABC):\n    \"\"\"Abstract class representing any (possibly compound) collection of atoms.\"\"\"\n\n    # abstract methods\n\n    @abc.abstractmethod\n    def get_atoms(self, frame: t.Literal['local'] = 'local') -> Atoms:\n        \"\"\"\n        Get atoms contained in `self`. This should be a low cost method.\n\n        Args:\n          frame: Coordinate frame to return atoms in. For a plain [`HasAtoms`][atomlib.atoms.HasAtoms],\n                 only `'local'` is supported.\n\n        Return:\n          The contained atoms\n        \"\"\"\n        ...\n\n    @abc.abstractmethod\n    def with_atoms(self, atoms: HasAtoms, frame: t.Literal['local'] = 'local') -> Self:\n        \"\"\"\n        Return a copy of self with the inner [`Atoms`][atomlib.atoms.Atoms] replaced.\n\n        Args:\n          atoms: [`HasAtoms`][atomlib.atoms.HasAtoms] to replace these with.\n          frame: Coordinate frame inside atoms are in. For a plain [`HasAtoms`][atomlib.atoms.HasAtoms],\n                 only `'local'` is supported.\n\n        Return:\n          A copy of `self` updated with the given atoms\n        \"\"\"\n        ...\n\n    @classmethod\n    @abc.abstractmethod\n    def _combine_metadata(cls: t.Type[HasAtomsT], *atoms: HasAtoms) -> HasAtomsT:\n        \"\"\"\n        When combining multiple `HasAtoms`, check that they are compatible with each other,\n        and return a 'representative' which best represents the combined metadata.\n        Implementors should treat `Atoms` as acceptable, but having no metadata.\n        \"\"\"\n        ...\n\n    def _get_frame(self) -> polars.DataFrame:\n        return self.get_atoms().inner\n\n    # dataframe methods\n\n    @property\n    @_fwd_frame(lambda df: df.columns)\n    def columns(self) -> t.List[str]:\n        \"\"\"\n        Return the column names in `self`.\n\n        Returns:\n          A sequence of column names\n        \"\"\"\n        ...\n\n    @property\n    @_fwd_frame(lambda df: df.dtypes)\n    def dtypes(self) -> t.List[polars.DataType]:\n        \"\"\"\n        Return the datatypes in `self`.\n\n        Returns:\n          A sequence of column [`DataType`][polars.datatypes.DataType]s\n        \"\"\"\n        ...\n\n    @property\n    @_fwd_frame(lambda df: df.schema)  # type: ignore\n    def schema(self) -> Schema:\n        \"\"\"\n        Return the schema of `self`.\n\n        Returns:\n          A dictionary of column names and [`DataType`][polars.datatypes.DataType]s\n        \"\"\"\n        ...\n\n    @_fwd_frame(polars.DataFrame.describe)\n    def describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n                 interpolation: RollingInterpolationMethod = 'nearest') -> polars.DataFrame:\n        \"\"\"\n        Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n        Args:\n          percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                       50% (median), and 75% (third quartile).\n\n        Returns:\n          A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n        \"\"\"\n        ...\n\n    @_fwd_frame_map\n    def with_columns(self,\n                     *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                     **named_exprs: IntoExpr) -> polars.DataFrame:\n        \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n        return self._get_frame().with_columns(*exprs, **named_exprs)\n\n    with_column = with_columns\n\n    @_fwd_frame_map\n    def insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n        return self._get_frame().insert_column(index, column)\n\n    @_fwd_frame(lambda df, name: df.get_column(name))\n    def get_column(self, name: str) -> polars.Series:\n        \"\"\"\n        Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        ...\n\n    @_fwd_frame(polars.DataFrame.get_columns)\n    def get_columns(self) -> t.List[polars.Series]:\n        \"\"\"\n        Return all columns from `self` as a list of [`Series`][polars.Series].\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        ...\n\n    @_fwd_frame(polars.DataFrame.get_column_index)\n    def get_column_index(self, name: str) -> int:\n        \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n        ...\n\n    @_fwd_frame(polars.DataFrame.group_by)\n    def group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]], maintain_order: bool = False,\n                 **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n        \"\"\"\n        Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n        \"\"\"\n        ...\n\n    def pipe(self: HasAtomsT, function: t.Callable[Concatenate[HasAtomsT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n        \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n        return function(self, *args, **kwargs)\n\n    @_fwd_frame_map\n    def clone(self) -> polars.DataFrame:\n        \"\"\"Return a copy of `self`.\"\"\"\n        return self._get_frame().clone()\n\n    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n        \"\"\"Return `self` with the specified columns removed.\"\"\"\n        return self._get_frame().drop(*columns, strict=strict)\n\n    # row-wise operations\n\n    def filter(\n        self,\n        *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n        **constraints: t.Any,\n    ) -> Self:\n        \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n        # TODO clean up\n        preds_not_none = tuple(filter(lambda p: p is not None, predicates))\n        if not len(preds_not_none) and not len(constraints):\n            return self\n        return self.with_atoms(Atoms(self._get_frame().filter(*preds_not_none, **constraints), _unchecked=True))  # type: ignore\n\n    @_fwd_frame_map\n    def sort(\n        self,\n        by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        *more_by: IntoExpr,\n        descending: t.Union[bool, t.Sequence[bool]] = False,\n        nulls_last: bool = False,\n    ) -> polars.DataFrame:\n        \"\"\"\n        Sort the atoms in `self` by the given columns/expressions.\n        \"\"\"\n        return self._get_frame().sort(\n            by, *more_by, descending=descending, nulls_last=nulls_last\n        )\n\n    @_fwd_frame_map\n    def slice(self, offset: int, length: t.Optional[int] = None) -> polars.DataFrame:\n        \"\"\"Return a slice of the rows in `self`.\"\"\"\n        return self._get_frame().slice(offset, length)\n\n    @_fwd_frame_map\n    def head(self, n: int = 5) -> polars.DataFrame:\n        \"\"\"Return the first `n` rows of `self`.\"\"\"\n        return self._get_frame().head(n)\n\n    @_fwd_frame_map\n    def tail(self, n: int = 5) -> polars.DataFrame:\n        \"\"\"Return the last `n` rows of `self`.\"\"\"\n        return self._get_frame().tail(n)\n\n    @_fwd_frame_map\n    def drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n        \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n        return self._get_frame().drop_nulls(subset)\n\n    @_fwd_frame_map\n    def fill_null(\n        self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n        limit: t.Optional[int] = None, matches_supertype: bool = True,\n    ) -> polars.DataFrame:\n        \"\"\"Fill null values in `self`, using the specified value or strategy.\"\"\"\n        return self._get_frame().fill_null(value, strategy, limit, matches_supertype=matches_supertype)\n\n    @_fwd_frame_map\n    def fill_nan(self, value: t.Union[polars.Expr, int, float, None]) -> polars.DataFrame:\n        \"\"\"Fill floating-point NaN values in `self`.\"\"\"\n        return self._get_frame().fill_nan(value)\n\n    @classmethod\n    def concat(cls: t.Type[HasAtomsT],\n               atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n               rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n        \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n        # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n        # same type as ``cls``.\n        if _is_abstract(cls):\n            raise TypeError(\"concat() must be called on a concrete class.\")\n\n        if isinstance(atoms, HasAtoms):\n            atoms = (atoms,)\n        dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n        representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n        if len(dfs) == 0:\n            return representative.with_atoms(Atoms.empty(), 'local')\n\n        if how in ('vertical', 'vertical_relaxed'):\n            # get order from first member\n            cols = dfs[0].columns\n            dfs = [df.select(cols) for df in dfs]\n        elif how == 'inner':\n            cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n            schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n            if len(schema) == 0:\n                raise ValueError(\"Atoms have no columns in common\")\n\n            dfs = [_select_schema(df, schema) for df in dfs]\n            how = 'vertical'\n\n        return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n\n    @t.overload\n    def partition_by(\n        self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n        maintain_order: bool = True, include_key: bool = True, as_dict: t.Literal[False] = False\n    ) -> t.List[Self]:\n        ...\n\n    @t.overload\n    def partition_by(\n        self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n        maintain_order: bool = True, include_key: bool = True, as_dict: t.Literal[True] = ...\n    ) -> t.Dict[t.Any, Self]:\n        ...\n\n    def partition_by(\n        self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n        maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n    ) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n        \"\"\"\n        Group by the given columns and partition into separate dataframes.\n\n        Return the partitions as a dictionary by specifying `as_dict=True`.\n        \"\"\"\n        if as_dict:\n            d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n            return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n        return [\n            self.with_atoms(Atoms(df, _unchecked=True))\n            for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n        ]\n\n    # column-wise operations\n\n    @_fwd_frame(polars.DataFrame.select)\n    def select(\n        self,\n        *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        **named_exprs: IntoExpr,\n    ) -> polars.DataFrame:\n        \"\"\"\n        Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n        Expressions may either be columns or expressions of columns.\n\n        [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n        \"\"\"\n        ...\n\n    # some helpers we add\n\n    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n        \"\"\"\n        Select columns from `self` and cast to the given schema.\n        Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n        \"\"\"\n        return _select_schema(self, schema)\n\n    def select_props(\n        self,\n        *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        **named_exprs: IntoExpr\n    ) -> Self:\n        \"\"\"\n        Select `exprs` from `self`, while keeping required columns.\n\n        Returns:\n          A [`HasAtoms`][atomlib.atoms.HasAtoms] filtered to contain the\n          specified properties (as well as required columns).\n        \"\"\"\n        props = self._get_frame().lazy().select(*exprs, **named_exprs).drop(_REQUIRED_COLUMNS, strict=False).collect(_eager=True)\n        return self.with_atoms(\n            Atoms(self._get_frame().select(_REQUIRED_COLUMNS).hstack(props), _unchecked=False)\n        )\n\n    def try_select(\n        self,\n        *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        **named_exprs: IntoExpr,\n    ) -> t.Optional[polars.DataFrame]:\n        \"\"\"\n        Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n        Expressions may either be columns or expressions of columns. Returns `None` if any\n        columns are missing.\n\n        [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n        \"\"\"\n        try:\n            return self._get_frame().select(*exprs, **named_exprs)\n        except polars.ColumnNotFoundError:\n            return None\n\n    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n        \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n        try:\n            return self.get_column(name)\n        except polars.exceptions.ColumnNotFoundError:\n            return None\n\n    def assert_equal(self, other: t.Any):\n        assert isinstance(other, HasAtoms)\n        assert dict(self.schema) == dict(other.schema)\n        for col in self.schema.keys():\n            polars.testing.assert_series_equal(self[col], other[col], check_names=False, rtol=1e-3, atol=1e-8)\n\n    # dunders\n\n    @_fwd_frame(polars.DataFrame.__len__)\n    def __len__(self) -> int:\n        \"\"\"Return the number of atoms in `self`.\"\"\"\n        ...\n\n    @_fwd_frame(polars.DataFrame.__contains__)\n    def __contains__(self, key: str) -> bool:\n        \"\"\"Return whether `self` contains the given column.\"\"\"\n        ...\n\n    def __add__(self, other: IntoAtoms) -> HasAtoms:\n        return self.__class__.concat((self, other), how='inner')\n\n    def __radd__(self, other: IntoAtoms) -> HasAtoms:\n        return self.__class__.concat((other, self), how='inner')\n\n    def __getitem__(self, column: str) -> polars.Series:\n        try:\n            return self.get_column(column)\n        except polars.exceptions.ColumnNotFoundError:\n            if column in ('x', 'y', 'z'):\n                return self.select(_coord_expr(column)).to_series()\n            raise\n\n    @_fwd_frame(polars.DataFrame.__dataframe__)\n    def __dataframe__(self, nan_as_null: bool = False, allow_copy: bool = True) -> polars.interchange.dataframe.PolarsDataFrame:\n        ...\n\n    # atoms-specific methods\n\n    def bbox_atoms(self) -> BBox3D:\n        \"\"\"Return the bounding box of all the atoms in ``self``.\"\"\"\n        return BBox3D.from_pts(self.coords())\n\n    bbox = bbox_atoms\n\n    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                        transform_velocities: bool = False) -> Self:\n        \"\"\"\n        Transform the atoms in `self` by `transform`.\n        If `selection` is given, only transform the atoms in `selection`.\n        \"\"\"\n        transform = Transform3D.make(transform)\n        selection = _selection_to_numpy(self, selection)\n        transformed = self.with_coords(Transform3D.make(transform) @ self.coords(selection), selection)\n        # try to transform velocities as well\n        if transform_velocities and (velocities := self.velocities(selection)) is not None:\n            return transformed.with_velocity(transform.transform_vec(velocities), selection)\n        return transformed\n\n    transform = transform_atoms\n\n    def round_near_zero(self, tol: float = 1e-14) -> Self:\n        \"\"\"\n        Round atom position values near zero to zero.\n        \"\"\"\n        return self.with_columns(coords=polars.concat_list(\n            polars.when(_coord_expr(col).abs() >= tol).then(_coord_expr(col)).otherwise(polars.lit(0.))\n            for col in range(3)\n        ).list.to_array(3))\n\n    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n             y_min: float = -numpy.inf, y_max: float = numpy.inf,\n             z_min: float = -numpy.inf, z_max: float = numpy.inf) -> Self:\n        \"\"\"\n        Crop, removing all atoms outside of the specified region, inclusive.\n        \"\"\"\n\n        return self.filter(\n            self.x().is_between(x_min, x_max, closed='both'),\n            self.y().is_between(y_min, y_max, closed='both'),\n            self.z().is_between(z_min, z_max, closed='both'),\n        )\n\n    crop_atoms = crop\n\n    def _wrap(self, eps: float = 1e-5) -> Self:\n        coords = (self.coords() + eps) % 1. - eps\n        return self.with_coords(coords)\n\n    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                    keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n        \"\"\"\n        De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n        to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n        `keep` (defaults to the first atom).\n\n        If `subset` is specified, only those columns will be included while assessing duplicates.\n        Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n        \"\"\"\n        import scipy.spatial\n\n        cols = set((subset,) if isinstance(subset, str) else subset)\n\n        indices = numpy.arange(len(self))\n\n        spatial_cols = cols.intersection(('x', 'y', 'z'))\n        cols -= spatial_cols\n        if len(spatial_cols) > 0:\n            coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n            tree = scipy.spatial.KDTree(coords)\n\n            # TODO This is a bad algorithm\n            while True:\n                changed = False\n                for (i, j) in tree.query_pairs(tol, 2.):\n                    # whenever we encounter a pair, ensure their index matches\n                    i_i, i_j = indices[[i, j]]\n                    if i_i != i_j:\n                        indices[i] = indices[j] = min(i_i, i_j)\n                        changed = True\n                if not changed:\n                    break\n\n            self = self.with_column(polars.Series('_unique_pts', indices))\n            cols.add('_unique_pts')\n\n        frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n        if len(spatial_cols) > 0:\n            frame = frame.drop('_unique_pts')\n\n        return self.with_atoms(Atoms(frame, _unchecked=True))\n\n    unique = deduplicate\n\n    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n        \"\"\"\n        Return a periodic cell with the given orthogonal cell dimensions.\n\n        If cell_size is not specified, it will be assumed (and may be incorrect).\n        \"\"\"\n        # TODO: test this\n        from .atomcell import AtomCell\n\n        if cell_size is None:\n            warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n            cell_size = self.bbox().size\n            cell_origin = self.bbox().min\n\n        # TODO test this origin code\n        cell = Cell.from_unit_cell(cell_size)\n        if cell_origin is not None:\n            cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n        return AtomCell(self.get_atoms(), cell, frame='local')\n\n    # property getters and setters\n\n    def coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> NDArray[numpy.float64]:\n        \"\"\"Return a `(N, 3)` ndarray of atom coordinates (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n        df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n        return df.get_column('coords').to_numpy().astype(numpy.float64)\n\n    def x(self) -> polars.Expr:\n        return polars.col('coords').arr.get(0).alias('x')\n\n    def y(self) -> polars.Expr:\n        return polars.col('coords').arr.get(1).alias('y')\n\n    def z(self) -> polars.Expr:\n        return polars.col('coords').arr.get(2).alias('z')\n\n    def velocities(self, selection: t.Optional[AtomSelection] = None) -> t.Optional[NDArray[numpy.float64]]:\n        \"\"\"Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n        if 'velocity' not in self:\n            return None\n\n        df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n        return df.get_column('velocity').to_numpy().astype(numpy.float64)\n\n    def types(self) -> t.Optional[polars.Series]:\n        \"\"\"\n        Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        return self.try_get_column('type')\n\n    def masses(self) -> t.Optional[polars.Series]:\n        \"\"\"\n        Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        return self.try_get_column('mass')\n\n    @t.overload\n    def add_atom(self, elem: t.Union[int, str], x: ArrayLike, /, *,\n                 y: None = None, z: None = None,\n                 **kwargs: t.Any) -> Self:\n        ...\n\n    @t.overload\n    def add_atom(self, elem: t.Union[int, str], /,\n                 x: float, y: float, z: float,\n                 **kwargs: t.Any) -> Self:\n        ...\n\n    def add_atom(self, elem: t.Union[int, str], /,\n                 x: t.Union[ArrayLike, float],\n                 y: t.Optional[float] = None,\n                 z: t.Optional[float] = None,\n                 **kwargs: t.Any) -> Self:\n        \"\"\"\n        Return a copy of `self` with an extra atom.\n\n        By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n        Try to avoid calling this in a loop (Use [`HasAtoms.concat`][atomlib.atoms.HasAtoms.concat] instead).\n        \"\"\"\n        if isinstance(elem, int):\n            kwargs.update(elem=elem)\n        else:\n            kwargs.update(symbol=elem)\n        if hasattr(x, '__len__') and len(x) > 1:  # type: ignore\n            (x, y, z) = to_vec3(x)\n        elif y is None or z is None:\n            raise ValueError(\"Must specify vector of positions or x, y, & z.\")\n\n        sym = get_sym(elem) if isinstance(elem, int) else elem\n        d: t.Dict[str, t.Any] = {'x': x, 'y': y, 'z': z, 'symbol': sym, **kwargs}\n        return self.concat(\n            (self, Atoms(d).select_schema(self.schema)),\n            how='vertical'\n        )\n\n    @t.overload\n    def pos(self, x: t.Sequence[t.Optional[float]], /, *,\n            y: None = None, z: None = None,\n            tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n        ...\n\n    @t.overload\n    def pos(self, x: t.Optional[float] = None, y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n            tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n        ...\n\n    def pos(self,\n            x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n            y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n            tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n        \"\"\"\n        Select all atoms at a given position.\n\n        Formally, returns all atoms within a cube of radius ``tol``\n        centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n        Additional parameters given as ``kwargs`` will be checked\n        as additional parameters (with strict equality).\n        \"\"\"\n\n        if isinstance(x, t.Sequence):\n            (x, y, z) = x\n\n        tol = abs(float(tol))\n        selection = polars.lit(True)\n        if x is not None:\n            selection &= self.x().is_between(x - tol, x + tol, closed='none')\n        if y is not None:\n            selection &= self.y().is_between(y - tol, y + tol, closed='none')\n        if z is not None:\n            selection &= self.z().is_between(z - tol, z + tol, closed='none')\n        for (col, val) in kwargs.items():\n            selection &= (polars.col(col) == val)\n\n        return selection\n\n    def with_index(self, index: t.Optional[AtomValues] = None) -> Self:\n        \"\"\"\n        Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n        If `index` is not specified, defaults to an existing index or a new index.\n        \"\"\"\n        if index is None and 'i' in self.columns:\n            return self\n        if index is None:\n            index = numpy.arange(len(self), dtype=numpy.int64)\n        return self.with_column(_values_to_expr(self, index, polars.Int64).alias('i'))\n\n    def with_wobble(self, wobble: t.Optional[AtomValues] = None) -> Self:\n        \"\"\"\n        Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n        If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n        \"\"\"\n        if wobble is None and 'wobble' in self.columns:\n            return self\n        wobble = 0. if wobble is None else wobble\n        return self.with_column(_values_to_expr(self, wobble, polars.Float64).alias('wobble'))\n\n    def with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None) -> Self:\n        \"\"\"\n        Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n        If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n        \"\"\"\n        if frac_occupancy is None and 'frac_occupancy' in self.columns:\n            return self\n        frac_occupancy = 1. if frac_occupancy is None else frac_occupancy\n        return self.with_column(_values_to_expr(self, frac_occupancy, polars.Float64).alias('frac_occupancy'))\n\n    def apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n        \"\"\"\n        Displace the atoms in `self` by the amount in the `wobble` column.\n        `wobble` is interpretated as a mean-squared displacement, which is distributed\n        equally over each axis.\n        \"\"\"\n        if 'wobble' not in self.columns:\n            return self\n        rng = numpy.random.default_rng(seed=rng)\n\n        stddev = self.select((polars.col('wobble') / 3.).sqrt()).to_series().to_numpy()\n        coords = self.coords()\n        coords += stddev[:, None] * rng.standard_normal(coords.shape)\n        return self.with_coords(coords)\n\n    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n        \"\"\"\n        For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n        \"\"\"\n        if 'frac_occupancy' not in self.columns:\n            return self\n        rng = numpy.random.default_rng(seed=rng)\n\n        frac = self.select('frac_occupancy').to_series().to_numpy()\n        choice = rng.binomial(1, frac).astype(numpy.bool_)\n        return self.filter(polars.lit(choice))\n\n    def with_type(self, types: t.Optional[AtomValues] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atom types in column 'type'.\n        If `types` is not specified, use the already existing types or auto-assign them.\n\n        When auto-assigning, each symbol is given a unique value, case-sensitive.\n        Values are assigned from lowest atomic number to highest.\n        For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n        \"\"\"\n        if types is not None:\n            return self.with_columns(type=_values_to_expr(self, types, polars.Int32))\n        if 'type' in self.columns:\n            return self\n\n        unique = Atoms(self._get_frame().unique(maintain_order=False, subset=['elem', 'symbol']).sort(['elem', 'symbol']), _unchecked=True)\n        new = self.with_column(polars.Series('type', values=numpy.zeros(len(self)), dtype=polars.Int32))\n\n        logging.warning(\"Auto-assigning element types\")\n        for (i, (elem, sym)) in enumerate(unique.select(('elem', 'symbol')).rows()):\n            print(f\"Assigning type {i+1} to element '{sym}'\")\n            new = new.with_column(polars.when((polars.col('elem') == elem) & (polars.col('symbol') == sym))\n                                        .then(polars.lit(i+1))\n                                        .otherwise(polars.col('type'))\n                                        .alias('type'))\n\n        assert (new.get_column('type') == 0).sum() == 0\n        return new\n\n    def with_mass(self, mass: t.Optional[ArrayLike] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atom masses in column `'mass'`.\n        If `mass` is not specified, use the already existing masses or auto-assign them.\n        \"\"\"\n        if mass is not None:\n            return self.with_column(_values_to_expr(self, mass, polars.Float32).alias('mass'))\n        if 'mass' in self.columns:\n            return self\n\n        unique_elems = self.get_column('elem').unique()\n        new = self.with_column(polars.Series('mass', values=numpy.zeros(len(self)), dtype=polars.Float32))\n\n        logging.warning(\"Auto-assigning element masses\")\n        for elem in unique_elems:\n            new = new.with_column(polars.when(polars.col('elem') == elem)\n                                        .then(polars.lit(get_mass(elem)))\n                                        .otherwise(polars.col('mass'))\n                                        .alias('mass'))\n\n        assert (new.get_column('mass').abs() < 1e-10).sum() == 0\n        return new\n\n    def with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atomic symbols.\n        \"\"\"\n        if selection is not None:\n            selection = _selection_to_numpy(self, selection)\n            new_symbols = self.get_column('symbol')\n            new_symbols[selection] = polars.Series(list(numpy.broadcast_to(symbols, len(selection))), dtype=polars.Utf8)\n            symbols = new_symbols\n\n        # TODO better cast here\n        symbols = polars.Series('symbol', list(numpy.broadcast_to(symbols, len(self))), dtype=polars.Utf8)\n        return self.with_columns((symbols, get_elem(symbols)))\n\n    def with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> Self:\n        \"\"\"\n        Return `self` replaced with the given atomic positions.\n        \"\"\"\n        if selection is not None:\n            selection = _selection_to_numpy(self, selection)\n            new_pts = self.coords()\n            pts = numpy.atleast_2d(pts)\n            assert pts.shape[-1] == 3\n            new_pts[selection] = pts\n            pts = new_pts\n\n        # https://github.com/pola-rs/polars/issues/18369\n        pts = numpy.broadcast_to(pts, (len(self), 3)) if len(self) else []\n        return self.with_columns(polars.Series('coords', pts, polars.Array(polars.Float64, 3)))\n\n    def with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                      selection: t.Optional[AtomSelection] = None) -> Self:\n        \"\"\"\n        Return `self` replaced with the given atomic velocities.\n        If `pts` is not specified, use the already existing velocities or zero.\n        \"\"\"\n        if pts is None:\n            if 'velocity' in self:\n                return self\n            all_pts = numpy.zeros((len(self), 3))\n        else:\n            all_pts = self['velocity'].to_numpy()\n\n        if selection is None:\n            all_pts = pts or all_pts\n        elif pts is not None:\n            selection = _selection_to_numpy(self, selection)\n            all_pts = numpy.require(all_pts, requirements=['WRITEABLE'])\n            pts = numpy.atleast_2d(pts)\n            assert pts.shape[-1] == 3\n            all_pts[selection] = pts\n\n        all_pts = numpy.broadcast_to(all_pts, (len(self), 3))\n        return self.with_columns(polars.Series('velocity', all_pts, polars.Array(polars.Float64, 3)))\n
    "},{"location":"api/#atomlib.HasAtoms.columns","title":"columns property","text":"
    columns: List[str]\n

    Return the column names in self.

    RETURNS DESCRIPTION List[str]

    A sequence of column names

    "},{"location":"api/#atomlib.HasAtoms.dtypes","title":"dtypes property","text":"
    dtypes: List[DataType]\n

    Return the datatypes in self.

    RETURNS DESCRIPTION List[DataType]

    A sequence of column DataTypes

    "},{"location":"api/#atomlib.HasAtoms.schema","title":"schema property","text":"
    schema: Schema\n

    Return the schema of self.

    RETURNS DESCRIPTION Schema

    A dictionary of column names and DataTypes

    "},{"location":"api/#atomlib.HasAtoms.with_column","title":"with_column class-attribute instance-attribute","text":"
    with_column = with_columns\n
    "},{"location":"api/#atomlib.HasAtoms.bbox","title":"bbox class-attribute instance-attribute","text":"
    bbox = bbox_atoms\n
    "},{"location":"api/#atomlib.HasAtoms.transform","title":"transform class-attribute instance-attribute","text":"
    transform = transform_atoms\n
    "},{"location":"api/#atomlib.HasAtoms.crop_atoms","title":"crop_atoms class-attribute instance-attribute","text":"
    crop_atoms = crop\n
    "},{"location":"api/#atomlib.HasAtoms.unique","title":"unique class-attribute instance-attribute","text":"
    unique = deduplicate\n
    "},{"location":"api/#atomlib.HasAtoms.get_atoms","title":"get_atoms abstractmethod","text":"
    get_atoms(frame: Literal['local'] = 'local') -> Atoms\n

    Get atoms contained in self. This should be a low cost method.

    PARAMETER DESCRIPTION frame

    Coordinate frame to return atoms in. For a plain HasAtoms, only 'local' is supported.

    TYPE: Literal['local'] DEFAULT: 'local'

    Return

    The contained atoms

    Source code in atomlib/atoms.py
    @abc.abstractmethod\ndef get_atoms(self, frame: t.Literal['local'] = 'local') -> Atoms:\n    \"\"\"\n    Get atoms contained in `self`. This should be a low cost method.\n\n    Args:\n      frame: Coordinate frame to return atoms in. For a plain [`HasAtoms`][atomlib.atoms.HasAtoms],\n             only `'local'` is supported.\n\n    Return:\n      The contained atoms\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtoms.with_atoms","title":"with_atoms abstractmethod","text":"
    with_atoms(\n    atoms: HasAtoms, frame: Literal[\"local\"] = \"local\"\n) -> Self\n

    Return a copy of self with the inner Atoms replaced.

    PARAMETER DESCRIPTION atoms

    HasAtoms to replace these with.

    TYPE: HasAtoms

    frame

    Coordinate frame inside atoms are in. For a plain HasAtoms, only 'local' is supported.

    TYPE: Literal['local'] DEFAULT: 'local'

    Return

    A copy of self updated with the given atoms

    Source code in atomlib/atoms.py
    @abc.abstractmethod\ndef with_atoms(self, atoms: HasAtoms, frame: t.Literal['local'] = 'local') -> Self:\n    \"\"\"\n    Return a copy of self with the inner [`Atoms`][atomlib.atoms.Atoms] replaced.\n\n    Args:\n      atoms: [`HasAtoms`][atomlib.atoms.HasAtoms] to replace these with.\n      frame: Coordinate frame inside atoms are in. For a plain [`HasAtoms`][atomlib.atoms.HasAtoms],\n             only `'local'` is supported.\n\n    Return:\n      A copy of `self` updated with the given atoms\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtoms.describe","title":"describe","text":"
    describe(\n    percentiles: Union[Sequence[float], float, None] = (\n        0.25,\n        0.5,\n        0.75,\n    ),\n    *,\n    interpolation: RollingInterpolationMethod = \"nearest\"\n) -> DataFrame\n

    Return summary statistics for self. See DataFrame.describe for more information.

    PARAMETER DESCRIPTION percentiles

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), 50% (median), and 75% (third quartile).

    TYPE: Union[Sequence[float], float, None] DEFAULT: (0.25, 0.5, 0.75)

    RETURNS DESCRIPTION DataFrame

    A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.describe)\ndef describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n             interpolation: RollingInterpolationMethod = 'nearest') -> polars.DataFrame:\n    \"\"\"\n    Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n    Args:\n      percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                   50% (median), and 75% (third quartile).\n\n    Returns:\n      A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtoms.with_columns","title":"with_columns","text":"
    with_columns(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Return a copy of self with the given columns added.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef with_columns(self,\n                 *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 **named_exprs: IntoExpr) -> polars.DataFrame:\n    \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n    return self._get_frame().with_columns(*exprs, **named_exprs)\n
    "},{"location":"api/#atomlib.HasAtoms.insert_column","title":"insert_column","text":"
    insert_column(index: int, column: Series) -> DataFrame\n
    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n    return self._get_frame().insert_column(index, column)\n
    "},{"location":"api/#atomlib.HasAtoms.get_column","title":"get_column","text":"
    get_column(name: str) -> Series\n

    Get the specified column from self, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(lambda df, name: df.get_column(name))\ndef get_column(self, name: str) -> polars.Series:\n    \"\"\"\n    Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtoms.get_columns","title":"get_columns","text":"
    get_columns() -> List[Series]\n

    Return all columns from self as a list of Series.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_columns)\ndef get_columns(self) -> t.List[polars.Series]:\n    \"\"\"\n    Return all columns from `self` as a list of [`Series`][polars.Series].\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtoms.get_column_index","title":"get_column_index","text":"
    get_column_index(name: str) -> int\n

    Get the index of a column by name, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_column_index)\ndef get_column_index(self, name: str) -> int:\n    \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtoms.group_by","title":"group_by","text":"
    group_by(\n    *by: Union[IntoExpr, Iterable[IntoExpr]],\n    maintain_order: bool = False,\n    **named_by: IntoExpr\n) -> GroupBy\n

    Start a group by operation. See DataFrame.group_by for more information.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.group_by)\ndef group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]], maintain_order: bool = False,\n             **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n    \"\"\"\n    Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtoms.pipe","title":"pipe","text":"
    pipe(\n    function: Callable[Concatenate[HasAtomsT, P], T],\n    *args: args,\n    **kwargs: kwargs\n) -> T\n

    Apply function to self (in method-call syntax).

    Source code in atomlib/atoms.py
    def pipe(self: HasAtomsT, function: t.Callable[Concatenate[HasAtomsT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n    \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n    return function(self, *args, **kwargs)\n
    "},{"location":"api/#atomlib.HasAtoms.clone","title":"clone","text":"
    clone() -> DataFrame\n

    Return a copy of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef clone(self) -> polars.DataFrame:\n    \"\"\"Return a copy of `self`.\"\"\"\n    return self._get_frame().clone()\n
    "},{"location":"api/#atomlib.HasAtoms.drop","title":"drop","text":"
    drop(\n    *columns: Union[str, Iterable[str]], strict: bool = True\n) -> DataFrame\n

    Return self with the specified columns removed.

    Source code in atomlib/atoms.py
    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n    \"\"\"Return `self` with the specified columns removed.\"\"\"\n    return self._get_frame().drop(*columns, strict=strict)\n
    "},{"location":"api/#atomlib.HasAtoms.filter","title":"filter","text":"
    filter(\n    *predicates: Union[\n        None,\n        IntoExprColumn,\n        Iterable[IntoExprColumn],\n        bool,\n        List[bool],\n        ndarray,\n    ],\n    **constraints: Any\n) -> Self\n

    Filter self, removing rows which evaluate to False.

    Source code in atomlib/atoms.py
    def filter(\n    self,\n    *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n    **constraints: t.Any,\n) -> Self:\n    \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n    # TODO clean up\n    preds_not_none = tuple(filter(lambda p: p is not None, predicates))\n    if not len(preds_not_none) and not len(constraints):\n        return self\n    return self.with_atoms(Atoms(self._get_frame().filter(*preds_not_none, **constraints), _unchecked=True))  # type: ignore\n
    "},{"location":"api/#atomlib.HasAtoms.sort","title":"sort","text":"
    sort(\n    by: Union[IntoExpr, Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: Union[bool, Sequence[bool]] = False,\n    nulls_last: bool = False\n) -> DataFrame\n

    Sort the atoms in self by the given columns/expressions.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef sort(\n    self,\n    by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: t.Union[bool, t.Sequence[bool]] = False,\n    nulls_last: bool = False,\n) -> polars.DataFrame:\n    \"\"\"\n    Sort the atoms in `self` by the given columns/expressions.\n    \"\"\"\n    return self._get_frame().sort(\n        by, *more_by, descending=descending, nulls_last=nulls_last\n    )\n
    "},{"location":"api/#atomlib.HasAtoms.slice","title":"slice","text":"
    slice(\n    offset: int, length: Optional[int] = None\n) -> DataFrame\n

    Return a slice of the rows in self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef slice(self, offset: int, length: t.Optional[int] = None) -> polars.DataFrame:\n    \"\"\"Return a slice of the rows in `self`.\"\"\"\n    return self._get_frame().slice(offset, length)\n
    "},{"location":"api/#atomlib.HasAtoms.head","title":"head","text":"
    head(n: int = 5) -> DataFrame\n

    Return the first n rows of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef head(self, n: int = 5) -> polars.DataFrame:\n    \"\"\"Return the first `n` rows of `self`.\"\"\"\n    return self._get_frame().head(n)\n
    "},{"location":"api/#atomlib.HasAtoms.tail","title":"tail","text":"
    tail(n: int = 5) -> DataFrame\n

    Return the last n rows of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef tail(self, n: int = 5) -> polars.DataFrame:\n    \"\"\"Return the last `n` rows of `self`.\"\"\"\n    return self._get_frame().tail(n)\n
    "},{"location":"api/#atomlib.HasAtoms.drop_nulls","title":"drop_nulls","text":"
    drop_nulls(\n    subset: Union[str, Collection[str], None] = None\n) -> DataFrame\n

    Drop rows that contain nulls in any of columns subset.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n    \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n    return self._get_frame().drop_nulls(subset)\n
    "},{"location":"api/#atomlib.HasAtoms.fill_null","title":"fill_null","text":"
    fill_null(\n    value: Any = None,\n    strategy: Optional[FillNullStrategy] = None,\n    limit: Optional[int] = None,\n    matches_supertype: bool = True,\n) -> DataFrame\n

    Fill null values in self, using the specified value or strategy.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef fill_null(\n    self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n    limit: t.Optional[int] = None, matches_supertype: bool = True,\n) -> polars.DataFrame:\n    \"\"\"Fill null values in `self`, using the specified value or strategy.\"\"\"\n    return self._get_frame().fill_null(value, strategy, limit, matches_supertype=matches_supertype)\n
    "},{"location":"api/#atomlib.HasAtoms.fill_nan","title":"fill_nan","text":"
    fill_nan(value: Union[Expr, int, float, None]) -> DataFrame\n

    Fill floating-point NaN values in self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef fill_nan(self, value: t.Union[polars.Expr, int, float, None]) -> polars.DataFrame:\n    \"\"\"Fill floating-point NaN values in `self`.\"\"\"\n    return self._get_frame().fill_nan(value)\n
    "},{"location":"api/#atomlib.HasAtoms.concat","title":"concat classmethod","text":"
    concat(\n    atoms: Union[\n        HasAtomsT,\n        IntoAtoms,\n        Iterable[Union[HasAtomsT, IntoAtoms]],\n    ],\n    *,\n    rechunk: bool = True,\n    how: ConcatMethod = \"vertical\"\n) -> HasAtomsT\n

    Concatenate multiple Atoms together, handling metadata appropriately.

    Source code in atomlib/atoms.py
    @classmethod\ndef concat(cls: t.Type[HasAtomsT],\n           atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n           rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n    \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n    # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n    # same type as ``cls``.\n    if _is_abstract(cls):\n        raise TypeError(\"concat() must be called on a concrete class.\")\n\n    if isinstance(atoms, HasAtoms):\n        atoms = (atoms,)\n    dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n    representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n    if len(dfs) == 0:\n        return representative.with_atoms(Atoms.empty(), 'local')\n\n    if how in ('vertical', 'vertical_relaxed'):\n        # get order from first member\n        cols = dfs[0].columns\n        dfs = [df.select(cols) for df in dfs]\n    elif how == 'inner':\n        cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n        schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n        if len(schema) == 0:\n            raise ValueError(\"Atoms have no columns in common\")\n\n        dfs = [_select_schema(df, schema) for df in dfs]\n        how = 'vertical'\n\n    return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n
    "},{"location":"api/#atomlib.HasAtoms.partition_by","title":"partition_by","text":"
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: bool = False\n) -> Union[List[Self], Dict[Any, Self]]\n

    Group by the given columns and partition into separate dataframes.

    Return the partitions as a dictionary by specifying as_dict=True.

    Source code in atomlib/atoms.py
    def partition_by(\n    self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n    maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n    \"\"\"\n    Group by the given columns and partition into separate dataframes.\n\n    Return the partitions as a dictionary by specifying `as_dict=True`.\n    \"\"\"\n    if as_dict:\n        d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n        return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n    return [\n        self.with_atoms(Atoms(df, _unchecked=True))\n        for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n    ]\n
    "},{"location":"api/#atomlib.HasAtoms.select","title":"select","text":"
    select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.select)\ndef select(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr,\n) -> polars.DataFrame:\n    \"\"\"\n    Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtoms.select_schema","title":"select_schema","text":"
    select_schema(schema: SchemaDict) -> DataFrame\n

    Select columns from self and cast to the given schema. Raises TypeError if a column is not found or if it can't be cast.

    Source code in atomlib/atoms.py
    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n    \"\"\"\n    Select columns from `self` and cast to the given schema.\n    Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n    \"\"\"\n    return _select_schema(self, schema)\n
    "},{"location":"api/#atomlib.HasAtoms.select_props","title":"select_props","text":"
    select_props(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Self\n

    Select exprs from self, while keeping required columns.

    RETURNS DESCRIPTION Self

    A HasAtoms filtered to contain the

    Self

    specified properties (as well as required columns).

    Source code in atomlib/atoms.py
    def select_props(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Self:\n    \"\"\"\n    Select `exprs` from `self`, while keeping required columns.\n\n    Returns:\n      A [`HasAtoms`][atomlib.atoms.HasAtoms] filtered to contain the\n      specified properties (as well as required columns).\n    \"\"\"\n    props = self._get_frame().lazy().select(*exprs, **named_exprs).drop(_REQUIRED_COLUMNS, strict=False).collect(_eager=True)\n    return self.with_atoms(\n        Atoms(self._get_frame().select(_REQUIRED_COLUMNS).hstack(props), _unchecked=False)\n    )\n
    "},{"location":"api/#atomlib.HasAtoms.try_select","title":"try_select","text":"
    try_select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Optional[DataFrame]\n

    Try to select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns. Returns None if any columns are missing.

    Source code in atomlib/atoms.py
    def try_select(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr,\n) -> t.Optional[polars.DataFrame]:\n    \"\"\"\n    Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns. Returns `None` if any\n    columns are missing.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    try:\n        return self._get_frame().select(*exprs, **named_exprs)\n    except polars.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/#atomlib.HasAtoms.try_get_column","title":"try_get_column","text":"
    try_get_column(name: str) -> Optional[Series]\n

    Try to get a column from self, returning None if it doesn't exist.

    Source code in atomlib/atoms.py
    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n    \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n    try:\n        return self.get_column(name)\n    except polars.exceptions.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/#atomlib.HasAtoms.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n
    Source code in atomlib/atoms.py
    def assert_equal(self, other: t.Any):\n    assert isinstance(other, HasAtoms)\n    assert dict(self.schema) == dict(other.schema)\n    for col in self.schema.keys():\n        polars.testing.assert_series_equal(self[col], other[col], check_names=False, rtol=1e-3, atol=1e-8)\n
    "},{"location":"api/#atomlib.HasAtoms.bbox_atoms","title":"bbox_atoms","text":"
    bbox_atoms() -> BBox3D\n

    Return the bounding box of all the atoms in self.

    Source code in atomlib/atoms.py
    def bbox_atoms(self) -> BBox3D:\n    \"\"\"Return the bounding box of all the atoms in ``self``.\"\"\"\n    return BBox3D.from_pts(self.coords())\n
    "},{"location":"api/#atomlib.HasAtoms.transform_atoms","title":"transform_atoms","text":"
    transform_atoms(\n    transform: IntoTransform3D,\n    selection: Optional[AtomSelection] = None,\n    *,\n    transform_velocities: bool = False\n) -> Self\n

    Transform the atoms in self by transform. If selection is given, only transform the atoms in selection.

    Source code in atomlib/atoms.py
    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                    transform_velocities: bool = False) -> Self:\n    \"\"\"\n    Transform the atoms in `self` by `transform`.\n    If `selection` is given, only transform the atoms in `selection`.\n    \"\"\"\n    transform = Transform3D.make(transform)\n    selection = _selection_to_numpy(self, selection)\n    transformed = self.with_coords(Transform3D.make(transform) @ self.coords(selection), selection)\n    # try to transform velocities as well\n    if transform_velocities and (velocities := self.velocities(selection)) is not None:\n        return transformed.with_velocity(transform.transform_vec(velocities), selection)\n    return transformed\n
    "},{"location":"api/#atomlib.HasAtoms.round_near_zero","title":"round_near_zero","text":"
    round_near_zero(tol: float = 1e-14) -> Self\n

    Round atom position values near zero to zero.

    Source code in atomlib/atoms.py
    def round_near_zero(self, tol: float = 1e-14) -> Self:\n    \"\"\"\n    Round atom position values near zero to zero.\n    \"\"\"\n    return self.with_columns(coords=polars.concat_list(\n        polars.when(_coord_expr(col).abs() >= tol).then(_coord_expr(col)).otherwise(polars.lit(0.))\n        for col in range(3)\n    ).list.to_array(3))\n
    "},{"location":"api/#atomlib.HasAtoms.crop","title":"crop","text":"
    crop(\n    x_min: float = -numpy.inf,\n    x_max: float = numpy.inf,\n    y_min: float = -numpy.inf,\n    y_max: float = numpy.inf,\n    z_min: float = -numpy.inf,\n    z_max: float = numpy.inf,\n) -> Self\n

    Crop, removing all atoms outside of the specified region, inclusive.

    Source code in atomlib/atoms.py
    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf) -> Self:\n    \"\"\"\n    Crop, removing all atoms outside of the specified region, inclusive.\n    \"\"\"\n\n    return self.filter(\n        self.x().is_between(x_min, x_max, closed='both'),\n        self.y().is_between(y_min, y_max, closed='both'),\n        self.z().is_between(z_min, z_max, closed='both'),\n    )\n
    "},{"location":"api/#atomlib.HasAtoms.deduplicate","title":"deduplicate","text":"
    deduplicate(\n    tol: float = 0.001,\n    subset: Iterable[str] = (\"x\", \"y\", \"z\", \"symbol\"),\n    keep: UniqueKeepStrategy = \"first\",\n    maintain_order: bool = True,\n) -> Self\n

    De-duplicate atoms in self. Atoms of the same symbol that are closer than tolerance to each other (by Euclidian distance) will be removed, leaving only the atom specified by keep (defaults to the first atom).

    If subset is specified, only those columns will be included while assessing duplicates. Floating point columns other than 'x', 'y', and 'z' will not by toleranced.

    Source code in atomlib/atoms.py
    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n    \"\"\"\n    De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n    to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n    `keep` (defaults to the first atom).\n\n    If `subset` is specified, only those columns will be included while assessing duplicates.\n    Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n    \"\"\"\n    import scipy.spatial\n\n    cols = set((subset,) if isinstance(subset, str) else subset)\n\n    indices = numpy.arange(len(self))\n\n    spatial_cols = cols.intersection(('x', 'y', 'z'))\n    cols -= spatial_cols\n    if len(spatial_cols) > 0:\n        coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n        tree = scipy.spatial.KDTree(coords)\n\n        # TODO This is a bad algorithm\n        while True:\n            changed = False\n            for (i, j) in tree.query_pairs(tol, 2.):\n                # whenever we encounter a pair, ensure their index matches\n                i_i, i_j = indices[[i, j]]\n                if i_i != i_j:\n                    indices[i] = indices[j] = min(i_i, i_j)\n                    changed = True\n            if not changed:\n                break\n\n        self = self.with_column(polars.Series('_unique_pts', indices))\n        cols.add('_unique_pts')\n\n    frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n    if len(spatial_cols) > 0:\n        frame = frame.drop('_unique_pts')\n\n    return self.with_atoms(Atoms(frame, _unchecked=True))\n
    "},{"location":"api/#atomlib.HasAtoms.with_bounds","title":"with_bounds","text":"
    with_bounds(\n    cell_size: Optional[VecLike] = None,\n    cell_origin: Optional[VecLike] = None,\n) -> \"AtomCell\"\n

    Return a periodic cell with the given orthogonal cell dimensions.

    If cell_size is not specified, it will be assumed (and may be incorrect).

    Source code in atomlib/atoms.py
    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n    \"\"\"\n    Return a periodic cell with the given orthogonal cell dimensions.\n\n    If cell_size is not specified, it will be assumed (and may be incorrect).\n    \"\"\"\n    # TODO: test this\n    from .atomcell import AtomCell\n\n    if cell_size is None:\n        warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n        cell_size = self.bbox().size\n        cell_origin = self.bbox().min\n\n    # TODO test this origin code\n    cell = Cell.from_unit_cell(cell_size)\n    if cell_origin is not None:\n        cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n    return AtomCell(self.get_atoms(), cell, frame='local')\n
    "},{"location":"api/#atomlib.HasAtoms.coords","title":"coords","text":"
    coords(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Literal[\"local\"] = \"local\"\n) -> NDArray[float64]\n

    Return a (N, 3) ndarray of atom coordinates (dtype numpy.float64).

    Source code in atomlib/atoms.py
    def coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> NDArray[numpy.float64]:\n    \"\"\"Return a `(N, 3)` ndarray of atom coordinates (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n    df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n    return df.get_column('coords').to_numpy().astype(numpy.float64)\n
    "},{"location":"api/#atomlib.HasAtoms.x","title":"x","text":"
    x() -> Expr\n
    Source code in atomlib/atoms.py
    def x(self) -> polars.Expr:\n    return polars.col('coords').arr.get(0).alias('x')\n
    "},{"location":"api/#atomlib.HasAtoms.y","title":"y","text":"
    y() -> Expr\n
    Source code in atomlib/atoms.py
    def y(self) -> polars.Expr:\n    return polars.col('coords').arr.get(1).alias('y')\n
    "},{"location":"api/#atomlib.HasAtoms.z","title":"z","text":"
    z() -> Expr\n
    Source code in atomlib/atoms.py
    def z(self) -> polars.Expr:\n    return polars.col('coords').arr.get(2).alias('z')\n
    "},{"location":"api/#atomlib.HasAtoms.velocities","title":"velocities","text":"
    velocities(\n    selection: Optional[AtomSelection] = None,\n) -> Optional[NDArray[float64]]\n

    Return a (N, 3) ndarray of atom velocities (dtype numpy.float64).

    Source code in atomlib/atoms.py
    def velocities(self, selection: t.Optional[AtomSelection] = None) -> t.Optional[NDArray[numpy.float64]]:\n    \"\"\"Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n    if 'velocity' not in self:\n        return None\n\n    df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n    return df.get_column('velocity').to_numpy().astype(numpy.float64)\n
    "},{"location":"api/#atomlib.HasAtoms.types","title":"types","text":"
    types() -> Optional[Series]\n

    Returns a Series of atom types (dtype polars.Int32).

    Source code in atomlib/atoms.py
    def types(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('type')\n
    "},{"location":"api/#atomlib.HasAtoms.masses","title":"masses","text":"
    masses() -> Optional[Series]\n

    Returns a Series of atom masses (dtype polars.Float32).

    Source code in atomlib/atoms.py
    def masses(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('mass')\n
    "},{"location":"api/#atomlib.HasAtoms.add_atom","title":"add_atom","text":"
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: Union[ArrayLike, float],\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    **kwargs: Any,\n) -> Self\n

    Return a copy of self with an extra atom.

    By default, all extra columns present in self must be specified as **kwargs.

    Try to avoid calling this in a loop (Use HasAtoms.concat instead).

    Source code in atomlib/atoms.py
    def add_atom(self, elem: t.Union[int, str], /,\n             x: t.Union[ArrayLike, float],\n             y: t.Optional[float] = None,\n             z: t.Optional[float] = None,\n             **kwargs: t.Any) -> Self:\n    \"\"\"\n    Return a copy of `self` with an extra atom.\n\n    By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n    Try to avoid calling this in a loop (Use [`HasAtoms.concat`][atomlib.atoms.HasAtoms.concat] instead).\n    \"\"\"\n    if isinstance(elem, int):\n        kwargs.update(elem=elem)\n    else:\n        kwargs.update(symbol=elem)\n    if hasattr(x, '__len__') and len(x) > 1:  # type: ignore\n        (x, y, z) = to_vec3(x)\n    elif y is None or z is None:\n        raise ValueError(\"Must specify vector of positions or x, y, & z.\")\n\n    sym = get_sym(elem) if isinstance(elem, int) else elem\n    d: t.Dict[str, t.Any] = {'x': x, 'y': y, 'z': z, 'symbol': sym, **kwargs}\n    return self.concat(\n        (self, Atoms(d).select_schema(self.schema)),\n        how='vertical'\n    )\n
    "},{"location":"api/#atomlib.HasAtoms.pos","title":"pos","text":"
    pos(\n    x: Union[Sequence[Optional[float]], float, None] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n

    Select all atoms at a given position.

    Formally, returns all atoms within a cube of radius tol centered at (x,y,z), exclusive of the cube's surface.

    Additional parameters given as kwargs will be checked as additional parameters (with strict equality).

    Source code in atomlib/atoms.py
    def pos(self,\n        x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n        y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n        tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n    \"\"\"\n    Select all atoms at a given position.\n\n    Formally, returns all atoms within a cube of radius ``tol``\n    centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n    Additional parameters given as ``kwargs`` will be checked\n    as additional parameters (with strict equality).\n    \"\"\"\n\n    if isinstance(x, t.Sequence):\n        (x, y, z) = x\n\n    tol = abs(float(tol))\n    selection = polars.lit(True)\n    if x is not None:\n        selection &= self.x().is_between(x - tol, x + tol, closed='none')\n    if y is not None:\n        selection &= self.y().is_between(y - tol, y + tol, closed='none')\n    if z is not None:\n        selection &= self.z().is_between(z - tol, z + tol, closed='none')\n    for (col, val) in kwargs.items():\n        selection &= (polars.col(col) == val)\n\n    return selection\n
    "},{"location":"api/#atomlib.HasAtoms.with_index","title":"with_index","text":"
    with_index(index: Optional[AtomValues] = None) -> Self\n

    Returns self with a row index added in column 'i' (dtype polars.Int64). If index is not specified, defaults to an existing index or a new index.

    Source code in atomlib/atoms.py
    def with_index(self, index: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n    If `index` is not specified, defaults to an existing index or a new index.\n    \"\"\"\n    if index is None and 'i' in self.columns:\n        return self\n    if index is None:\n        index = numpy.arange(len(self), dtype=numpy.int64)\n    return self.with_column(_values_to_expr(self, index, polars.Int64).alias('i'))\n
    "},{"location":"api/#atomlib.HasAtoms.with_wobble","title":"with_wobble","text":"
    with_wobble(wobble: Optional[AtomValues] = None) -> Self\n

    Return self with the given displacements in column 'wobble' (dtype polars.Float64). If wobble is not specified, defaults to the already-existing wobbles or 0.

    Source code in atomlib/atoms.py
    def with_wobble(self, wobble: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n    \"\"\"\n    if wobble is None and 'wobble' in self.columns:\n        return self\n    wobble = 0. if wobble is None else wobble\n    return self.with_column(_values_to_expr(self, wobble, polars.Float64).alias('wobble'))\n
    "},{"location":"api/#atomlib.HasAtoms.with_occupancy","title":"with_occupancy","text":"
    with_occupancy(\n    frac_occupancy: Optional[AtomValues] = None,\n) -> Self\n

    Return self with the given fractional occupancies (dtype polars.Float64). If frac_occupancy is not specified, defaults to the already-existing occupancies or 1.

    Source code in atomlib/atoms.py
    def with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n    \"\"\"\n    if frac_occupancy is None and 'frac_occupancy' in self.columns:\n        return self\n    frac_occupancy = 1. if frac_occupancy is None else frac_occupancy\n    return self.with_column(_values_to_expr(self, frac_occupancy, polars.Float64).alias('frac_occupancy'))\n
    "},{"location":"api/#atomlib.HasAtoms.apply_wobble","title":"apply_wobble","text":"
    apply_wobble(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    Displace the atoms in self by the amount in the wobble column. wobble is interpretated as a mean-squared displacement, which is distributed equally over each axis.

    Source code in atomlib/atoms.py
    def apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    Displace the atoms in `self` by the amount in the `wobble` column.\n    `wobble` is interpretated as a mean-squared displacement, which is distributed\n    equally over each axis.\n    \"\"\"\n    if 'wobble' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    stddev = self.select((polars.col('wobble') / 3.).sqrt()).to_series().to_numpy()\n    coords = self.coords()\n    coords += stddev[:, None] * rng.standard_normal(coords.shape)\n    return self.with_coords(coords)\n
    "},{"location":"api/#atomlib.HasAtoms.apply_occupancy","title":"apply_occupancy","text":"
    apply_occupancy(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    For each atom in self, use its frac_occupancy to randomly decide whether to remove it.

    Source code in atomlib/atoms.py
    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n    \"\"\"\n    if 'frac_occupancy' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    frac = self.select('frac_occupancy').to_series().to_numpy()\n    choice = rng.binomial(1, frac).astype(numpy.bool_)\n    return self.filter(polars.lit(choice))\n
    "},{"location":"api/#atomlib.HasAtoms.with_type","title":"with_type","text":"
    with_type(types: Optional[AtomValues] = None) -> Self\n

    Return self with the given atom types in column 'type'. If types is not specified, use the already existing types or auto-assign them.

    When auto-assigning, each symbol is given a unique value, case-sensitive. Values are assigned from lowest atomic number to highest. For instance: [\"Ag+\", \"Na\", \"H\", \"Ag\"] => [3, 11, 1, 2]

    Source code in atomlib/atoms.py
    def with_type(self, types: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom types in column 'type'.\n    If `types` is not specified, use the already existing types or auto-assign them.\n\n    When auto-assigning, each symbol is given a unique value, case-sensitive.\n    Values are assigned from lowest atomic number to highest.\n    For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n    \"\"\"\n    if types is not None:\n        return self.with_columns(type=_values_to_expr(self, types, polars.Int32))\n    if 'type' in self.columns:\n        return self\n\n    unique = Atoms(self._get_frame().unique(maintain_order=False, subset=['elem', 'symbol']).sort(['elem', 'symbol']), _unchecked=True)\n    new = self.with_column(polars.Series('type', values=numpy.zeros(len(self)), dtype=polars.Int32))\n\n    logging.warning(\"Auto-assigning element types\")\n    for (i, (elem, sym)) in enumerate(unique.select(('elem', 'symbol')).rows()):\n        print(f\"Assigning type {i+1} to element '{sym}'\")\n        new = new.with_column(polars.when((polars.col('elem') == elem) & (polars.col('symbol') == sym))\n                                    .then(polars.lit(i+1))\n                                    .otherwise(polars.col('type'))\n                                    .alias('type'))\n\n    assert (new.get_column('type') == 0).sum() == 0\n    return new\n
    "},{"location":"api/#atomlib.HasAtoms.with_mass","title":"with_mass","text":"
    with_mass(mass: Optional[ArrayLike] = None) -> Self\n

    Return self with the given atom masses in column 'mass'. If mass is not specified, use the already existing masses or auto-assign them.

    Source code in atomlib/atoms.py
    def with_mass(self, mass: t.Optional[ArrayLike] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom masses in column `'mass'`.\n    If `mass` is not specified, use the already existing masses or auto-assign them.\n    \"\"\"\n    if mass is not None:\n        return self.with_column(_values_to_expr(self, mass, polars.Float32).alias('mass'))\n    if 'mass' in self.columns:\n        return self\n\n    unique_elems = self.get_column('elem').unique()\n    new = self.with_column(polars.Series('mass', values=numpy.zeros(len(self)), dtype=polars.Float32))\n\n    logging.warning(\"Auto-assigning element masses\")\n    for elem in unique_elems:\n        new = new.with_column(polars.when(polars.col('elem') == elem)\n                                    .then(polars.lit(get_mass(elem)))\n                                    .otherwise(polars.col('mass'))\n                                    .alias('mass'))\n\n    assert (new.get_column('mass').abs() < 1e-10).sum() == 0\n    return new\n
    "},{"location":"api/#atomlib.HasAtoms.with_symbol","title":"with_symbol","text":"
    with_symbol(\n    symbols: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n) -> Self\n

    Return self with the given atomic symbols.

    Source code in atomlib/atoms.py
    def with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atomic symbols.\n    \"\"\"\n    if selection is not None:\n        selection = _selection_to_numpy(self, selection)\n        new_symbols = self.get_column('symbol')\n        new_symbols[selection] = polars.Series(list(numpy.broadcast_to(symbols, len(selection))), dtype=polars.Utf8)\n        symbols = new_symbols\n\n    # TODO better cast here\n    symbols = polars.Series('symbol', list(numpy.broadcast_to(symbols, len(self))), dtype=polars.Utf8)\n    return self.with_columns((symbols, get_elem(symbols)))\n
    "},{"location":"api/#atomlib.HasAtoms.with_coords","title":"with_coords","text":"
    with_coords(\n    pts: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Literal[\"local\"] = \"local\"\n) -> Self\n

    Return self replaced with the given atomic positions.

    Source code in atomlib/atoms.py
    def with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic positions.\n    \"\"\"\n    if selection is not None:\n        selection = _selection_to_numpy(self, selection)\n        new_pts = self.coords()\n        pts = numpy.atleast_2d(pts)\n        assert pts.shape[-1] == 3\n        new_pts[selection] = pts\n        pts = new_pts\n\n    # https://github.com/pola-rs/polars/issues/18369\n    pts = numpy.broadcast_to(pts, (len(self), 3)) if len(self) else []\n    return self.with_columns(polars.Series('coords', pts, polars.Array(polars.Float64, 3)))\n
    "},{"location":"api/#atomlib.HasAtoms.with_velocity","title":"with_velocity","text":"
    with_velocity(\n    pts: Optional[ArrayLike] = None,\n    selection: Optional[AtomSelection] = None,\n) -> Self\n

    Return self replaced with the given atomic velocities. If pts is not specified, use the already existing velocities or zero.

    Source code in atomlib/atoms.py
    def with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                  selection: t.Optional[AtomSelection] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic velocities.\n    If `pts` is not specified, use the already existing velocities or zero.\n    \"\"\"\n    if pts is None:\n        if 'velocity' in self:\n            return self\n        all_pts = numpy.zeros((len(self), 3))\n    else:\n        all_pts = self['velocity'].to_numpy()\n\n    if selection is None:\n        all_pts = pts or all_pts\n    elif pts is not None:\n        selection = _selection_to_numpy(self, selection)\n        all_pts = numpy.require(all_pts, requirements=['WRITEABLE'])\n        pts = numpy.atleast_2d(pts)\n        assert pts.shape[-1] == 3\n        all_pts[selection] = pts\n\n    all_pts = numpy.broadcast_to(all_pts, (len(self), 3))\n    return self.with_columns(polars.Series('velocity', all_pts, polars.Array(polars.Float64, 3)))\n
    "},{"location":"api/#atomlib.Cell","title":"Cell dataclass","text":"

    Bases: HasCell

    Internal class for representing the coordinate systems of a crystal.

    The overall transformation from crystal coordinates to real-space coordinates is is split into four transformations, applied from bottom to top. First is n_cells, which scales from fractions of a unit cell to fractions of a supercell. Next is cell_size, which scales to real-space units. ortho is an orthogonalization matrix, a det = 1 upper-triangular matrix which transforms crystal axes to an orthogonal coordinate system. Finally, affine contains any remaining transformations to the local coordinate system, which atoms are stored in.

    Source code in atomlib/cell.py
    @dataclass(frozen=True, init=False)\nclass Cell(HasCell):\n    \"\"\"\n    Internal class for representing the coordinate systems of a crystal.\n\n    The overall transformation from crystal coordinates to real-space coordinates is\n    is split into four transformations, applied from bottom to top. First is `n_cells`,\n    which scales from fractions of a unit cell to fractions of a supercell. Next is\n    `cell_size`, which scales to real-space units. `ortho` is an orthogonalization\n    matrix, a det = 1 upper-triangular matrix which transforms crystal axes to\n    an orthogonal coordinate system. Finally, `affine` contains any remaining\n    transformations to the local coordinate system, which atoms are stored in.\n    \"\"\"\n\n    def get_cell(self) -> Cell:\n        return self\n\n    def with_cell(self: Cell, cell: Cell) -> Cell:\n        return cell\n\n    _affine: AffineTransform3D = AffineTransform3D()\n    \"\"\"\n    Affine transformation. Holds transformation from `'ortho'` to `'local'` coordinates,\n    including rotation away from the standard crystal orientation.\n    \"\"\"\n\n    _ortho: LinearTransform3D = LinearTransform3D()\n    \"\"\"\n    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.\n    \"\"\"\n\n    _cell_size: NDArray[numpy.float64]\n    \"\"\"Unit cell size.\"\"\"\n    _cell_angle: NDArray[numpy.float64] = field(default_factory=lambda: numpy.full(3, numpy.pi/2.))\n    \"\"\"Unit cell angles, in radians.\"\"\"\n    _n_cells: NDArray[numpy.int64] = field(default_factory=lambda: numpy.ones(3, numpy.int64))\n    \"\"\"Number of unit cells.\"\"\"\n    _pbc: NDArray[numpy.bool_] = field(default_factory=lambda: numpy.ones(3, numpy.bool_))\n    \"\"\"Flags indicating the presence of periodic boundary conditions along each axis.\"\"\"\n\n    def __init__(self, *,\n        affine: t.Optional[AffineTransform3D] = None, ortho: t.Optional[LinearTransform3D] = None,\n        cell_size: VecLike, cell_angle: t.Optional[VecLike] = None,\n        n_cells: t.Optional[VecLike] = None, pbc: t.Optional[VecLike] = None):\n\n        object.__setattr__(self, '_affine', AffineTransform3D() if affine is None else affine)\n        object.__setattr__(self, '_ortho', LinearTransform3D() if ortho is None else ortho)\n        object.__setattr__(self, '_cell_size', to_vec3(cell_size))\n        object.__setattr__(self, '_cell_angle', numpy.full(3, numpy.pi/2.) if cell_angle is None else to_vec3(cell_angle))\n        object.__setattr__(self, '_n_cells', numpy.ones(3, numpy.int_) if n_cells is None else to_vec3(n_cells, numpy.int64))\n        object.__setattr__(self, '_pbc', numpy.ones(3, numpy.bool_) if pbc is None else to_vec3(pbc, numpy.bool_))\n\n    @staticmethod\n    def from_unit_cell(cell_size: VecLike, cell_angle: t.Optional[VecLike] = None, n_cells: t.Optional[VecLike] = None,\n                       pbc: t.Optional[VecLike] = None):\n        return Cell(\n            ortho=cell_to_ortho([1.]*3, cell_angle),\n            n_cells=to_vec3([1]*3 if n_cells is None else n_cells, numpy.int_),\n            cell_size=to_vec3(cell_size),\n            cell_angle=to_vec3([numpy.pi/2.]*3 if cell_angle is None else cell_angle),\n            pbc=pbc\n        )\n\n    @staticmethod\n    def from_ortho(ortho: AffineTransform3D, n_cells: t.Optional[VecLike] = None, pbc: t.Optional[VecLike] = None):\n        lin = ortho.to_linear()\n        # decompose into orthogonal and upper triangular\n        q, r = numpy.linalg.qr(lin.inner)\n\n        # flip QR decomposition so R has positive diagonals\n        signs = numpy.sign(numpy.diagonal(r))\n        # multiply flips to columns of Q, rows of R\n        q = q * signs\n        r = r * signs[:, None]\n        #numpy.testing.assert_allclose(q @ r, lin.inner)\n        if numpy.linalg.det(q) < 0:\n            warn(\"Crystal is left-handed. This is currently unsupported, and may cause errors.\")\n            # currently, behavior is to leave `ortho` proper, and move the inversion into the affine transform\n\n        cell_size, cell_angle = ortho_to_cell(lin)\n        return Cell(\n            affine=LinearTransform3D(q).translate(ortho.translation()),\n            ortho=LinearTransform3D(r / cell_size).round_near_zero(),\n            cell_size=cell_size, cell_angle=cell_angle,\n            n_cells=to_vec3([1]*3 if n_cells is None else n_cells, numpy.int_),\n            pbc=pbc,\n        )\n\n    def __str__(self) -> str:\n        return \"\\n\".join((\n            self.__class__.__name__,\n            f\"Cell size: {self.cell_size!r}\",\n            f\"Cell angle: {self.cell_angle!r}\",\n            f\"# cells: {self.n_cells!r}\",\n            f\"pbc: {self.pbc!r}\",\n        ))\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"ortho={self.ortho}, affine={self.affine}, cell_size={self.cell_size}, \"\n            f\"cell_angle={self.cell_angle}, n_cells={self.n_cells}, pbc={self.pbc})\"\n        )\n\n    def _repr_pretty_(self, p: t.Any, cycle: bool) -> None:\n        p.text(f\"{self.__class__.__name__}(...)\") if cycle else p.text(str(self))\n
    "},{"location":"api/#atomlib.Cell.affine","title":"affine property","text":"
    affine: AffineTransform3D\n

    Affine transformation. Holds transformation from 'ortho' to 'local' coordinates, including rotation away from the standard crystal orientation.

    "},{"location":"api/#atomlib.Cell.ortho","title":"ortho property","text":"
    ortho: LinearTransform3D\n

    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.

    "},{"location":"api/#atomlib.Cell.metric","title":"metric property","text":"
    metric: LinearTransform3D\n

    Cell metric tensor

    Returns the dot product between every combination of basis vectors. :math:\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j

    "},{"location":"api/#atomlib.Cell.cell_size","title":"cell_size property","text":"
    cell_size: NDArray[float64]\n

    Unit cell size.

    "},{"location":"api/#atomlib.Cell.cell_angle","title":"cell_angle property","text":"
    cell_angle: NDArray[float64]\n

    Unit cell angles, in radians.

    "},{"location":"api/#atomlib.Cell.n_cells","title":"n_cells property","text":"
    n_cells: NDArray[int_]\n

    Number of unit cells.

    "},{"location":"api/#atomlib.Cell.pbc","title":"pbc property","text":"
    pbc: NDArray[bool_]\n

    Flags indicating the presence of periodic boundary conditions along each axis.

    "},{"location":"api/#atomlib.Cell.ortho_size","title":"ortho_size property","text":"
    ortho_size: NDArray[float64]\n

    Return size of orthogonal unit cell.

    Equivalent to the diagonal of the orthogonalization matrix.

    "},{"location":"api/#atomlib.Cell.box_size","title":"box_size property","text":"
    box_size: NDArray[float64]\n

    Return size of the cell box.

    Equivalent to self.n_cells * self.cell_size.

    "},{"location":"api/#atomlib.Cell.bbox","title":"bbox class-attribute instance-attribute","text":"
    bbox = bbox_cell\n
    "},{"location":"api/#atomlib.Cell.get_transform","title":"get_transform","text":"
    get_transform(\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n

    In the two-argument form, get the transform to frame_to from frame_from. In the one-argument form, get the transform from local coordinates to 'frame'.

    Source code in atomlib/cell.py
    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n    \"\"\"\n    In the two-argument form, get the transform to `frame_to` from `frame_from`.\n    In the one-argument form, get the transform from local coordinates to 'frame'.\n    \"\"\"\n    transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n    transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n    if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n        return AffineTransform3D()\n    return transform_to.inverse() @ transform_from\n
    "},{"location":"api/#atomlib.Cell.corners","title":"corners","text":"
    corners(frame: CoordinateFrame = 'local') -> ndarray\n
    Source code in atomlib/cell.py
    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n    corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n    return self.get_transform(frame, 'cell_box') @ corners\n
    "},{"location":"api/#atomlib.Cell.bbox_cell","title":"bbox_cell","text":"
    bbox_cell(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the bounding box of the cell box in the given coordinate system.

    Source code in atomlib/cell.py
    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n    return BBox3D.from_pts(self.corners(frame))\n
    "},{"location":"api/#atomlib.Cell.is_orthogonal","title":"is_orthogonal","text":"
    is_orthogonal(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal (axes are at right angles.)

    Source code in atomlib/cell.py
    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n    return self.ortho.is_diagonal(tol=tol)\n
    "},{"location":"api/#atomlib.Cell.is_orthogonal_in_local","title":"is_orthogonal_in_local","text":"
    is_orthogonal_in_local(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal and aligned with the local coordinate system.

    Source code in atomlib/cell.py
    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n    transform = (self.affine @ self.ortho).to_linear()\n    if not transform.is_scaled_orthogonal(tol):\n        return False\n    normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n    # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n    return all(\n        any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n        for row in normed\n    )\n
    "},{"location":"api/#atomlib.Cell.to_ortho","title":"to_ortho","text":"
    to_ortho() -> AffineTransform3D\n
    Source code in atomlib/cell.py
    def to_ortho(self) -> AffineTransform3D:\n    return self.get_transform('local', 'cell_box')\n
    "},{"location":"api/#atomlib.Cell.transform_cell","title":"transform_cell","text":"
    transform_cell(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> HasCellT\n

    Apply the given transform to the unit cell, and return a new Cell. The transform is applied in coordinate frame 'frame'. Orthogonal and affine transformations are applied to the affine matrix component, while skew and scaling is applied to the orthogonalization matrix/cell_size.

    Source code in atomlib/cell.py
    def transform_cell(self: HasCellT, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> HasCellT:\n    \"\"\"\n    Apply the given transform to the unit cell, and return a new `Cell`.\n    The transform is applied in coordinate frame 'frame'.\n    Orthogonal and affine transformations are applied to the affine matrix component,\n    while skew and scaling is applied to the orthogonalization matrix/cell_size.\n    \"\"\"\n    transform = t.cast(AffineTransform3D, self.change_transform(transform, 'local', frame))\n    if not transform.to_linear().is_orthogonal():\n        raise NotImplementedError()\n    return self.with_cell(Cell(\n        affine=transform @ self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size,\n        cell_angle=self.cell_angle,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.Cell.strain_orthogonal","title":"strain_orthogonal","text":"
    strain_orthogonal() -> HasCellT\n

    Orthogonalize using strain.

    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane. For small displacements, no hydrostatic strain is applied (volume is conserved).

    Source code in atomlib/cell.py
    def strain_orthogonal(self: HasCellT) -> HasCellT:\n    \"\"\"\n    Orthogonalize using strain.\n\n    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n    For small displacements, no hydrostatic strain is applied (volume is conserved).\n    \"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=LinearTransform3D(),\n        cell_size=self.cell_size,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.Cell.repeat","title":"repeat","text":"
    repeat(n: Union[int, VecLike]) -> HasCellT\n

    Tile the cell by n in each dimension.

    Source code in atomlib/cell.py
    def repeat(self: HasCellT, n: t.Union[int, VecLike]) -> HasCellT:\n    \"\"\"Tile the cell by `n` in each dimension.\"\"\"\n    ns = numpy.broadcast_to(n, 3)\n    if not numpy.issubdtype(ns.dtype, numpy.integer):\n        raise ValueError(\"repeat() argument must be an integer or integer array.\")\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size,\n        cell_angle=self.cell_angle,\n        n_cells=self.n_cells * numpy.broadcast_to(n, 3),\n        pbc = self.pbc | (ns > 1)  # assume periodic along repeated directions\n    ))\n
    "},{"location":"api/#atomlib.Cell.explode","title":"explode","text":"
    explode() -> HasCellT\n

    Materialize repeated cells as one supercell.

    Source code in atomlib/cell.py
    def explode(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*self.n_cells,\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.Cell.explode_z","title":"explode_z","text":"
    explode_z() -> HasCellT\n

    Materialize repeated cells as one supercell in z.

    Source code in atomlib/cell.py
    def explode_z(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n        n_cells=[*self.n_cells[:2], 1],\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.Cell.crop","title":"crop","text":"
    crop(\n    x_min: float = -numpy.inf,\n    x_max: float = numpy.inf,\n    y_min: float = -numpy.inf,\n    y_max: float = numpy.inf,\n    z_min: float = -numpy.inf,\n    z_max: float = numpy.inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> HasCellT\n

    Crop self to the given extents. For a non-orthogonal cell, this must be specified in cell coordinates. This function implicity explodes the cell as well.

    Source code in atomlib/cell.py
    def crop(self: HasCellT, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n         frame: CoordinateFrame = 'local') -> HasCellT:\n    \"\"\"\n    Crop `self` to the given extents. For a non-orthogonal\n    cell, this must be specified in cell coordinates. This\n    function implicity `explode`s the cell as well.\n    \"\"\"\n\n    if not frame.lower().startswith('cell'):\n        if not self.is_orthogonal():\n            raise ValueError(\"Cannot crop a non-orthogonal cell in orthogonal coordinates. Use crop_atoms instead.\")\n\n    min = to_vec3([x_min, y_min, z_min])\n    max = to_vec3([x_max, y_max, z_max])\n    (min, max) = self.get_transform('cell_box', frame).transform([min, max])\n    new_box = BBox3D(min, max) & BBox3D.unit()\n    cropped = (new_box.min > 0.) | (new_box.max < 1.)\n\n    return self.with_cell(Cell(\n        affine=self.affine @ AffineTransform3D.translate(-new_box.min),\n        ortho=self.ortho,\n        cell_size=new_box.size * self.cell_size * numpy.where(cropped, self.n_cells, 1),\n        n_cells=numpy.where(cropped, 1, self.n_cells),\n        cell_angle=self.cell_angle,\n        pbc=self.pbc & ~cropped  # remove periodicity along cropped directions\n    ))\n
    "},{"location":"api/#atomlib.Cell.change_transform","title":"change_transform","text":"
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n

    Coordinate-change a transformation from frame_from into frame_to.

    Source code in atomlib/cell.py
    def change_transform(self, transform: Transform3D,\n                     frame_to: t.Optional[CoordinateFrame] = None,\n                     frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n    \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n    if frame_to == frame_from and frame_to is not None:\n        return transform\n    coord_change = self.get_transform(frame_to, frame_from)\n    return coord_change @ transform @ coord_change.inverse()\n
    "},{"location":"api/#atomlib.Cell.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n
    Source code in atomlib/cell.py
    def assert_equal(self, other: t.Any):\n    assert isinstance(other, HasCell) and type(self) is type(other)\n    numpy.testing.assert_array_almost_equal(self.affine.inner, other.affine.inner, 6)\n    numpy.testing.assert_array_almost_equal(self.ortho.inner, other.ortho.inner, 6)\n    numpy.testing.assert_array_almost_equal(self.cell_size, other.cell_size, 6)\n    numpy.testing.assert_array_equal(self.n_cells, other.n_cells)\n    numpy.testing.assert_array_equal(self.pbc, other.pbc)\n
    "},{"location":"api/#atomlib.Cell.get_cell","title":"get_cell","text":"
    get_cell() -> Cell\n
    Source code in atomlib/cell.py
    def get_cell(self) -> Cell:\n    return self\n
    "},{"location":"api/#atomlib.Cell.with_cell","title":"with_cell","text":"
    with_cell(cell: Cell) -> Cell\n
    Source code in atomlib/cell.py
    def with_cell(self: Cell, cell: Cell) -> Cell:\n    return cell\n
    "},{"location":"api/#atomlib.Cell.from_unit_cell","title":"from_unit_cell staticmethod","text":"
    from_unit_cell(\n    cell_size: VecLike,\n    cell_angle: Optional[VecLike] = None,\n    n_cells: Optional[VecLike] = None,\n    pbc: Optional[VecLike] = None,\n)\n
    Source code in atomlib/cell.py
    @staticmethod\ndef from_unit_cell(cell_size: VecLike, cell_angle: t.Optional[VecLike] = None, n_cells: t.Optional[VecLike] = None,\n                   pbc: t.Optional[VecLike] = None):\n    return Cell(\n        ortho=cell_to_ortho([1.]*3, cell_angle),\n        n_cells=to_vec3([1]*3 if n_cells is None else n_cells, numpy.int_),\n        cell_size=to_vec3(cell_size),\n        cell_angle=to_vec3([numpy.pi/2.]*3 if cell_angle is None else cell_angle),\n        pbc=pbc\n    )\n
    "},{"location":"api/#atomlib.Cell.from_ortho","title":"from_ortho staticmethod","text":"
    from_ortho(\n    ortho: AffineTransform3D,\n    n_cells: Optional[VecLike] = None,\n    pbc: Optional[VecLike] = None,\n)\n
    Source code in atomlib/cell.py
    @staticmethod\ndef from_ortho(ortho: AffineTransform3D, n_cells: t.Optional[VecLike] = None, pbc: t.Optional[VecLike] = None):\n    lin = ortho.to_linear()\n    # decompose into orthogonal and upper triangular\n    q, r = numpy.linalg.qr(lin.inner)\n\n    # flip QR decomposition so R has positive diagonals\n    signs = numpy.sign(numpy.diagonal(r))\n    # multiply flips to columns of Q, rows of R\n    q = q * signs\n    r = r * signs[:, None]\n    #numpy.testing.assert_allclose(q @ r, lin.inner)\n    if numpy.linalg.det(q) < 0:\n        warn(\"Crystal is left-handed. This is currently unsupported, and may cause errors.\")\n        # currently, behavior is to leave `ortho` proper, and move the inversion into the affine transform\n\n    cell_size, cell_angle = ortho_to_cell(lin)\n    return Cell(\n        affine=LinearTransform3D(q).translate(ortho.translation()),\n        ortho=LinearTransform3D(r / cell_size).round_near_zero(),\n        cell_size=cell_size, cell_angle=cell_angle,\n        n_cells=to_vec3([1]*3 if n_cells is None else n_cells, numpy.int_),\n        pbc=pbc,\n    )\n
    "},{"location":"api/#atomlib.HasCell","title":"HasCell","text":"Source code in atomlib/cell.py
    class HasCell:\n    # abstract methods\n\n    @abc.abstractmethod\n    def get_cell(self) -> Cell:\n        \"\"\"Get the cell contained in ``self``. This should be a low cost method.\"\"\"\n        ...\n\n    @abc.abstractmethod\n    def with_cell(self: HasCellT, cell: Cell) -> HasCellT:\n        \"\"\"Replace the cell in ``self`` with ``cell``.\"\"\"\n        ...\n\n    # getters\n\n    @property\n    def affine(self) -> AffineTransform3D:\n        \"\"\"\n        Affine transformation. Holds transformation from 'ortho' to 'local' coordinates,\n        including rotation away from the standard crystal orientation.\n        \"\"\"\n        return self.get_cell()._affine\n\n    @property\n    def ortho(self) -> LinearTransform3D:\n        \"\"\"\n        Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.\n        \"\"\"\n        return self.get_cell()._ortho\n\n    @property\n    def metric(self) -> LinearTransform3D:\n        r\"\"\"\n        Cell metric tensor\n\n        Returns the dot product between every combination of basis vectors.\n        :math:`\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j`\n        \"\"\"\n        ortho = self.get_cell()._ortho.scale(self.cell_size)\n        return ortho.T @ ortho\n\n    @property\n    def cell_size(self) -> NDArray[numpy.float64]:\n        \"\"\"Unit cell size.\"\"\"\n        return self.get_cell()._cell_size\n\n    @property\n    def cell_angle(self) -> NDArray[numpy.float64]:\n        \"\"\"Unit cell angles, in radians.\"\"\"\n        return self.get_cell()._cell_angle\n\n    @property\n    def n_cells(self) -> NDArray[numpy.int_]:\n        \"\"\"Number of unit cells.\"\"\"\n        return self.get_cell()._n_cells\n\n    @property\n    def pbc(self) -> NDArray[numpy.bool_]:\n        \"\"\"Flags indicating the presence of periodic boundary conditions along each axis.\"\"\"\n        return self.get_cell()._pbc\n\n    @property\n    def ortho_size(self) -> NDArray[numpy.float64]:\n        \"\"\"\n        Return size of orthogonal unit cell.\n\n        Equivalent to the diagonal of the orthogonalization matrix.\n        \"\"\"\n        return self.cell_size * numpy.diag(self.ortho.inner)\n\n    @property\n    def box_size(self) -> NDArray[numpy.float64]:\n        \"\"\"\n        Return size of the cell box.\n\n        Equivalent to ``self.n_cells * self.cell_size``.\n        \"\"\"\n        return self.n_cells * self.cell_size\n\n    # get transforms\n\n    def _get_transform_to_local(self, frame: CoordinateFrame) -> AffineTransform3D:\n        \"\"\"Get the transform from `frame` to local coordinates.\"\"\"\n        frame = t.cast(CoordinateFrame, frame.lower())\n\n        if frame == 'local' or frame == 'global':\n            return LinearTransform3D()\n\n        if frame == 'linear':\n            return self.affine.to_translation()\n\n        if frame.startswith('cell'):\n            transform = self.affine @ self.ortho\n            cell_size = self.cell_size\n        elif frame.startswith('ortho'):\n            transform = self.affine\n            cell_size = self.ortho_size\n        else:\n            raise ValueError(f\"Unknown coordinate frame '{frame}'\")\n\n        if '_' not in frame:\n            return transform\n        end = frame.split('_', 2)[1]\n        if end == 'frac':\n            return transform @ LinearTransform3D.scale(cell_size)\n        if end == 'box':\n            return transform @ LinearTransform3D.scale(cell_size * self.n_cells)\n        raise ValueError(f\"Unknown coordinate frame '{frame}'\")\n\n    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n        \"\"\"\n        In the two-argument form, get the transform to `frame_to` from `frame_from`.\n        In the one-argument form, get the transform from local coordinates to 'frame'.\n        \"\"\"\n        transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n        transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n        if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n            return AffineTransform3D()\n        return transform_to.inverse() @ transform_from\n\n    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n        corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n        return self.get_transform(frame, 'cell_box') @ corners\n\n    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n        \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n        return BBox3D.from_pts(self.corners(frame))\n\n    bbox = bbox_cell\n\n    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n        \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n        return self.ortho.is_diagonal(tol=tol)\n\n    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n        \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n        transform = (self.affine @ self.ortho).to_linear()\n        if not transform.is_scaled_orthogonal(tol):\n            return False\n        normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n        # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n        return all(\n            any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n            for row in normed\n        )\n\n    def _cell_size_in_local(self) -> Vec3:\n        \"\"\"Calculate cell_size in the local coordinate system. Assumes `self.is_orthogonal_in_local()`.\"\"\"\n        return numpy.abs(self.get_transform('local', 'ortho').transform_vec(self.cell_size))\n\n    def _box_size_in_local(self) -> Vec3:\n        \"\"\"Calculate box_size in the local coordinate system. Assumes `self.is_orthogonal_in_local()`.\"\"\"\n        return numpy.abs(self.get_transform('local', 'ortho').transform_vec(self.box_size))\n\n    def _n_cells_in_local(self) -> NDArray[numpy.int_]:\n        \"\"\"Calculate n_cells after any local rotation. Assumes `self.is_orthogonal_in_local()`.\"\"\"\n        return numpy.abs(numpy.round(self.get_transform('local', 'ortho').transform_vec(self.n_cells)).astype(int))\n\n    def to_ortho(self) -> AffineTransform3D:\n        return self.get_transform('local', 'cell_box')\n\n    def transform_cell(self: HasCellT, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> HasCellT:\n        \"\"\"\n        Apply the given transform to the unit cell, and return a new `Cell`.\n        The transform is applied in coordinate frame 'frame'.\n        Orthogonal and affine transformations are applied to the affine matrix component,\n        while skew and scaling is applied to the orthogonalization matrix/cell_size.\n        \"\"\"\n        transform = t.cast(AffineTransform3D, self.change_transform(transform, 'local', frame))\n        if not transform.to_linear().is_orthogonal():\n            raise NotImplementedError()\n        return self.with_cell(Cell(\n            affine=transform @ self.affine,\n            ortho=self.ortho,\n            cell_size=self.cell_size,\n            cell_angle=self.cell_angle,\n            n_cells=self.n_cells,\n            pbc=self.pbc,\n        ))\n\n    def strain_orthogonal(self: HasCellT) -> HasCellT:\n        \"\"\"\n        Orthogonalize using strain.\n\n        Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n        For small displacements, no hydrostatic strain is applied (volume is conserved).\n        \"\"\"\n        return self.with_cell(Cell(\n            affine=self.affine,\n            ortho=LinearTransform3D(),\n            cell_size=self.cell_size,\n            n_cells=self.n_cells,\n            pbc=self.pbc,\n        ))\n\n    def repeat(self: HasCellT, n: t.Union[int, VecLike]) -> HasCellT:\n        \"\"\"Tile the cell by `n` in each dimension.\"\"\"\n        ns = numpy.broadcast_to(n, 3)\n        if not numpy.issubdtype(ns.dtype, numpy.integer):\n            raise ValueError(\"repeat() argument must be an integer or integer array.\")\n        return self.with_cell(Cell(\n            affine=self.affine,\n            ortho=self.ortho,\n            cell_size=self.cell_size,\n            cell_angle=self.cell_angle,\n            n_cells=self.n_cells * numpy.broadcast_to(n, 3),\n            pbc = self.pbc | (ns > 1)  # assume periodic along repeated directions\n        ))\n\n    def explode(self: HasCellT) -> HasCellT:\n        \"\"\"Materialize repeated cells as one supercell.\"\"\"\n        return self.with_cell(Cell(\n            affine=self.affine,\n            ortho=self.ortho,\n            cell_size=self.cell_size*self.n_cells,\n            cell_angle=self.cell_angle,\n            pbc=self.pbc,\n        ))\n\n    def explode_z(self: HasCellT) -> HasCellT:\n        \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n        return self.with_cell(Cell(\n            affine=self.affine,\n            ortho=self.ortho,\n            cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n            n_cells=[*self.n_cells[:2], 1],\n            cell_angle=self.cell_angle,\n            pbc=self.pbc,\n        ))\n\n    def crop(self: HasCellT, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n             y_min: float = -numpy.inf, y_max: float = numpy.inf,\n             z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n             frame: CoordinateFrame = 'local') -> HasCellT:\n        \"\"\"\n        Crop `self` to the given extents. For a non-orthogonal\n        cell, this must be specified in cell coordinates. This\n        function implicity `explode`s the cell as well.\n        \"\"\"\n\n        if not frame.lower().startswith('cell'):\n            if not self.is_orthogonal():\n                raise ValueError(\"Cannot crop a non-orthogonal cell in orthogonal coordinates. Use crop_atoms instead.\")\n\n        min = to_vec3([x_min, y_min, z_min])\n        max = to_vec3([x_max, y_max, z_max])\n        (min, max) = self.get_transform('cell_box', frame).transform([min, max])\n        new_box = BBox3D(min, max) & BBox3D.unit()\n        cropped = (new_box.min > 0.) | (new_box.max < 1.)\n\n        return self.with_cell(Cell(\n            affine=self.affine @ AffineTransform3D.translate(-new_box.min),\n            ortho=self.ortho,\n            cell_size=new_box.size * self.cell_size * numpy.where(cropped, self.n_cells, 1),\n            n_cells=numpy.where(cropped, 1, self.n_cells),\n            cell_angle=self.cell_angle,\n            pbc=self.pbc & ~cropped  # remove periodicity along cropped directions\n        ))\n\n    @t.overload\n    def change_transform(self, transform: AffineTransform3D,\n                         frame_to: t.Optional[CoordinateFrame] = None,\n                         frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n        ...\n\n    @t.overload\n    def change_transform(self, transform: Transform3D,\n                         frame_to: t.Optional[CoordinateFrame] = None,\n                         frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n        ...\n\n    def change_transform(self, transform: Transform3D,\n                         frame_to: t.Optional[CoordinateFrame] = None,\n                         frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n        \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n        if frame_to == frame_from and frame_to is not None:\n            return transform\n        coord_change = self.get_transform(frame_to, frame_from)\n        return coord_change @ transform @ coord_change.inverse()\n\n    def assert_equal(self, other: t.Any):\n        assert isinstance(other, HasCell) and type(self) is type(other)\n        numpy.testing.assert_array_almost_equal(self.affine.inner, other.affine.inner, 6)\n        numpy.testing.assert_array_almost_equal(self.ortho.inner, other.ortho.inner, 6)\n        numpy.testing.assert_array_almost_equal(self.cell_size, other.cell_size, 6)\n        numpy.testing.assert_array_equal(self.n_cells, other.n_cells)\n        numpy.testing.assert_array_equal(self.pbc, other.pbc)\n
    "},{"location":"api/#atomlib.HasCell.affine","title":"affine property","text":"
    affine: AffineTransform3D\n

    Affine transformation. Holds transformation from 'ortho' to 'local' coordinates, including rotation away from the standard crystal orientation.

    "},{"location":"api/#atomlib.HasCell.ortho","title":"ortho property","text":"
    ortho: LinearTransform3D\n

    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.

    "},{"location":"api/#atomlib.HasCell.metric","title":"metric property","text":"
    metric: LinearTransform3D\n

    Cell metric tensor

    Returns the dot product between every combination of basis vectors. :math:\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j

    "},{"location":"api/#atomlib.HasCell.cell_size","title":"cell_size property","text":"
    cell_size: NDArray[float64]\n

    Unit cell size.

    "},{"location":"api/#atomlib.HasCell.cell_angle","title":"cell_angle property","text":"
    cell_angle: NDArray[float64]\n

    Unit cell angles, in radians.

    "},{"location":"api/#atomlib.HasCell.n_cells","title":"n_cells property","text":"
    n_cells: NDArray[int_]\n

    Number of unit cells.

    "},{"location":"api/#atomlib.HasCell.pbc","title":"pbc property","text":"
    pbc: NDArray[bool_]\n

    Flags indicating the presence of periodic boundary conditions along each axis.

    "},{"location":"api/#atomlib.HasCell.ortho_size","title":"ortho_size property","text":"
    ortho_size: NDArray[float64]\n

    Return size of orthogonal unit cell.

    Equivalent to the diagonal of the orthogonalization matrix.

    "},{"location":"api/#atomlib.HasCell.box_size","title":"box_size property","text":"
    box_size: NDArray[float64]\n

    Return size of the cell box.

    Equivalent to self.n_cells * self.cell_size.

    "},{"location":"api/#atomlib.HasCell.bbox","title":"bbox class-attribute instance-attribute","text":"
    bbox = bbox_cell\n
    "},{"location":"api/#atomlib.HasCell.get_cell","title":"get_cell abstractmethod","text":"
    get_cell() -> Cell\n

    Get the cell contained in self. This should be a low cost method.

    Source code in atomlib/cell.py
    @abc.abstractmethod\ndef get_cell(self) -> Cell:\n    \"\"\"Get the cell contained in ``self``. This should be a low cost method.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasCell.with_cell","title":"with_cell abstractmethod","text":"
    with_cell(cell: Cell) -> HasCellT\n

    Replace the cell in self with cell.

    Source code in atomlib/cell.py
    @abc.abstractmethod\ndef with_cell(self: HasCellT, cell: Cell) -> HasCellT:\n    \"\"\"Replace the cell in ``self`` with ``cell``.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasCell.get_transform","title":"get_transform","text":"
    get_transform(\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n

    In the two-argument form, get the transform to frame_to from frame_from. In the one-argument form, get the transform from local coordinates to 'frame'.

    Source code in atomlib/cell.py
    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n    \"\"\"\n    In the two-argument form, get the transform to `frame_to` from `frame_from`.\n    In the one-argument form, get the transform from local coordinates to 'frame'.\n    \"\"\"\n    transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n    transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n    if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n        return AffineTransform3D()\n    return transform_to.inverse() @ transform_from\n
    "},{"location":"api/#atomlib.HasCell.corners","title":"corners","text":"
    corners(frame: CoordinateFrame = 'local') -> ndarray\n
    Source code in atomlib/cell.py
    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n    corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n    return self.get_transform(frame, 'cell_box') @ corners\n
    "},{"location":"api/#atomlib.HasCell.bbox_cell","title":"bbox_cell","text":"
    bbox_cell(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the bounding box of the cell box in the given coordinate system.

    Source code in atomlib/cell.py
    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n    return BBox3D.from_pts(self.corners(frame))\n
    "},{"location":"api/#atomlib.HasCell.is_orthogonal","title":"is_orthogonal","text":"
    is_orthogonal(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal (axes are at right angles.)

    Source code in atomlib/cell.py
    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n    return self.ortho.is_diagonal(tol=tol)\n
    "},{"location":"api/#atomlib.HasCell.is_orthogonal_in_local","title":"is_orthogonal_in_local","text":"
    is_orthogonal_in_local(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal and aligned with the local coordinate system.

    Source code in atomlib/cell.py
    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n    transform = (self.affine @ self.ortho).to_linear()\n    if not transform.is_scaled_orthogonal(tol):\n        return False\n    normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n    # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n    return all(\n        any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n        for row in normed\n    )\n
    "},{"location":"api/#atomlib.HasCell.to_ortho","title":"to_ortho","text":"
    to_ortho() -> AffineTransform3D\n
    Source code in atomlib/cell.py
    def to_ortho(self) -> AffineTransform3D:\n    return self.get_transform('local', 'cell_box')\n
    "},{"location":"api/#atomlib.HasCell.transform_cell","title":"transform_cell","text":"
    transform_cell(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> HasCellT\n

    Apply the given transform to the unit cell, and return a new Cell. The transform is applied in coordinate frame 'frame'. Orthogonal and affine transformations are applied to the affine matrix component, while skew and scaling is applied to the orthogonalization matrix/cell_size.

    Source code in atomlib/cell.py
    def transform_cell(self: HasCellT, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> HasCellT:\n    \"\"\"\n    Apply the given transform to the unit cell, and return a new `Cell`.\n    The transform is applied in coordinate frame 'frame'.\n    Orthogonal and affine transformations are applied to the affine matrix component,\n    while skew and scaling is applied to the orthogonalization matrix/cell_size.\n    \"\"\"\n    transform = t.cast(AffineTransform3D, self.change_transform(transform, 'local', frame))\n    if not transform.to_linear().is_orthogonal():\n        raise NotImplementedError()\n    return self.with_cell(Cell(\n        affine=transform @ self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size,\n        cell_angle=self.cell_angle,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.HasCell.strain_orthogonal","title":"strain_orthogonal","text":"
    strain_orthogonal() -> HasCellT\n

    Orthogonalize using strain.

    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane. For small displacements, no hydrostatic strain is applied (volume is conserved).

    Source code in atomlib/cell.py
    def strain_orthogonal(self: HasCellT) -> HasCellT:\n    \"\"\"\n    Orthogonalize using strain.\n\n    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n    For small displacements, no hydrostatic strain is applied (volume is conserved).\n    \"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=LinearTransform3D(),\n        cell_size=self.cell_size,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.HasCell.repeat","title":"repeat","text":"
    repeat(n: Union[int, VecLike]) -> HasCellT\n

    Tile the cell by n in each dimension.

    Source code in atomlib/cell.py
    def repeat(self: HasCellT, n: t.Union[int, VecLike]) -> HasCellT:\n    \"\"\"Tile the cell by `n` in each dimension.\"\"\"\n    ns = numpy.broadcast_to(n, 3)\n    if not numpy.issubdtype(ns.dtype, numpy.integer):\n        raise ValueError(\"repeat() argument must be an integer or integer array.\")\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size,\n        cell_angle=self.cell_angle,\n        n_cells=self.n_cells * numpy.broadcast_to(n, 3),\n        pbc = self.pbc | (ns > 1)  # assume periodic along repeated directions\n    ))\n
    "},{"location":"api/#atomlib.HasCell.explode","title":"explode","text":"
    explode() -> HasCellT\n

    Materialize repeated cells as one supercell.

    Source code in atomlib/cell.py
    def explode(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*self.n_cells,\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.HasCell.explode_z","title":"explode_z","text":"
    explode_z() -> HasCellT\n

    Materialize repeated cells as one supercell in z.

    Source code in atomlib/cell.py
    def explode_z(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n        n_cells=[*self.n_cells[:2], 1],\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.HasCell.crop","title":"crop","text":"
    crop(\n    x_min: float = -numpy.inf,\n    x_max: float = numpy.inf,\n    y_min: float = -numpy.inf,\n    y_max: float = numpy.inf,\n    z_min: float = -numpy.inf,\n    z_max: float = numpy.inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> HasCellT\n

    Crop self to the given extents. For a non-orthogonal cell, this must be specified in cell coordinates. This function implicity explodes the cell as well.

    Source code in atomlib/cell.py
    def crop(self: HasCellT, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n         frame: CoordinateFrame = 'local') -> HasCellT:\n    \"\"\"\n    Crop `self` to the given extents. For a non-orthogonal\n    cell, this must be specified in cell coordinates. This\n    function implicity `explode`s the cell as well.\n    \"\"\"\n\n    if not frame.lower().startswith('cell'):\n        if not self.is_orthogonal():\n            raise ValueError(\"Cannot crop a non-orthogonal cell in orthogonal coordinates. Use crop_atoms instead.\")\n\n    min = to_vec3([x_min, y_min, z_min])\n    max = to_vec3([x_max, y_max, z_max])\n    (min, max) = self.get_transform('cell_box', frame).transform([min, max])\n    new_box = BBox3D(min, max) & BBox3D.unit()\n    cropped = (new_box.min > 0.) | (new_box.max < 1.)\n\n    return self.with_cell(Cell(\n        affine=self.affine @ AffineTransform3D.translate(-new_box.min),\n        ortho=self.ortho,\n        cell_size=new_box.size * self.cell_size * numpy.where(cropped, self.n_cells, 1),\n        n_cells=numpy.where(cropped, 1, self.n_cells),\n        cell_angle=self.cell_angle,\n        pbc=self.pbc & ~cropped  # remove periodicity along cropped directions\n    ))\n
    "},{"location":"api/#atomlib.HasCell.change_transform","title":"change_transform","text":"
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n

    Coordinate-change a transformation from frame_from into frame_to.

    Source code in atomlib/cell.py
    def change_transform(self, transform: Transform3D,\n                     frame_to: t.Optional[CoordinateFrame] = None,\n                     frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n    \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n    if frame_to == frame_from and frame_to is not None:\n        return transform\n    coord_change = self.get_transform(frame_to, frame_from)\n    return coord_change @ transform @ coord_change.inverse()\n
    "},{"location":"api/#atomlib.HasCell.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n
    Source code in atomlib/cell.py
    def assert_equal(self, other: t.Any):\n    assert isinstance(other, HasCell) and type(self) is type(other)\n    numpy.testing.assert_array_almost_equal(self.affine.inner, other.affine.inner, 6)\n    numpy.testing.assert_array_almost_equal(self.ortho.inner, other.ortho.inner, 6)\n    numpy.testing.assert_array_almost_equal(self.cell_size, other.cell_size, 6)\n    numpy.testing.assert_array_equal(self.n_cells, other.n_cells)\n    numpy.testing.assert_array_equal(self.pbc, other.pbc)\n
    "},{"location":"api/#atomlib.AtomCell","title":"AtomCell dataclass","text":"

    Bases: AtomCellIOMixin, HasAtomCell

    Cell of atoms with known size and periodic boundary conditions.

    Source code in atomlib/atomcell.py
    @dataclass(init=False, repr=False, frozen=True)\nclass AtomCell(AtomCellIOMixin, HasAtomCell):\n    \"\"\"\n    Cell of atoms with known size and periodic boundary conditions.\n    \"\"\"\n\n    atoms: Atoms\n    \"\"\"Atoms in the cell. Stored in 'local' coordinates (i.e. relative to the enclosing group but not relative to box dimensions).\"\"\"\n\n    cell: Cell\n    \"\"\"Cell coordinate system.\"\"\"\n\n    frame: CoordinateFrame = 'local'\n    \"\"\"Coordinate frame 'atoms' are stored in.\"\"\"\n\n    def get_cell(self) -> Cell:\n        return self.cell\n\n    def with_cell(self, cell: Cell) -> Self:\n        return self.__class__(self.atoms, cell, frame=self.frame, keep_frame=True)\n\n    def get_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> Atoms:\n        \"\"\"Get atoms contained in ``self``, in the given coordinate frame.\"\"\"\n\n        if frame is None or frame == self.get_frame():\n            return self.atoms\n        return self.atoms.transform(self.get_transform(frame, self.get_frame()))\n\n    def with_atoms(self, atoms: HasAtoms, frame: t.Optional[CoordinateFrame] = None) -> Self:\n        frame = frame if frame is not None else self.frame\n        return self.__class__(atoms.get_atoms(), cell=self.cell, frame=frame, keep_frame=True)\n        #return replace(self, atoms=atoms, frame = frame if frame is not None else self.frame, keep_frame=True)\n\n    def get_frame(self) -> CoordinateFrame:\n        \"\"\"Get the coordinate frame atoms are stored in.\"\"\"\n        return self.frame\n\n    @classmethod\n    def _combine_metadata(cls: t.Type[AtomCellT], *atoms: HasAtoms, n: t.Optional[int] = None) -> AtomCellT:\n        \"\"\"\n        When combining multiple [`HasAtoms`][atomlib.atoms.HasAtoms], check that they are compatible with each other,\n        and return a 'representative' which best represents the combined metadata.\n        Implementors should treat [`Atoms`][atomlib.atoms.Atoms] as acceptable, but having no metadata.\n        \"\"\"\n        if n is not None:\n            rep = atoms[n]\n            if not isinstance(rep, AtomCell):\n                raise ValueError(f\"Atoms #{n} has no cell\")\n        else:\n            atom_cells = [a for a in atoms if isinstance(a, AtomCell)]\n            if len(atom_cells) == 0:\n                raise TypeError(\"No AtomCells to combine\")\n            rep = atom_cells[0]\n            if not all(a.cell == rep.cell for a in atom_cells[1:]):\n                raise TypeError(\"Can't combine AtomCells with different cells\")\n\n        return cls(Atoms.empty(), frame=rep.frame, cell=rep.cell)\n\n    @classmethod\n    def from_ortho(cls, atoms: IntoAtoms, ortho: LinearTransform3D, *,\n                   n_cells: t.Optional[VecLike] = None,\n                   frame: CoordinateFrame = 'local',\n                   keep_frame: bool = False):\n        \"\"\"\n        Make an atom cell given a list of atoms and an orthogonalization matrix.\n        Atoms are assumed to be in the coordinate system `frame`.\n        \"\"\"\n        cell = Cell.from_ortho(ortho, n_cells)\n        return cls(atoms, cell, frame=frame, keep_frame=keep_frame)\n\n    @classmethod\n    def from_unit_cell(cls, atoms: IntoAtoms, cell_size: VecLike,\n                       cell_angle: t.Optional[VecLike] = None, *,\n                       n_cells: t.Optional[VecLike] = None,\n                       frame: CoordinateFrame = 'local',\n                       keep_frame: bool = False):\n        \"\"\"\n        Make a cell given a list of atoms and unit cell parameters.\n        Atoms are assumed to be in the coordinate system `frame`.\n        \"\"\"\n        cell = Cell.from_unit_cell(cell_size, cell_angle, n_cells=n_cells)\n        return cls(atoms, cell, frame=frame, keep_frame=keep_frame)\n\n    def __init__(self, atoms: IntoAtoms, cell: Cell, *,\n                 frame: CoordinateFrame = 'local',\n                 keep_frame: bool = False):\n        atoms = Atoms(atoms)\n        # by default, store in local coordinates\n        if not keep_frame and frame != 'local':\n            atoms = atoms.transform(cell.get_transform('local', frame))\n            frame = 'local'\n\n        object.__setattr__(self, 'atoms', atoms)\n        object.__setattr__(self, 'cell', cell)\n        object.__setattr__(self, 'frame', frame)\n\n        self.__post_init__()\n\n    def __post_init__(self):\n        pass\n\n    def orthogonalize(self) -> OrthoCell:\n        if self.is_orthogonal():\n            return OrthoCell(self.atoms, self.cell, frame=self.frame)\n        raise NotImplementedError()\n\n    def clone(self: AtomCellT) -> AtomCellT:\n        \"\"\"Make a deep copy of `self`.\"\"\"\n        return self.__class__(**{field.name: copy.deepcopy(getattr(self, field.name)) for field in fields(self)})\n\n    def assert_equal(self, other: t.Any):\n        \"\"\"Assert this structure is equal to `other`\"\"\"\n        assert isinstance(other, AtomCell)\n        self.cell.assert_equal(other.cell)\n        self.get_atoms('local').assert_equal(other.get_atoms('local'))\n\n    def _str_parts(self) -> t.Iterable[t.Any]:\n        return (\n            f\"Cell size:  {self.cell.cell_size!s}\",\n            f\"Cell angle: {self.cell.cell_angle!s}\",\n            f\"# Cells: {self.cell.n_cells!s}\",\n            f\"Frame: {self.frame}\",\n            self.atoms,\n        )\n\n    def __str__(self) -> str:\n        return \"\\n\".join(map(str, self._str_parts()))\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}({self.atoms!r}, cell={self.cell!r}, frame={self.frame})\"\n\n    def _repr_pretty_(self, p: t.Any, cycle: bool) -> None:\n        p.text(f'{self.__class__.__name__}(...)') if cycle else p.text(str(self))\n
    "},{"location":"api/#atomlib.AtomCell.affine","title":"affine property","text":"
    affine: AffineTransform3D\n

    Affine transformation. Holds transformation from 'ortho' to 'local' coordinates, including rotation away from the standard crystal orientation.

    "},{"location":"api/#atomlib.AtomCell.ortho","title":"ortho property","text":"
    ortho: LinearTransform3D\n

    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.

    "},{"location":"api/#atomlib.AtomCell.metric","title":"metric property","text":"
    metric: LinearTransform3D\n

    Cell metric tensor

    Returns the dot product between every combination of basis vectors. :math:\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j

    "},{"location":"api/#atomlib.AtomCell.cell_size","title":"cell_size property","text":"
    cell_size: NDArray[float64]\n

    Unit cell size.

    "},{"location":"api/#atomlib.AtomCell.cell_angle","title":"cell_angle property","text":"
    cell_angle: NDArray[float64]\n

    Unit cell angles, in radians.

    "},{"location":"api/#atomlib.AtomCell.n_cells","title":"n_cells property","text":"
    n_cells: NDArray[int_]\n

    Number of unit cells.

    "},{"location":"api/#atomlib.AtomCell.pbc","title":"pbc property","text":"
    pbc: NDArray[bool_]\n

    Flags indicating the presence of periodic boundary conditions along each axis.

    "},{"location":"api/#atomlib.AtomCell.ortho_size","title":"ortho_size property","text":"
    ortho_size: NDArray[float64]\n

    Return size of orthogonal unit cell.

    Equivalent to the diagonal of the orthogonalization matrix.

    "},{"location":"api/#atomlib.AtomCell.box_size","title":"box_size property","text":"
    box_size: NDArray[float64]\n

    Return size of the cell box.

    Equivalent to self.n_cells * self.cell_size.

    "},{"location":"api/#atomlib.AtomCell.columns","title":"columns property","text":"
    columns: List[str]\n

    Return the column names in self.

    RETURNS DESCRIPTION List[str]

    A sequence of column names

    "},{"location":"api/#atomlib.AtomCell.dtypes","title":"dtypes property","text":"
    dtypes: List[DataType]\n

    Return the datatypes in self.

    RETURNS DESCRIPTION List[DataType]

    A sequence of column DataTypes

    "},{"location":"api/#atomlib.AtomCell.schema","title":"schema property","text":"
    schema: Schema\n

    Return the schema of self.

    RETURNS DESCRIPTION Schema

    A dictionary of column names and DataTypes

    "},{"location":"api/#atomlib.AtomCell.with_column","title":"with_column class-attribute instance-attribute","text":"
    with_column = with_columns\n
    "},{"location":"api/#atomlib.AtomCell.unique","title":"unique class-attribute instance-attribute","text":"
    unique = deduplicate\n
    "},{"location":"api/#atomlib.AtomCell.atoms","title":"atoms instance-attribute","text":"
    atoms: Atoms\n

    Atoms in the cell. Stored in 'local' coordinates (i.e. relative to the enclosing group but not relative to box dimensions).

    "},{"location":"api/#atomlib.AtomCell.cell","title":"cell instance-attribute","text":"
    cell: Cell\n

    Cell coordinate system.

    "},{"location":"api/#atomlib.AtomCell.frame","title":"frame class-attribute instance-attribute","text":"
    frame: CoordinateFrame = 'local'\n

    Coordinate frame 'atoms' are stored in.

    "},{"location":"api/#atomlib.AtomCell.get_transform","title":"get_transform","text":"
    get_transform(\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n

    In the two-argument form, get the transform to frame_to from frame_from. In the one-argument form, get the transform from local coordinates to 'frame'.

    Source code in atomlib/cell.py
    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n    \"\"\"\n    In the two-argument form, get the transform to `frame_to` from `frame_from`.\n    In the one-argument form, get the transform from local coordinates to 'frame'.\n    \"\"\"\n    transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n    transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n    if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n        return AffineTransform3D()\n    return transform_to.inverse() @ transform_from\n
    "},{"location":"api/#atomlib.AtomCell.corners","title":"corners","text":"
    corners(frame: CoordinateFrame = 'local') -> ndarray\n
    Source code in atomlib/cell.py
    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n    corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n    return self.get_transform(frame, 'cell_box') @ corners\n
    "},{"location":"api/#atomlib.AtomCell.bbox_cell","title":"bbox_cell","text":"
    bbox_cell(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the bounding box of the cell box in the given coordinate system.

    Source code in atomlib/cell.py
    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n    return BBox3D.from_pts(self.corners(frame))\n
    "},{"location":"api/#atomlib.AtomCell.bbox","title":"bbox","text":"
    bbox(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the combined bounding box of the cell and atoms in the given coordinate system. To get the cell or atoms bounding box only, use bbox_cell or bbox_atoms.

    Source code in atomlib/atomcell.py
    def bbox(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"\n    Return the combined bounding box of the cell and atoms in the given coordinate system.\n    To get the cell or atoms bounding box only, use [`bbox_cell`][atomlib.atomcell.HasAtomCell.bbox_cell] or [`bbox_atoms`][atomlib.atomcell.HasAtomCell.bbox_atoms].\n    \"\"\"\n    return self.bbox_atoms(frame) | self.bbox_cell(frame)\n
    "},{"location":"api/#atomlib.AtomCell.is_orthogonal","title":"is_orthogonal","text":"
    is_orthogonal(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal (axes are at right angles.)

    Source code in atomlib/cell.py
    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n    return self.ortho.is_diagonal(tol=tol)\n
    "},{"location":"api/#atomlib.AtomCell.is_orthogonal_in_local","title":"is_orthogonal_in_local","text":"
    is_orthogonal_in_local(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal and aligned with the local coordinate system.

    Source code in atomlib/cell.py
    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n    transform = (self.affine @ self.ortho).to_linear()\n    if not transform.is_scaled_orthogonal(tol):\n        return False\n    normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n    # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n    return all(\n        any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n        for row in normed\n    )\n
    "},{"location":"api/#atomlib.AtomCell.to_ortho","title":"to_ortho","text":"
    to_ortho() -> AffineTransform3D\n
    Source code in atomlib/cell.py
    def to_ortho(self) -> AffineTransform3D:\n    return self.get_transform('local', 'cell_box')\n
    "},{"location":"api/#atomlib.AtomCell.transform_cell","title":"transform_cell","text":"
    transform_cell(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> Self\n

    Apply the given transform to the unit cell, without changing atom positions. The transform is applied in coordinate frame 'frame'.

    Source code in atomlib/atomcell.py
    def transform_cell(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n    \"\"\"\n    Apply the given transform to the unit cell, without changing atom positions.\n    The transform is applied in coordinate frame 'frame'.\n    \"\"\"\n    return self.with_cell(self.get_cell().transform_cell(transform, frame=frame))\n
    "},{"location":"api/#atomlib.AtomCell.strain_orthogonal","title":"strain_orthogonal","text":"
    strain_orthogonal() -> HasCellT\n

    Orthogonalize using strain.

    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane. For small displacements, no hydrostatic strain is applied (volume is conserved).

    Source code in atomlib/cell.py
    def strain_orthogonal(self: HasCellT) -> HasCellT:\n    \"\"\"\n    Orthogonalize using strain.\n\n    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n    For small displacements, no hydrostatic strain is applied (volume is conserved).\n    \"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=LinearTransform3D(),\n        cell_size=self.cell_size,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.AtomCell.repeat","title":"repeat","text":"
    repeat(n: Union[int, VecLike]) -> Self\n

    Tile the cell

    Source code in atomlib/atomcell.py
    def repeat(self, n: t.Union[int, VecLike]) -> Self:\n    \"\"\"Tile the cell\"\"\"\n    ns = numpy.broadcast_to(n, 3)\n    if not numpy.issubdtype(ns.dtype, numpy.integer):\n        raise ValueError(\"repeat() argument must be an integer or integer array.\")\n\n    cells = numpy.stack(numpy.meshgrid(*map(numpy.arange, ns))) \\\n        .reshape(3, -1).T.astype(float)\n    cells = cells * self.box_size\n\n    atoms = self.get_atoms('cell')\n    atoms = Atoms.concat([\n        atoms.transform(AffineTransform3D.translate(cell))\n        for cell in cells\n    ]) #.transform(self.cell.get_transform('local', 'cell_frac'))\n    return self.with_atoms(atoms, 'cell').with_cell(self.get_cell().repeat(ns))\n
    "},{"location":"api/#atomlib.AtomCell.explode","title":"explode","text":"
    explode() -> Self\n

    Materialize repeated cells as one supercell.

    Source code in atomlib/atomcell.py
    def explode(self) -> Self:\n    \"\"\"Materialize repeated cells as one supercell.\"\"\"\n    frame = self.get_frame()\n\n    return self.with_atoms(self.get_atoms('local'), 'local') \\\n        .with_cell(self.get_cell().explode()) \\\n        .to_frame(frame)\n
    "},{"location":"api/#atomlib.AtomCell.explode_z","title":"explode_z","text":"
    explode_z() -> HasCellT\n

    Materialize repeated cells as one supercell in z.

    Source code in atomlib/cell.py
    def explode_z(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n        n_cells=[*self.n_cells[:2], 1],\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.AtomCell.crop","title":"crop","text":"
    crop(\n    x_min: float = -numpy.inf,\n    x_max: float = numpy.inf,\n    y_min: float = -numpy.inf,\n    y_max: float = numpy.inf,\n    z_min: float = -numpy.inf,\n    z_max: float = numpy.inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> Self\n

    Crop atoms and cell to the given extents. For a non-orthogonal cell, this must be specified in cell coordinates. This function implicity explodes the cell as well.

    To crop atoms only, use crop_atoms instead.

    Source code in atomlib/atomcell.py
    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n         frame: CoordinateFrame = 'local') -> Self:\n    \"\"\"\n    Crop atoms and cell to the given extents. For a non-orthogonal\n    cell, this must be specified in cell coordinates. This\n    function implicity `explode`s the cell as well.\n\n    To crop atoms only, use `crop_atoms` instead.\n    \"\"\"\n\n    cell = self.get_cell().crop(x_min, x_max, y_min, y_max, z_min, z_max, frame=frame)\n    atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n    return self.with_cell(cell).with_atoms(atoms)\n
    "},{"location":"api/#atomlib.AtomCell.change_transform","title":"change_transform","text":"
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n

    Coordinate-change a transformation from frame_from into frame_to.

    Source code in atomlib/cell.py
    def change_transform(self, transform: Transform3D,\n                     frame_to: t.Optional[CoordinateFrame] = None,\n                     frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n    \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n    if frame_to == frame_from and frame_to is not None:\n        return transform\n    coord_change = self.get_transform(frame_to, frame_from)\n    return coord_change @ transform @ coord_change.inverse()\n
    "},{"location":"api/#atomlib.AtomCell.describe","title":"describe","text":"
    describe(\n    percentiles: Union[Sequence[float], float, None] = (\n        0.25,\n        0.5,\n        0.75,\n    ),\n    *,\n    interpolation: RollingInterpolationMethod = \"nearest\",\n    frame: Optional[CoordinateFrame] = None\n) -> DataFrame\n

    Return summary statistics for self. See DataFrame.describe for more information.

    PARAMETER DESCRIPTION percentiles

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), 50% (median), and 75% (third quartile).

    TYPE: Union[Sequence[float], float, None] DEFAULT: (0.25, 0.5, 0.75)

    RETURNS DESCRIPTION DataFrame

    A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n             interpolation: RollingInterpolationMethod = 'nearest',\n             frame: t.Optional[CoordinateFrame] = None) -> polars.DataFrame:\n    \"\"\"\n    Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n    Args:\n      percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                   50% (median), and 75% (third quartile).\n\n    Returns:\n      A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.with_columns","title":"with_columns","text":"
    with_columns(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self\n

    Return a copy of self with the given columns added.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_columns(self,\n                 *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 frame: t.Optional[CoordinateFrame] = None,\n                 **named_exprs: IntoExpr) -> Self:\n    \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.insert_column","title":"insert_column","text":"
    insert_column(index: int, column: Series) -> DataFrame\n
    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n    return self._get_frame().insert_column(index, column)\n
    "},{"location":"api/#atomlib.AtomCell.get_column","title":"get_column","text":"
    get_column(\n    name: str, *, frame: Optional[CoordinateFrame] = None\n) -> Series\n

    Get the specified column from self, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef get_column(self, name: str, *, frame: t.Optional[CoordinateFrame] = None) -> polars.Series:\n    \"\"\"\n    Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.get_columns","title":"get_columns","text":"
    get_columns(\n    *, frame: Optional[CoordinateFrame] = None\n) -> List[Series]\n

    Return all columns from self as a list of Series.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef get_columns(self, *, frame: t.Optional[CoordinateFrame] = None) -> t.List[polars.Series]:\n    \"\"\"\n    Return all columns from `self` as a list of [`Series`][polars.Series].\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.get_column_index","title":"get_column_index","text":"
    get_column_index(name: str) -> int\n

    Get the index of a column by name, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_column_index)\ndef get_column_index(self, name: str) -> int:\n    \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.group_by","title":"group_by","text":"
    group_by(\n    *by: Union[IntoExpr, Iterable[IntoExpr]],\n    maintain_order: bool = False,\n    frame: Optional[CoordinateFrame] = None,\n    **named_by: IntoExpr\n) -> GroupBy\n

    Start a group by operation. See DataFrame.group_by for more information.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n             maintain_order: bool = False, frame: t.Optional[CoordinateFrame] = None,\n             **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n    \"\"\"\n    Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.pipe","title":"pipe","text":"
    pipe(\n    function: Callable[Concatenate[HasAtomCellT, P], T],\n    *args: args,\n    **kwargs: kwargs\n) -> T\n

    Apply function to self (in method-call syntax).

    Source code in atomlib/atomcell.py
    def pipe(self: HasAtomCellT, function: t.Callable[Concatenate[HasAtomCellT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n    \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n    return function(self, *args, **kwargs)\n
    "},{"location":"api/#atomlib.AtomCell.drop","title":"drop","text":"
    drop(\n    *columns: Union[str, Iterable[str]], strict: bool = True\n) -> DataFrame\n

    Return self with the specified columns removed.

    Source code in atomlib/atoms.py
    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n    \"\"\"Return `self` with the specified columns removed.\"\"\"\n    return self._get_frame().drop(*columns, strict=strict)\n
    "},{"location":"api/#atomlib.AtomCell.filter","title":"filter","text":"
    filter(\n    *predicates: Union[\n        None,\n        IntoExprColumn,\n        Iterable[IntoExprColumn],\n        bool,\n        List[bool],\n        ndarray,\n    ],\n    frame: Optional[CoordinateFrame] = None,\n    **constraints: Any\n) -> Self\n

    Filter self, removing rows which evaluate to False.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef filter(\n    self,\n    *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n    frame: t.Optional[CoordinateFrame] = None,\n    **constraints: t.Any,\n) -> Self:\n    \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.sort","title":"sort","text":"
    sort(\n    by: Union[IntoExpr, Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: Union[bool, Sequence[bool]] = False,\n    nulls_last: bool = False\n) -> Self\n

    Sort the atoms in self by the given columns/expressions.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef sort(\n    self,\n    by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: t.Union[bool, t.Sequence[bool]] = False,\n    nulls_last: bool = False,\n) -> Self:\n    \"\"\"\n    Sort the atoms in `self` by the given columns/expressions.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.slice","title":"slice","text":"
    slice(\n    offset: int,\n    length: Optional[int] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return a slice of the rows in self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef slice(self, offset: int, length: t.Optional[int] = None, *,\n          frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return a slice of the rows in `self`.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.head","title":"head","text":"
    head(\n    n: int = 5, *, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return the first n rows of self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef head(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return the first `n` rows of `self`.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.tail","title":"tail","text":"
    tail(\n    n: int = 5, *, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return the last n rows of self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef tail(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return the last `n` rows of `self`.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.drop_nulls","title":"drop_nulls","text":"
    drop_nulls(\n    subset: Union[str, Collection[str], None] = None\n) -> DataFrame\n

    Drop rows that contain nulls in any of columns subset.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n    \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n    return self._get_frame().drop_nulls(subset)\n
    "},{"location":"api/#atomlib.AtomCell.fill_null","title":"fill_null","text":"
    fill_null(\n    value: Any = None,\n    strategy: Optional[FillNullStrategy] = None,\n    limit: Optional[int] = None,\n    matches_supertype: bool = True,\n) -> Self\n
    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef fill_null(\n    self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n    limit: t.Optional[int] = None, matches_supertype: bool = True,\n) -> Self:\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.fill_nan","title":"fill_nan","text":"
    fill_nan(\n    value: Union[Expr, int, float, None],\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n
    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef fill_nan(self, value: t.Union[polars.Expr, int, float, None], *,\n             frame: t.Optional[CoordinateFrame] = None) -> Self:\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.concat","title":"concat classmethod","text":"
    concat(\n    atoms: Union[\n        HasAtomsT,\n        IntoAtoms,\n        Iterable[Union[HasAtomsT, IntoAtoms]],\n    ],\n    *,\n    rechunk: bool = True,\n    how: ConcatMethod = \"vertical\"\n) -> HasAtomsT\n

    Concatenate multiple Atoms together, handling metadata appropriately.

    Source code in atomlib/atoms.py
    @classmethod\ndef concat(cls: t.Type[HasAtomsT],\n           atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n           rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n    \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n    # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n    # same type as ``cls``.\n    if _is_abstract(cls):\n        raise TypeError(\"concat() must be called on a concrete class.\")\n\n    if isinstance(atoms, HasAtoms):\n        atoms = (atoms,)\n    dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n    representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n    if len(dfs) == 0:\n        return representative.with_atoms(Atoms.empty(), 'local')\n\n    if how in ('vertical', 'vertical_relaxed'):\n        # get order from first member\n        cols = dfs[0].columns\n        dfs = [df.select(cols) for df in dfs]\n    elif how == 'inner':\n        cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n        schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n        if len(schema) == 0:\n            raise ValueError(\"Atoms have no columns in common\")\n\n        dfs = [_select_schema(df, schema) for df in dfs]\n        how = 'vertical'\n\n    return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n
    "},{"location":"api/#atomlib.AtomCell.partition_by","title":"partition_by","text":"
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: bool = False\n) -> Union[List[Self], Dict[Any, Self]]\n

    Group by the given columns and partition into separate dataframes.

    Return the partitions as a dictionary by specifying as_dict=True.

    Source code in atomlib/atoms.py
    def partition_by(\n    self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n    maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n    \"\"\"\n    Group by the given columns and partition into separate dataframes.\n\n    Return the partitions as a dictionary by specifying `as_dict=True`.\n    \"\"\"\n    if as_dict:\n        d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n        return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n    return [\n        self.with_atoms(Atoms(df, _unchecked=True))\n        for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n    ]\n
    "},{"location":"api/#atomlib.AtomCell.select","title":"select","text":"
    select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef select(\n    self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> polars.DataFrame:\n    \"\"\"\n    Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.select_schema","title":"select_schema","text":"
    select_schema(schema: SchemaDict) -> DataFrame\n

    Select columns from self and cast to the given schema. Raises TypeError if a column is not found or if it can't be cast.

    Source code in atomlib/atoms.py
    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n    \"\"\"\n    Select columns from `self` and cast to the given schema.\n    Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n    \"\"\"\n    return _select_schema(self, schema)\n
    "},{"location":"api/#atomlib.AtomCell.select_props","title":"select_props","text":"
    select_props(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self\n

    Select exprs from self, while keeping required columns. Doesn't affect the cell.

    RETURNS DESCRIPTION Self

    A HasAtomCell filtered to contain

    Self

    the specified properties (as well as required columns).

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef select_props(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self:\n    \"\"\"\n    Select `exprs` from `self`, while keeping required columns.\n    Doesn't affect the cell.\n\n    Returns:\n      A [`HasAtomCell`][atomlib.atomcell.HasAtomCell] filtered to contain\n      the specified properties (as well as required columns).\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.try_select","title":"try_select","text":"
    try_select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Optional[DataFrame]\n

    Try to select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns. Returns None if any columns are missing.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef try_select(\n    self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> t.Optional[polars.DataFrame]:\n    \"\"\"\n    Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns. Returns `None` if any\n    columns are missing.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.try_get_column","title":"try_get_column","text":"
    try_get_column(name: str) -> Optional[Series]\n

    Try to get a column from self, returning None if it doesn't exist.

    Source code in atomlib/atoms.py
    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n    \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n    try:\n        return self.get_column(name)\n    except polars.exceptions.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/#atomlib.AtomCell.bbox_atoms","title":"bbox_atoms","text":"
    bbox_atoms(\n    frame: Optional[CoordinateFrame] = None,\n) -> BBox3D\n

    Return the bounding box of all the atoms in self, in the given coordinate frame.

    Source code in atomlib/atomcell.py
    def bbox_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> BBox3D:\n    \"\"\"Return the bounding box of all the atoms in `self`, in the given coordinate frame.\"\"\"\n    return self.get_atoms(frame).bbox()\n
    "},{"location":"api/#atomlib.AtomCell.transform_atoms","title":"transform_atoms","text":"
    transform_atoms(\n    transform: IntoTransform3D,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: CoordinateFrame = \"local\",\n    transform_velocities: bool = False\n) -> Self\n

    Transform the atoms in self by transform. If selection is given, only transform the atoms in selection.

    Source code in atomlib/atomcell.py
    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                    frame: CoordinateFrame = 'local', transform_velocities: bool = False) -> Self:\n    \"\"\"\n    Transform the atoms in `self` by `transform`.\n    If `selection` is given, only transform the atoms in `selection`.\n    \"\"\"\n    transform = self.change_transform(Transform3D.make(transform), self.get_frame(), frame)\n    return self.with_atoms(self.get_atoms(self.get_frame()).transform(transform, selection, transform_velocities=transform_velocities))\n
    "},{"location":"api/#atomlib.AtomCell.transform","title":"transform","text":"
    transform(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> Self\n
    Source code in atomlib/atomcell.py
    def transform(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n    if isinstance(transform, Transform3D) and not isinstance(transform, AffineTransform3D):\n        raise ValueError(\"Non-affine transforms cannot change the box dimensions. Use 'transform_atoms' instead.\")\n    # TODO: cleanup once tests pass\n    # coordinate change the transform into atomic coordinates\n    new_cell = self.get_cell().transform_cell(transform, frame)\n    transform = self.get_cell().change_transform(transform, self.get_frame(), frame)\n    return self.with_atoms(self.get_atoms().transform(transform), self.get_frame()).with_cell(new_cell)\n
    "},{"location":"api/#atomlib.AtomCell.round_near_zero","title":"round_near_zero","text":"
    round_near_zero(\n    tol: float = 1e-14,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Round atom position values near zero to zero.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef round_near_zero(self, tol: float = 1e-14, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Round atom position values near zero to zero.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.crop_atoms","title":"crop_atoms","text":"
    crop_atoms(\n    x_min: float = -numpy.inf,\n    x_max: float = numpy.inf,\n    y_min: float = -numpy.inf,\n    y_max: float = numpy.inf,\n    z_min: float = -numpy.inf,\n    z_max: float = numpy.inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> Self\n
    Source code in atomlib/atomcell.py
    def crop_atoms(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n               y_min: float = -numpy.inf, y_max: float = numpy.inf,\n               z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n               frame: CoordinateFrame = 'local') -> Self:\n    atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n    return self.with_atoms(atoms)\n
    "},{"location":"api/#atomlib.AtomCell.deduplicate","title":"deduplicate","text":"
    deduplicate(\n    tol: float = 0.001,\n    subset: Iterable[str] = (\"x\", \"y\", \"z\", \"symbol\"),\n    keep: UniqueKeepStrategy = \"first\",\n    maintain_order: bool = True,\n) -> Self\n

    De-duplicate atoms in self. Atoms of the same symbol that are closer than tolerance to each other (by Euclidian distance) will be removed, leaving only the atom specified by keep (defaults to the first atom).

    If subset is specified, only those columns will be included while assessing duplicates. Floating point columns other than 'x', 'y', and 'z' will not by toleranced.

    Source code in atomlib/atoms.py
    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n    \"\"\"\n    De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n    to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n    `keep` (defaults to the first atom).\n\n    If `subset` is specified, only those columns will be included while assessing duplicates.\n    Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n    \"\"\"\n    import scipy.spatial\n\n    cols = set((subset,) if isinstance(subset, str) else subset)\n\n    indices = numpy.arange(len(self))\n\n    spatial_cols = cols.intersection(('x', 'y', 'z'))\n    cols -= spatial_cols\n    if len(spatial_cols) > 0:\n        coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n        tree = scipy.spatial.KDTree(coords)\n\n        # TODO This is a bad algorithm\n        while True:\n            changed = False\n            for (i, j) in tree.query_pairs(tol, 2.):\n                # whenever we encounter a pair, ensure their index matches\n                i_i, i_j = indices[[i, j]]\n                if i_i != i_j:\n                    indices[i] = indices[j] = min(i_i, i_j)\n                    changed = True\n            if not changed:\n                break\n\n        self = self.with_column(polars.Series('_unique_pts', indices))\n        cols.add('_unique_pts')\n\n    frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n    if len(spatial_cols) > 0:\n        frame = frame.drop('_unique_pts')\n\n    return self.with_atoms(Atoms(frame, _unchecked=True))\n
    "},{"location":"api/#atomlib.AtomCell.with_bounds","title":"with_bounds","text":"
    with_bounds(\n    cell_size: Optional[VecLike] = None,\n    cell_origin: Optional[VecLike] = None,\n) -> \"AtomCell\"\n

    Return a periodic cell with the given orthogonal cell dimensions.

    If cell_size is not specified, it will be assumed (and may be incorrect).

    Source code in atomlib/atoms.py
    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n    \"\"\"\n    Return a periodic cell with the given orthogonal cell dimensions.\n\n    If cell_size is not specified, it will be assumed (and may be incorrect).\n    \"\"\"\n    # TODO: test this\n    from .atomcell import AtomCell\n\n    if cell_size is None:\n        warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n        cell_size = self.bbox().size\n        cell_origin = self.bbox().min\n\n    # TODO test this origin code\n    cell = Cell.from_unit_cell(cell_size)\n    if cell_origin is not None:\n        cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n    return AtomCell(self.get_atoms(), cell, frame='local')\n
    "},{"location":"api/#atomlib.AtomCell.coords","title":"coords","text":"
    coords(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> NDArray[float64]\n

    Return a (N, 3) ndarray of atom positions (dtype numpy.float64) in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> NDArray[numpy.float64]:\n    \"\"\"\n    Return a `(N, 3)` ndarray of atom positions (dtype [`numpy.float64`][numpy.float64])\n    in the given coordinate frame.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.x","title":"x","text":"
    x() -> Expr\n
    Source code in atomlib/atoms.py
    def x(self) -> polars.Expr:\n    return polars.col('coords').arr.get(0).alias('x')\n
    "},{"location":"api/#atomlib.AtomCell.y","title":"y","text":"
    y() -> Expr\n
    Source code in atomlib/atoms.py
    def y(self) -> polars.Expr:\n    return polars.col('coords').arr.get(1).alias('y')\n
    "},{"location":"api/#atomlib.AtomCell.z","title":"z","text":"
    z() -> Expr\n
    Source code in atomlib/atoms.py
    def z(self) -> polars.Expr:\n    return polars.col('coords').arr.get(2).alias('z')\n
    "},{"location":"api/#atomlib.AtomCell.velocities","title":"velocities","text":"
    velocities(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Optional[NDArray[float64]]\n

    Return a (N, 3) ndarray of atom velocities (dtype numpy.float64) in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef velocities(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> t.Optional[NDArray[numpy.float64]]:\n    \"\"\"\n    Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64])\n    in the given coordinate frame.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.types","title":"types","text":"
    types() -> Optional[Series]\n

    Returns a Series of atom types (dtype polars.Int32).

    Source code in atomlib/atoms.py
    def types(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('type')\n
    "},{"location":"api/#atomlib.AtomCell.masses","title":"masses","text":"
    masses() -> Optional[Series]\n

    Returns a Series of atom masses (dtype polars.Float32).

    Source code in atomlib/atoms.py
    def masses(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('mass')\n
    "},{"location":"api/#atomlib.AtomCell.add_atom","title":"add_atom","text":"
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: Union[ArrayLike, float],\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None,\n    **kwargs: Any,\n) -> Self\n

    Return a copy of self with an extra atom.

    By default, all extra columns present in self must be specified as **kwargs.

    Try to avoid calling this in a loop (Use concat instead).

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef add_atom(self, elem: t.Union[int, str], /,  # type: ignore (spurious)\n             x: t.Union[ArrayLike, float],\n             y: t.Optional[float] = None,\n             z: t.Optional[float] = None, *,\n             frame: t.Optional[CoordinateFrame] = None,\n             **kwargs: t.Any) -> Self:\n    \"\"\"\n    Return a copy of `self` with an extra atom.\n\n    By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n    Try to avoid calling this in a loop (Use [`concat`][atomlib.atomcell.HasAtomCell.concat] instead).\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.pos","title":"pos","text":"
    pos(\n    x: Union[Sequence[Optional[float]], float, None] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n

    Select all atoms at a given position.

    Formally, returns all atoms within a cube of radius tol centered at (x,y,z), exclusive of the cube's surface.

    Additional parameters given as kwargs will be checked as additional parameters (with strict equality).

    Source code in atomlib/atoms.py
    def pos(self,\n        x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n        y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n        tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n    \"\"\"\n    Select all atoms at a given position.\n\n    Formally, returns all atoms within a cube of radius ``tol``\n    centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n    Additional parameters given as ``kwargs`` will be checked\n    as additional parameters (with strict equality).\n    \"\"\"\n\n    if isinstance(x, t.Sequence):\n        (x, y, z) = x\n\n    tol = abs(float(tol))\n    selection = polars.lit(True)\n    if x is not None:\n        selection &= self.x().is_between(x - tol, x + tol, closed='none')\n    if y is not None:\n        selection &= self.y().is_between(y - tol, y + tol, closed='none')\n    if z is not None:\n        selection &= self.z().is_between(z - tol, z + tol, closed='none')\n    for (col, val) in kwargs.items():\n        selection &= (polars.col(col) == val)\n\n    return selection\n
    "},{"location":"api/#atomlib.AtomCell.with_index","title":"with_index","text":"
    with_index(\n    index: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Returns self with a row index added in column 'i' (dtype polars.Int64). If index is not specified, defaults to an existing index or a new index.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_index(self, index: t.Optional[AtomValues] = None, *,\n               frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n    If `index` is not specified, defaults to an existing index or a new index.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.with_wobble","title":"with_wobble","text":"
    with_wobble(\n    wobble: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given displacements in column 'wobble' (dtype polars.Float64). If wobble is not specified, defaults to the already-existing wobbles or 0.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_wobble(self, wobble: t.Optional[AtomValues] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.with_occupancy","title":"with_occupancy","text":"
    with_occupancy(\n    frac_occupancy: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given fractional occupancies (dtype polars.Float64). If frac_occupancy is not specified, defaults to the already-existing occupancies or 1.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None, *,\n                   frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.apply_wobble","title":"apply_wobble","text":"
    apply_wobble(\n    rng: Union[Generator, int, None] = None,\n    frame: Optional[CoordinateFrame] = None,\n) -> Self\n

    Displace the atoms in self by the amount in the wobble column. wobble is interpretated as a mean-squared displacement, which is distributed equally over each axis.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None,\n                 frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Displace the atoms in `self` by the amount in the `wobble` column.\n    `wobble` is interpretated as a mean-squared displacement, which is distributed\n    equally over each axis.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.apply_occupancy","title":"apply_occupancy","text":"
    apply_occupancy(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    For each atom in self, use its frac_occupancy to randomly decide whether to remove it.

    Source code in atomlib/atoms.py
    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n    \"\"\"\n    if 'frac_occupancy' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    frac = self.select('frac_occupancy').to_series().to_numpy()\n    choice = rng.binomial(1, frac).astype(numpy.bool_)\n    return self.filter(polars.lit(choice))\n
    "},{"location":"api/#atomlib.AtomCell.with_type","title":"with_type","text":"
    with_type(\n    types: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atom types in column 'type'. If types is not specified, use the already existing types or auto-assign them.

    When auto-assigning, each symbol is given a unique value, case-sensitive. Values are assigned from lowest atomic number to highest. For instance: [\"Ag+\", \"Na\", \"H\", \"Ag\"] => [3, 11, 1, 2]

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_type(self, types: t.Optional[AtomValues] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom types in column 'type'.\n    If `types` is not specified, use the already existing types or auto-assign them.\n\n    When auto-assigning, each symbol is given a unique value, case-sensitive.\n    Values are assigned from lowest atomic number to highest.\n    For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.with_mass","title":"with_mass","text":"
    with_mass(\n    mass: Optional[ArrayLike] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atom masses in column 'mass'. If mass is not specified, use the already existing masses or auto-assign them.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_mass(self, mass: t.Optional[ArrayLike] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom masses in column `'mass'`.\n    If `mass` is not specified, use the already existing masses or auto-assign them.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.with_symbol","title":"with_symbol","text":"
    with_symbol(\n    symbols: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atomic symbols.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atomic symbols.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.with_coords","title":"with_coords","text":"
    with_coords(\n    pts: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self replaced with the given atomic positions.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic positions.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.with_velocity","title":"with_velocity","text":"
    with_velocity(\n    pts: Optional[ArrayLike] = None,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self replaced with the given atomic velocities. If pts is not specified, use the already existing velocities or zero.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                  selection: t.Optional[AtomSelection] = None, *,\n                  frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic velocities.\n    If `pts` is not specified, use the already existing velocities or zero.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.get_atomcell","title":"get_atomcell","text":"
    get_atomcell() -> AtomCell\n
    Source code in atomlib/atomcell.py
    def get_atomcell(self) -> AtomCell:\n    frame = self.get_frame()\n    return AtomCell(self.get_atoms(frame), self.get_cell(), frame=frame, keep_frame=True)\n
    "},{"location":"api/#atomlib.AtomCell.to_frame","title":"to_frame","text":"
    to_frame(frame: CoordinateFrame) -> Self\n

    Convert the stored Atoms to the given coordinate frame.

    Source code in atomlib/atomcell.py
    def to_frame(self, frame: CoordinateFrame) -> Self:\n    \"\"\"Convert the stored Atoms to the given coordinate frame.\"\"\"\n    return self.with_atoms(self.get_atoms(frame), frame)\n
    "},{"location":"api/#atomlib.AtomCell.crop_to_box","title":"crop_to_box","text":"
    crop_to_box(eps: float = 1e-05) -> Self\n
    Source code in atomlib/atomcell.py
    def crop_to_box(self, eps: float = 1e-5) -> Self:\n    atoms = self._transform_atoms_in_frame('cell_box', lambda atoms: atoms.crop_atoms(*([-eps, 1-eps]*3)))\n    return self.with_atoms(atoms)\n
    "},{"location":"api/#atomlib.AtomCell.wrap","title":"wrap","text":"
    wrap(eps: float = 1e-05) -> Self\n

    Wrap atoms around the cell boundaries.

    Source code in atomlib/atomcell.py
    def wrap(self, eps: float = 1e-5) -> Self:\n    \"\"\"Wrap atoms around the cell boundaries.\"\"\"\n    return self.with_atoms(self._transform_atoms_in_frame('cell_box', lambda a: a._wrap(eps)))\n
    "},{"location":"api/#atomlib.AtomCell.repeat_to","title":"repeat_to","text":"
    repeat_to(\n    size: VecLike, crop: Union[bool, Sequence[bool]] = False\n) -> Self\n

    Repeat the cell so it is at least size along the crystal's axes.

    If crop, then crop the cell to exactly size. This may break periodicity. crop may be a vector, in which case you can specify cropping only along some axes.

    Source code in atomlib/atomcell.py
    def repeat_to(self, size: VecLike, crop: t.Union[bool, t.Sequence[bool]] = False) -> Self:\n    \"\"\"\n    Repeat the cell so it is at least `size` along the crystal's axes.\n\n    If `crop`, then crop the cell to exactly `size`. This may break periodicity.\n    `crop` may be a vector, in which case you can specify cropping only along some axes.\n    \"\"\"\n    size = to_vec3(size)\n    cell_size = self.cell_size * self.n_cells\n    repeat = numpy.maximum(numpy.ceil(size / cell_size).astype(int), 1)\n    atom_cell = self.repeat(repeat)\n\n    crop_v = to_vec3(crop, dtype=numpy.bool_)\n    if numpy.any(crop_v):\n        crop_x, crop_y, crop_z = crop_v\n        return atom_cell.crop(\n            x_max = size[0] if crop_x else numpy.inf,\n            y_max = size[1] if crop_y else numpy.inf,\n            z_max = size[2] if crop_z else numpy.inf,\n            frame='cell'\n        )\n\n    return atom_cell\n
    "},{"location":"api/#atomlib.AtomCell.repeat_x","title":"repeat_x","text":"
    repeat_x(n: int) -> Self\n

    Tile the cell in the x axis.

    Source code in atomlib/atomcell.py
    def repeat_x(self, n: int) -> Self:\n    \"\"\"Tile the cell in the x axis.\"\"\"\n    return self.repeat((n, 1, 1))\n
    "},{"location":"api/#atomlib.AtomCell.repeat_y","title":"repeat_y","text":"
    repeat_y(n: int) -> Self\n

    Tile the cell in the y axis.

    Source code in atomlib/atomcell.py
    def repeat_y(self, n: int) -> Self:\n    \"\"\"Tile the cell in the y axis.\"\"\"\n    return self.repeat((1, n, 1))\n
    "},{"location":"api/#atomlib.AtomCell.repeat_z","title":"repeat_z","text":"
    repeat_z(n: int) -> Self\n

    Tile the cell in the z axis.

    Source code in atomlib/atomcell.py
    def repeat_z(self, n: int) -> Self:\n    \"\"\"Tile the cell in the z axis.\"\"\"\n    return self.repeat((1, 1, n))\n
    "},{"location":"api/#atomlib.AtomCell.repeat_to_x","title":"repeat_to_x","text":"
    repeat_to_x(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the x axis.

    Source code in atomlib/atomcell.py
    def repeat_to_x(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the x axis.\"\"\"\n    return self.repeat_to([size, 0., 0.], [crop, False, False])\n
    "},{"location":"api/#atomlib.AtomCell.repeat_to_y","title":"repeat_to_y","text":"
    repeat_to_y(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the y axis.

    Source code in atomlib/atomcell.py
    def repeat_to_y(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the y axis.\"\"\"\n    return self.repeat_to([0., size, 0.], [False, crop, False])\n
    "},{"location":"api/#atomlib.AtomCell.repeat_to_z","title":"repeat_to_z","text":"
    repeat_to_z(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the z axis.

    Source code in atomlib/atomcell.py
    def repeat_to_z(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the z axis.\"\"\"\n    return self.repeat_to([0., 0., size], [False, False, crop])\n
    "},{"location":"api/#atomlib.AtomCell.repeat_to_aspect","title":"repeat_to_aspect","text":"
    repeat_to_aspect(\n    plane: Literal[\"xy\", \"xz\", \"yz\"] = \"xy\",\n    *,\n    aspect: float = 1.0,\n    min_size: Optional[VecLike] = None,\n    max_size: Optional[VecLike] = None\n) -> Self\n

    Repeat to optimize the aspect ratio in plane, while staying above min_size and under max_size.

    Source code in atomlib/atomcell.py
    def repeat_to_aspect(self, plane: t.Literal['xy', 'xz', 'yz'] = 'xy', *,\n                     aspect: float = 1., min_size: t.Optional[VecLike] = None,\n                     max_size: t.Optional[VecLike] = None) -> Self:\n    \"\"\"\n    Repeat to optimize the aspect ratio in `plane`,\n    while staying above `min_size` and under `max_size`.\n    \"\"\"\n    if min_size is None:\n        min_n = numpy.array([1, 1, 1], numpy.int_)\n    else:\n        min_n = numpy.maximum(numpy.ceil(to_vec3(min_size) / self.box_size), 1).astype(numpy.int_)\n\n    if max_size is None:\n        max_n = 3 * min_n\n    else:\n        max_n = numpy.maximum(numpy.floor(to_vec3(max_size) / self.box_size), 1).astype(numpy.int_)\n\n    if plane == 'xy':\n        indices = [0, 1]\n    elif plane == 'xz':\n        indices = [0, 2]\n    elif plane == 'yz':\n        indices = [1, 2]\n    else:\n        raise ValueError(f\"Invalid plane '{plane}'. Exepcted 'xy', 'xz', 'or 'yz'.\")\n\n    na = numpy.arange(min_n[indices[0]], max_n[indices[0]])\n    nb = numpy.arange(min_n[indices[1]], max_n[indices[1]])\n    (na, nb) = numpy.meshgrid(na, nb)\n\n    aspects = na * self.box_size[indices[0]] / (nb * self.box_size[indices[1]])\n    # cost function: log(aspect)^2  (so cost(0.5) == cost(2))\n    min_i = numpy.argmin(numpy.log(aspects / aspect)**2)\n    repeat = numpy.array([1, 1, 1], numpy.int_)\n    repeat[indices] = na.flatten()[min_i], nb.flatten()[min_i]\n    return self.repeat(repeat)\n
    "},{"location":"api/#atomlib.AtomCell.periodic_duplicate","title":"periodic_duplicate","text":"
    periodic_duplicate(eps: float = 1e-05) -> Self\n

    Add duplicate copies of atoms near periodic boundaries.

    For instance, an atom at a corner will be duplicated into 8 copies. This is mostly only useful for visualization.

    Source code in atomlib/atomcell.py
    def periodic_duplicate(self, eps: float = 1e-5) -> Self:\n    \"\"\"\n    Add duplicate copies of atoms near periodic boundaries.\n\n    For instance, an atom at a corner will be duplicated into 8 copies.\n    This is mostly only useful for visualization.\n    \"\"\"\n    frame_save = self.get_frame()\n    self = self.to_frame('cell_box').wrap(eps=eps)\n\n    for i in range(3):\n        self = self.concat((self,\n            self.filter(polars.col('coords').arr.get(i).abs() <= eps, frame='cell_box')\n                .transform_atoms(AffineTransform3D.translate([1. if i == j else 0. for j in range(3)]), frame='cell_box')\n        ))\n\n    return self.to_frame(frame_save)\n
    "},{"location":"api/#atomlib.AtomCell.read","title":"read classmethod","text":"
    read(\n    path: FileOrPath, ty: Optional[FileType] = None\n) -> HasAtomsT\n

    Read a structure from a file.

    Supported types can be found in the io module. If no ty is specified, it is inferred from the file's extension.

    Source code in atomlib/mixins.py
    @classmethod\ndef read(cls: t.Type[HasAtomsT], path: FileOrPath, ty: t.Optional[FileType] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a file.\n\n    Supported types can be found in the [io][atomlib.io] module.\n    If no `ty` is specified, it is inferred from the file's extension.\n    \"\"\"\n    from .io import read\n    return _cast_atoms(read(path, ty), cls)  # type: ignore\n
    "},{"location":"api/#atomlib.AtomCell.read_cif","title":"read_cif classmethod","text":"
    read_cif(\n    f: Union[FileOrPath, CIF, CIFDataBlock],\n    block: Union[int, str, None] = None,\n) -> HasAtomsT\n

    Read a structure from a CIF file.

    If block is specified, read data from the given block of the CIF file (index or name).

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cif(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CIF, CIFDataBlock], block: t.Union[int, str, None] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a CIF file.\n\n    If `block` is specified, read data from the given block of the CIF file (index or name).\n    \"\"\"\n    from .io import read_cif\n    return _cast_atoms(read_cif(f, block), cls)\n
    "},{"location":"api/#atomlib.AtomCell.read_xyz","title":"read_xyz classmethod","text":"
    read_xyz(f: Union[FileOrPath, XYZ]) -> HasAtomsT\n

    Read a structure from an XYZ file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xyz(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XYZ]) -> HasAtomsT:\n    \"\"\"Read a structure from an XYZ file.\"\"\"\n    from .io import read_xyz\n    return _cast_atoms(read_xyz(f), cls)\n
    "},{"location":"api/#atomlib.AtomCell.read_xsf","title":"read_xsf classmethod","text":"
    read_xsf(f: Union[FileOrPath, XSF]) -> HasAtomsT\n

    Read a structure from an XSF file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xsf(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XSF]) -> HasAtomsT:\n    \"\"\"Read a structure from an XSF file.\"\"\"\n    from .io import read_xsf\n    return _cast_atoms(read_xsf(f), cls)\n
    "},{"location":"api/#atomlib.AtomCell.read_cfg","title":"read_cfg classmethod","text":"
    read_cfg(f: Union[FileOrPath, CFG]) -> HasAtomsT\n

    Read a structure from a CFG file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cfg(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CFG]) -> HasAtomsT:\n    \"\"\"Read a structure from a CFG file.\"\"\"\n    from .io import read_cfg\n    return _cast_atoms(read_cfg(f), cls)\n
    "},{"location":"api/#atomlib.AtomCell.read_lmp","title":"read_lmp classmethod","text":"
    read_lmp(\n    f: Union[FileOrPath, LMP],\n    type_map: Optional[Dict[int, Union[str, int]]] = None,\n) -> HasAtomsT\n

    Read a structure from a LAAMPS data file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_lmp(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, LMP], type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> HasAtomsT:\n    \"\"\"Read a structure from a LAAMPS data file.\"\"\"\n    from .io import read_lmp\n    return _cast_atoms(read_lmp(f, type_map=type_map), cls)\n
    "},{"location":"api/#atomlib.AtomCell.write_cif","title":"write_cif","text":"
    write_cif(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cif(self, f: FileOrPath):\n    from .io import write_cif\n    write_cif(self, f)\n
    "},{"location":"api/#atomlib.AtomCell.write_xyz","title":"write_xyz","text":"
    write_xyz(f: FileOrPath, fmt: XYZFormat = 'exyz')\n
    Source code in atomlib/mixins.py
    def write_xyz(self, f: FileOrPath, fmt: XYZFormat = 'exyz'):\n    from .io import write_xyz\n    write_xyz(self, f, fmt)\n
    "},{"location":"api/#atomlib.AtomCell.write_xsf","title":"write_xsf","text":"
    write_xsf(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_xsf(self, f: FileOrPath):\n    from .io import write_xsf\n    write_xsf(self, f)\n
    "},{"location":"api/#atomlib.AtomCell.write_cfg","title":"write_cfg","text":"
    write_cfg(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cfg(self, f: FileOrPath):\n    from .io import write_cfg\n    write_cfg(self, f)\n
    "},{"location":"api/#atomlib.AtomCell.write_lmp","title":"write_lmp","text":"
    write_lmp(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_lmp(self, f: FileOrPath):\n    from .io import write_lmp\n    write_lmp(self, f)\n
    "},{"location":"api/#atomlib.AtomCell.write","title":"write","text":"
    write(path: FileOrPath, ty: Optional[FileType] = None)\n

    Write this structure to a file.

    A file type may be specified using ty. If no ty is specified, it is inferred from the path's extension.

    Source code in atomlib/mixins.py
    def write(self, path: FileOrPath, ty: t.Optional[FileType] = None):\n    \"\"\"\n    Write this structure to a file.\n\n    A file type may be specified using `ty`.\n    If no `ty` is specified, it is inferred from the path's extension.\n    \"\"\"\n    from .io import write\n    write(self, path, ty)  # type: ignore\n
    "},{"location":"api/#atomlib.AtomCell.write_mslice","title":"write_mslice","text":"
    write_mslice(\n    f: BinaryFileOrPath,\n    template: Optional[MSliceFile] = None,\n    *,\n    slice_thickness: Optional[float] = None,\n    scan_points: Optional[ArrayLike] = None,\n    scan_extent: Optional[ArrayLike] = None,\n    noise_sigma: Optional[float] = None,\n    conv_angle: Optional[float] = None,\n    energy: Optional[float] = None,\n    defocus: Optional[float] = None,\n    tilt: Optional[Tuple[float, float]] = None,\n    tds: Optional[bool] = None,\n    n_cells: Optional[ArrayLike] = None\n)\n

    Write a structure to an mslice file.

    template may be a file, path, or ElementTree containing an existing mslice file. Its structure will be modified to make the final output. If not specified, a default template will be used.

    Additional options modify simulation properties. If an option is not specified, the template's properties are used.

    Source code in atomlib/mixins.py
    def write_mslice(self, f: BinaryFileOrPath, template: t.Optional[MSliceFile] = None, *,\n             slice_thickness: t.Optional[float] = None,  # angstrom\n             scan_points: t.Optional[ArrayLike] = None,\n             scan_extent: t.Optional[ArrayLike] = None,\n             noise_sigma: t.Optional[float] = None,  # angstrom\n             conv_angle: t.Optional[float] = None,  # mrad\n             energy: t.Optional[float] = None,  # keV\n             defocus: t.Optional[float] = None,  # angstrom\n             tilt: t.Optional[t.Tuple[float, float]] = None,  # (mrad, mrad)\n             tds: t.Optional[bool] = None,\n             n_cells: t.Optional[ArrayLike] = None):\n    \"\"\"\n    Write a structure to an mslice file.\n\n    `template` may be a file, path, or `ElementTree` containing an existing mslice file.\n    Its structure will be modified to make the final output. If not specified, a default\n    template will be used.\n\n    Additional options modify simulation properties. If an option is not specified, the\n    template's properties are used.\n    \"\"\"\n    from .io import write_mslice\n    return write_mslice(self, f, template, slice_thickness=slice_thickness,\n                        scan_points=scan_points, scan_extent=scan_extent,\n                        conv_angle=conv_angle, energy=energy, defocus=defocus,\n                        noise_sigma=noise_sigma, tilt=tilt, tds=tds, n_cells=n_cells)\n
    "},{"location":"api/#atomlib.AtomCell.write_qe","title":"write_qe","text":"
    write_qe(\n    f: FileOrPath,\n    pseudo: Optional[Mapping[str, str]] = None,\n)\n

    Write a structure to a Quantum Espresso pw.x file.

    PARAMETER DESCRIPTION f

    File or path to write to

    TYPE: FileOrPath

    pseudo

    Mapping from atom symbol

    TYPE: Optional[Mapping[str, str]] DEFAULT: None

    Source code in atomlib/mixins.py
    def write_qe(self, f: FileOrPath, pseudo: t.Optional[t.Mapping[str, str]] = None):\n    \"\"\"\n    Write a structure to a Quantum Espresso pw.x file.\n\n    Args:\n      f: File or path to write to\n      pseudo: Mapping from atom symbol\n    \"\"\"\n    from .io import write_qe\n    write_qe(self, f, pseudo)\n
    "},{"location":"api/#atomlib.AtomCell.get_cell","title":"get_cell","text":"
    get_cell() -> Cell\n
    Source code in atomlib/atomcell.py
    def get_cell(self) -> Cell:\n    return self.cell\n
    "},{"location":"api/#atomlib.AtomCell.with_cell","title":"with_cell","text":"
    with_cell(cell: Cell) -> Self\n
    Source code in atomlib/atomcell.py
    def with_cell(self, cell: Cell) -> Self:\n    return self.__class__(self.atoms, cell, frame=self.frame, keep_frame=True)\n
    "},{"location":"api/#atomlib.AtomCell.get_atoms","title":"get_atoms","text":"
    get_atoms(frame: Optional[CoordinateFrame] = None) -> Atoms\n

    Get atoms contained in self, in the given coordinate frame.

    Source code in atomlib/atomcell.py
    def get_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> Atoms:\n    \"\"\"Get atoms contained in ``self``, in the given coordinate frame.\"\"\"\n\n    if frame is None or frame == self.get_frame():\n        return self.atoms\n    return self.atoms.transform(self.get_transform(frame, self.get_frame()))\n
    "},{"location":"api/#atomlib.AtomCell.with_atoms","title":"with_atoms","text":"
    with_atoms(\n    atoms: HasAtoms, frame: Optional[CoordinateFrame] = None\n) -> Self\n
    Source code in atomlib/atomcell.py
    def with_atoms(self, atoms: HasAtoms, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    frame = frame if frame is not None else self.frame\n    return self.__class__(atoms.get_atoms(), cell=self.cell, frame=frame, keep_frame=True)\n
    "},{"location":"api/#atomlib.AtomCell.get_frame","title":"get_frame","text":"
    get_frame() -> CoordinateFrame\n

    Get the coordinate frame atoms are stored in.

    Source code in atomlib/atomcell.py
    def get_frame(self) -> CoordinateFrame:\n    \"\"\"Get the coordinate frame atoms are stored in.\"\"\"\n    return self.frame\n
    "},{"location":"api/#atomlib.AtomCell.from_ortho","title":"from_ortho classmethod","text":"
    from_ortho(\n    atoms: IntoAtoms,\n    ortho: LinearTransform3D,\n    *,\n    n_cells: Optional[VecLike] = None,\n    frame: CoordinateFrame = \"local\",\n    keep_frame: bool = False\n)\n

    Make an atom cell given a list of atoms and an orthogonalization matrix. Atoms are assumed to be in the coordinate system frame.

    Source code in atomlib/atomcell.py
    @classmethod\ndef from_ortho(cls, atoms: IntoAtoms, ortho: LinearTransform3D, *,\n               n_cells: t.Optional[VecLike] = None,\n               frame: CoordinateFrame = 'local',\n               keep_frame: bool = False):\n    \"\"\"\n    Make an atom cell given a list of atoms and an orthogonalization matrix.\n    Atoms are assumed to be in the coordinate system `frame`.\n    \"\"\"\n    cell = Cell.from_ortho(ortho, n_cells)\n    return cls(atoms, cell, frame=frame, keep_frame=keep_frame)\n
    "},{"location":"api/#atomlib.AtomCell.from_unit_cell","title":"from_unit_cell classmethod","text":"
    from_unit_cell(\n    atoms: IntoAtoms,\n    cell_size: VecLike,\n    cell_angle: Optional[VecLike] = None,\n    *,\n    n_cells: Optional[VecLike] = None,\n    frame: CoordinateFrame = \"local\",\n    keep_frame: bool = False\n)\n

    Make a cell given a list of atoms and unit cell parameters. Atoms are assumed to be in the coordinate system frame.

    Source code in atomlib/atomcell.py
    @classmethod\ndef from_unit_cell(cls, atoms: IntoAtoms, cell_size: VecLike,\n                   cell_angle: t.Optional[VecLike] = None, *,\n                   n_cells: t.Optional[VecLike] = None,\n                   frame: CoordinateFrame = 'local',\n                   keep_frame: bool = False):\n    \"\"\"\n    Make a cell given a list of atoms and unit cell parameters.\n    Atoms are assumed to be in the coordinate system `frame`.\n    \"\"\"\n    cell = Cell.from_unit_cell(cell_size, cell_angle, n_cells=n_cells)\n    return cls(atoms, cell, frame=frame, keep_frame=keep_frame)\n
    "},{"location":"api/#atomlib.AtomCell.orthogonalize","title":"orthogonalize","text":"
    orthogonalize() -> OrthoCell\n
    Source code in atomlib/atomcell.py
    def orthogonalize(self) -> OrthoCell:\n    if self.is_orthogonal():\n        return OrthoCell(self.atoms, self.cell, frame=self.frame)\n    raise NotImplementedError()\n
    "},{"location":"api/#atomlib.AtomCell.clone","title":"clone","text":"
    clone() -> AtomCellT\n

    Make a deep copy of self.

    Source code in atomlib/atomcell.py
    def clone(self: AtomCellT) -> AtomCellT:\n    \"\"\"Make a deep copy of `self`.\"\"\"\n    return self.__class__(**{field.name: copy.deepcopy(getattr(self, field.name)) for field in fields(self)})\n
    "},{"location":"api/#atomlib.AtomCell.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n

    Assert this structure is equal to other

    Source code in atomlib/atomcell.py
    def assert_equal(self, other: t.Any):\n    \"\"\"Assert this structure is equal to `other`\"\"\"\n    assert isinstance(other, AtomCell)\n    self.cell.assert_equal(other.cell)\n    self.get_atoms('local').assert_equal(other.get_atoms('local'))\n
    "},{"location":"api/#atomlib.HasAtomCell","title":"HasAtomCell","text":"

    Bases: HasAtoms, HasCell, ABC

    Source code in atomlib/atomcell.py
    class HasAtomCell(HasAtoms, HasCell, abc.ABC):\n    @abc.abstractmethod\n    def get_frame(self) -> CoordinateFrame:\n        \"\"\"Get the coordinate frame atoms are stored in.\"\"\"\n        ...\n\n    @abc.abstractmethod\n    def with_atoms(self, atoms: HasAtoms, frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Replace the atoms in `self`. If no coordinate frame is specified, keep the coordinate frame unchanged.\n        \"\"\"\n        ...\n\n    def with_cell(self, cell: Cell) -> Self:\n        \"\"\"\n        Replace the cell in `self`, without touching the atomic coordinates.\n        \"\"\"\n        return self.to_frame('local').with_cell(cell)\n\n    def get_atomcell(self) -> AtomCell:\n        frame = self.get_frame()\n        return AtomCell(self.get_atoms(frame), self.get_cell(), frame=frame, keep_frame=True)\n\n    @abc.abstractmethod\n    def get_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> Atoms:\n        \"\"\"Get atoms contained in `self`, in the given coordinate frame.\"\"\"\n        ...\n\n    def bbox_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> BBox3D:\n        \"\"\"Return the bounding box of all the atoms in `self`, in the given coordinate frame.\"\"\"\n        return self.get_atoms(frame).bbox()\n\n    def bbox(self, frame: CoordinateFrame = 'local') -> BBox3D:\n        \"\"\"\n        Return the combined bounding box of the cell and atoms in the given coordinate system.\n        To get the cell or atoms bounding box only, use [`bbox_cell`][atomlib.atomcell.HasAtomCell.bbox_cell] or [`bbox_atoms`][atomlib.atomcell.HasAtomCell.bbox_atoms].\n        \"\"\"\n        return self.bbox_atoms(frame) | self.bbox_cell(frame)\n\n    # transformation\n\n    def _transform_atoms_in_frame(self, frame: t.Optional[CoordinateFrame], f: t.Callable[[Atoms], Atoms]) -> Atoms:\n        # ugly code\n        if frame is None or frame == self.get_frame():\n            return f(self.get_atoms())\n        return f(self.get_atoms(frame)).transform(self.get_transform(self.get_frame(), frame))\n\n    def to_frame(self, frame: CoordinateFrame) -> Self:\n        \"\"\"Convert the stored Atoms to the given coordinate frame.\"\"\"\n        return self.with_atoms(self.get_atoms(frame), frame)\n\n    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                        frame: CoordinateFrame = 'local', transform_velocities: bool = False) -> Self:\n        \"\"\"\n        Transform the atoms in `self` by `transform`.\n        If `selection` is given, only transform the atoms in `selection`.\n        \"\"\"\n        transform = self.change_transform(Transform3D.make(transform), self.get_frame(), frame)\n        return self.with_atoms(self.get_atoms(self.get_frame()).transform(transform, selection, transform_velocities=transform_velocities))\n\n    def transform_cell(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n        \"\"\"\n        Apply the given transform to the unit cell, without changing atom positions.\n        The transform is applied in coordinate frame 'frame'.\n        \"\"\"\n        return self.with_cell(self.get_cell().transform_cell(transform, frame=frame))\n\n    def transform(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n        if isinstance(transform, Transform3D) and not isinstance(transform, AffineTransform3D):\n            raise ValueError(\"Non-affine transforms cannot change the box dimensions. Use 'transform_atoms' instead.\")\n        # TODO: cleanup once tests pass\n        # coordinate change the transform into atomic coordinates\n        new_cell = self.get_cell().transform_cell(transform, frame)\n        transform = self.get_cell().change_transform(transform, self.get_frame(), frame)\n        return self.with_atoms(self.get_atoms().transform(transform), self.get_frame()).with_cell(new_cell)\n\n    # crop methods\n\n    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n             y_min: float = -numpy.inf, y_max: float = numpy.inf,\n             z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n             frame: CoordinateFrame = 'local') -> Self:\n        \"\"\"\n        Crop atoms and cell to the given extents. For a non-orthogonal\n        cell, this must be specified in cell coordinates. This\n        function implicity `explode`s the cell as well.\n\n        To crop atoms only, use `crop_atoms` instead.\n        \"\"\"\n\n        cell = self.get_cell().crop(x_min, x_max, y_min, y_max, z_min, z_max, frame=frame)\n        atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n        return self.with_cell(cell).with_atoms(atoms)\n\n    def crop_atoms(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n                   y_min: float = -numpy.inf, y_max: float = numpy.inf,\n                   z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n                   frame: CoordinateFrame = 'local') -> Self:\n        atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n        return self.with_atoms(atoms)\n\n    def crop_to_box(self, eps: float = 1e-5) -> Self:\n        atoms = self._transform_atoms_in_frame('cell_box', lambda atoms: atoms.crop_atoms(*([-eps, 1-eps]*3)))\n        return self.with_atoms(atoms)\n\n    def wrap(self, eps: float = 1e-5) -> Self:\n        \"\"\"Wrap atoms around the cell boundaries.\"\"\"\n        return self.with_atoms(self._transform_atoms_in_frame('cell_box', lambda a: a._wrap(eps)))\n\n    def _repeat_to_contain(self, pts: numpy.ndarray, pad: int = 0, frame: CoordinateFrame = 'cell_frac') -> Self:\n        #print(f\"pts: {pts} in frame {frame}\")\n        pts = self.get_transform('cell_frac', frame) @ pts\n\n        bbox = BBox3D.unit() | BBox3D.from_pts(pts)\n        min_bounds = numpy.floor(bbox.min).astype(int) - pad\n        max_bounds = numpy.ceil(bbox.max).astype(int) + pad\n        #print(f\"tiling to {min_bounds}, {max_bounds}\")\n        repeat = max_bounds - min_bounds\n        cells = numpy.stack(numpy.meshgrid(*map(numpy.arange, repeat))).reshape(3, -1).T.astype(float)\n\n        atoms = self.get_atoms('cell_frac')\n        atoms = Atoms.concat([\n            atoms.transform(AffineTransform3D.translate(cell))\n            for cell in cells\n        ])\n        #print(f\"atoms:\\n{atoms}\")\n        cell = self.get_cell().repeat(repeat) \\\n            .transform_cell(AffineTransform3D.translate(min_bounds), 'cell_frac')\n        return self.with_cell(cell).with_atoms(atoms, 'cell_frac')\n\n    def repeat(self, n: t.Union[int, VecLike]) -> Self:\n        \"\"\"Tile the cell\"\"\"\n        ns = numpy.broadcast_to(n, 3)\n        if not numpy.issubdtype(ns.dtype, numpy.integer):\n            raise ValueError(\"repeat() argument must be an integer or integer array.\")\n\n        cells = numpy.stack(numpy.meshgrid(*map(numpy.arange, ns))) \\\n            .reshape(3, -1).T.astype(float)\n        cells = cells * self.box_size\n\n        atoms = self.get_atoms('cell')\n        atoms = Atoms.concat([\n            atoms.transform(AffineTransform3D.translate(cell))\n            for cell in cells\n        ]) #.transform(self.cell.get_transform('local', 'cell_frac'))\n        return self.with_atoms(atoms, 'cell').with_cell(self.get_cell().repeat(ns))\n\n    def repeat_to(self, size: VecLike, crop: t.Union[bool, t.Sequence[bool]] = False) -> Self:\n        \"\"\"\n        Repeat the cell so it is at least `size` along the crystal's axes.\n\n        If `crop`, then crop the cell to exactly `size`. This may break periodicity.\n        `crop` may be a vector, in which case you can specify cropping only along some axes.\n        \"\"\"\n        size = to_vec3(size)\n        cell_size = self.cell_size * self.n_cells\n        repeat = numpy.maximum(numpy.ceil(size / cell_size).astype(int), 1)\n        atom_cell = self.repeat(repeat)\n\n        crop_v = to_vec3(crop, dtype=numpy.bool_)\n        if numpy.any(crop_v):\n            crop_x, crop_y, crop_z = crop_v\n            return atom_cell.crop(\n                x_max = size[0] if crop_x else numpy.inf,\n                y_max = size[1] if crop_y else numpy.inf,\n                z_max = size[2] if crop_z else numpy.inf,\n                frame='cell'\n            )\n\n        return atom_cell\n\n    def repeat_x(self, n: int) -> Self:\n        \"\"\"Tile the cell in the x axis.\"\"\"\n        return self.repeat((n, 1, 1))\n\n    def repeat_y(self, n: int) -> Self:\n        \"\"\"Tile the cell in the y axis.\"\"\"\n        return self.repeat((1, n, 1))\n\n    def repeat_z(self, n: int) -> Self:\n        \"\"\"Tile the cell in the z axis.\"\"\"\n        return self.repeat((1, 1, n))\n\n    def repeat_to_x(self, size: float, crop: bool = False) -> Self:\n        \"\"\"Repeat the cell so it is at least size `size` along the x axis.\"\"\"\n        return self.repeat_to([size, 0., 0.], [crop, False, False])\n\n    def repeat_to_y(self, size: float, crop: bool = False) -> Self:\n        \"\"\"Repeat the cell so it is at least size `size` along the y axis.\"\"\"\n        return self.repeat_to([0., size, 0.], [False, crop, False])\n\n    def repeat_to_z(self, size: float, crop: bool = False) -> Self:\n        \"\"\"Repeat the cell so it is at least size `size` along the z axis.\"\"\"\n        return self.repeat_to([0., 0., size], [False, False, crop])\n\n    def repeat_to_aspect(self, plane: t.Literal['xy', 'xz', 'yz'] = 'xy', *,\n                         aspect: float = 1., min_size: t.Optional[VecLike] = None,\n                         max_size: t.Optional[VecLike] = None) -> Self:\n        \"\"\"\n        Repeat to optimize the aspect ratio in `plane`,\n        while staying above `min_size` and under `max_size`.\n        \"\"\"\n        if min_size is None:\n            min_n = numpy.array([1, 1, 1], numpy.int_)\n        else:\n            min_n = numpy.maximum(numpy.ceil(to_vec3(min_size) / self.box_size), 1).astype(numpy.int_)\n\n        if max_size is None:\n            max_n = 3 * min_n\n        else:\n            max_n = numpy.maximum(numpy.floor(to_vec3(max_size) / self.box_size), 1).astype(numpy.int_)\n\n        if plane == 'xy':\n            indices = [0, 1]\n        elif plane == 'xz':\n            indices = [0, 2]\n        elif plane == 'yz':\n            indices = [1, 2]\n        else:\n            raise ValueError(f\"Invalid plane '{plane}'. Exepcted 'xy', 'xz', 'or 'yz'.\")\n\n        na = numpy.arange(min_n[indices[0]], max_n[indices[0]])\n        nb = numpy.arange(min_n[indices[1]], max_n[indices[1]])\n        (na, nb) = numpy.meshgrid(na, nb)\n\n        aspects = na * self.box_size[indices[0]] / (nb * self.box_size[indices[1]])\n        # cost function: log(aspect)^2  (so cost(0.5) == cost(2))\n        min_i = numpy.argmin(numpy.log(aspects / aspect)**2)\n        repeat = numpy.array([1, 1, 1], numpy.int_)\n        repeat[indices] = na.flatten()[min_i], nb.flatten()[min_i]\n        return self.repeat(repeat)\n\n    def explode(self) -> Self:\n        \"\"\"Materialize repeated cells as one supercell.\"\"\"\n        frame = self.get_frame()\n\n        return self.with_atoms(self.get_atoms('local'), 'local') \\\n            .with_cell(self.get_cell().explode()) \\\n            .to_frame(frame)\n\n    def periodic_duplicate(self, eps: float = 1e-5) -> Self:\n        \"\"\"\n        Add duplicate copies of atoms near periodic boundaries.\n\n        For instance, an atom at a corner will be duplicated into 8 copies.\n        This is mostly only useful for visualization.\n        \"\"\"\n        frame_save = self.get_frame()\n        self = self.to_frame('cell_box').wrap(eps=eps)\n\n        for i in range(3):\n            self = self.concat((self,\n                self.filter(polars.col('coords').arr.get(i).abs() <= eps, frame='cell_box')\n                    .transform_atoms(AffineTransform3D.translate([1. if i == j else 0. for j in range(3)]), frame='cell_box')\n            ))\n\n        return self.to_frame(frame_save)\n\n    # add frame to some HasAtoms methods\n\n    @_fwd_atoms_get\n    def describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n                 interpolation: RollingInterpolationMethod = 'nearest',\n                 frame: t.Optional[CoordinateFrame] = None) -> polars.DataFrame:\n        \"\"\"\n        Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n        Args:\n          percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                       50% (median), and 75% (third quartile).\n\n        Returns:\n          A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_columns(self,\n                     *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                     frame: t.Optional[CoordinateFrame] = None,\n                     **named_exprs: IntoExpr) -> Self:\n        \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n        ...\n\n    with_column = with_columns\n\n    @_fwd_atoms_get\n    def get_column(self, name: str, *, frame: t.Optional[CoordinateFrame] = None) -> polars.Series:\n        \"\"\"\n        Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        ...\n\n    @_fwd_atoms_get\n    def get_columns(self, *, frame: t.Optional[CoordinateFrame] = None) -> t.List[polars.Series]:\n        \"\"\"\n        Return all columns from `self` as a list of [`Series`][polars.Series].\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        ...\n\n    @_fwd_atoms_get\n    def group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 maintain_order: bool = False, frame: t.Optional[CoordinateFrame] = None,\n                 **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n        \"\"\"\n        Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n        \"\"\"\n        ...\n\n    def pipe(self: HasAtomCellT, function: t.Callable[Concatenate[HasAtomCellT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n        \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n        return function(self, *args, **kwargs)\n\n    @_fwd_atoms_transform\n    def filter(\n        self,\n        *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n        frame: t.Optional[CoordinateFrame] = None,\n        **constraints: t.Any,\n    ) -> Self:\n        \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def sort(\n        self,\n        by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        *more_by: IntoExpr,\n        descending: t.Union[bool, t.Sequence[bool]] = False,\n        nulls_last: bool = False,\n    ) -> Self:\n        \"\"\"\n        Sort the atoms in `self` by the given columns/expressions.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def slice(self, offset: int, length: t.Optional[int] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"Return a slice of the rows in `self`.\"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def head(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"Return the first `n` rows of `self`.\"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def tail(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"Return the last `n` rows of `self`.\"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def fill_null(\n        self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n        limit: t.Optional[int] = None, matches_supertype: bool = True,\n    ) -> Self:\n        ...\n\n    @_fwd_atoms_transform\n    def fill_nan(self, value: t.Union[polars.Expr, int, float, None], *,\n                 frame: t.Optional[CoordinateFrame] = None) -> Self:\n        ...\n\n    # TODO: partition_by\n\n    @_fwd_atoms_get\n    def select(\n        self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        frame: t.Optional[CoordinateFrame] = None,\n        **named_exprs: IntoExpr\n    ) -> polars.DataFrame:\n        \"\"\"\n        Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n        Expressions may either be columns or expressions of columns.\n\n        [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def select_props(\n        self,\n        *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        frame: t.Optional[CoordinateFrame] = None,\n        **named_exprs: IntoExpr\n    ) -> Self:\n        \"\"\"\n        Select `exprs` from `self`, while keeping required columns.\n        Doesn't affect the cell.\n\n        Returns:\n          A [`HasAtomCell`][atomlib.atomcell.HasAtomCell] filtered to contain\n          the specified properties (as well as required columns).\n        \"\"\"\n        ...\n\n    @_fwd_atoms_get\n    def try_select(\n        self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        frame: t.Optional[CoordinateFrame] = None,\n        **named_exprs: IntoExpr\n    ) -> t.Optional[polars.DataFrame]:\n        \"\"\"\n        Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n        Expressions may either be columns or expressions of columns. Returns `None` if any\n        columns are missing.\n\n        [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def round_near_zero(self, tol: float = 1e-14, *,\n                        frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Round atom position values near zero to zero.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_get\n    def coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> NDArray[numpy.float64]:\n        \"\"\"\n        Return a `(N, 3)` ndarray of atom positions (dtype [`numpy.float64`][numpy.float64])\n        in the given coordinate frame.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_get\n    def velocities(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> t.Optional[NDArray[numpy.float64]]:\n        \"\"\"\n        Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64])\n        in the given coordinate frame.\n        \"\"\"\n        ...\n\n    @t.overload\n    def add_atom(self, elem: t.Union[int, str], x: ArrayLike, /, *,\n                 y: None = None, z: None = None, frame: t.Optional[CoordinateFrame] = None,\n                 **kwargs: t.Any) -> Self:\n        ...\n\n    @t.overload\n    def add_atom(self, elem: t.Union[int, str], /,\n                 x: float, y: float, z: float, *,\n                 frame: t.Optional[CoordinateFrame] = None,\n                 **kwargs: t.Any) -> Self:\n        ...\n\n    @_fwd_atoms_transform\n    def add_atom(self, elem: t.Union[int, str], /,  # type: ignore (spurious)\n                 x: t.Union[ArrayLike, float],\n                 y: t.Optional[float] = None,\n                 z: t.Optional[float] = None, *,\n                 frame: t.Optional[CoordinateFrame] = None,\n                 **kwargs: t.Any) -> Self:\n        \"\"\"\n        Return a copy of `self` with an extra atom.\n\n        By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n        Try to avoid calling this in a loop (Use [`concat`][atomlib.atomcell.HasAtomCell.concat] instead).\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_index(self, index: t.Optional[AtomValues] = None, *,\n                   frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n        If `index` is not specified, defaults to an existing index or a new index.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_wobble(self, wobble: t.Optional[AtomValues] = None, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n        If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None, *,\n                       frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n        If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None,\n                     frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Displace the atoms in `self` by the amount in the `wobble` column.\n        `wobble` is interpretated as a mean-squared displacement, which is distributed\n        equally over each axis.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_type(self, types: t.Optional[AtomValues] = None, *,\n                  frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atom types in column 'type'.\n        If `types` is not specified, use the already existing types or auto-assign them.\n\n        When auto-assigning, each symbol is given a unique value, case-sensitive.\n        Values are assigned from lowest atomic number to highest.\n        For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_mass(self, mass: t.Optional[ArrayLike] = None, *,\n                  frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atom masses in column `'mass'`.\n        If `mass` is not specified, use the already existing masses or auto-assign them.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atomic symbols.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` replaced with the given atomic positions.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                      selection: t.Optional[AtomSelection] = None, *,\n                      frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` replaced with the given atomic velocities.\n        If `pts` is not specified, use the already existing velocities or zero.\n        \"\"\"\n        ...\n
    "},{"location":"api/#atomlib.HasAtomCell.affine","title":"affine property","text":"
    affine: AffineTransform3D\n

    Affine transformation. Holds transformation from 'ortho' to 'local' coordinates, including rotation away from the standard crystal orientation.

    "},{"location":"api/#atomlib.HasAtomCell.ortho","title":"ortho property","text":"
    ortho: LinearTransform3D\n

    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.

    "},{"location":"api/#atomlib.HasAtomCell.metric","title":"metric property","text":"
    metric: LinearTransform3D\n

    Cell metric tensor

    Returns the dot product between every combination of basis vectors. :math:\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j

    "},{"location":"api/#atomlib.HasAtomCell.cell_size","title":"cell_size property","text":"
    cell_size: NDArray[float64]\n

    Unit cell size.

    "},{"location":"api/#atomlib.HasAtomCell.cell_angle","title":"cell_angle property","text":"
    cell_angle: NDArray[float64]\n

    Unit cell angles, in radians.

    "},{"location":"api/#atomlib.HasAtomCell.n_cells","title":"n_cells property","text":"
    n_cells: NDArray[int_]\n

    Number of unit cells.

    "},{"location":"api/#atomlib.HasAtomCell.pbc","title":"pbc property","text":"
    pbc: NDArray[bool_]\n

    Flags indicating the presence of periodic boundary conditions along each axis.

    "},{"location":"api/#atomlib.HasAtomCell.ortho_size","title":"ortho_size property","text":"
    ortho_size: NDArray[float64]\n

    Return size of orthogonal unit cell.

    Equivalent to the diagonal of the orthogonalization matrix.

    "},{"location":"api/#atomlib.HasAtomCell.box_size","title":"box_size property","text":"
    box_size: NDArray[float64]\n

    Return size of the cell box.

    Equivalent to self.n_cells * self.cell_size.

    "},{"location":"api/#atomlib.HasAtomCell.columns","title":"columns property","text":"
    columns: List[str]\n

    Return the column names in self.

    RETURNS DESCRIPTION List[str]

    A sequence of column names

    "},{"location":"api/#atomlib.HasAtomCell.dtypes","title":"dtypes property","text":"
    dtypes: List[DataType]\n

    Return the datatypes in self.

    RETURNS DESCRIPTION List[DataType]

    A sequence of column DataTypes

    "},{"location":"api/#atomlib.HasAtomCell.schema","title":"schema property","text":"
    schema: Schema\n

    Return the schema of self.

    RETURNS DESCRIPTION Schema

    A dictionary of column names and DataTypes

    "},{"location":"api/#atomlib.HasAtomCell.unique","title":"unique class-attribute instance-attribute","text":"
    unique = deduplicate\n
    "},{"location":"api/#atomlib.HasAtomCell.with_column","title":"with_column class-attribute instance-attribute","text":"
    with_column = with_columns\n
    "},{"location":"api/#atomlib.HasAtomCell.get_cell","title":"get_cell abstractmethod","text":"
    get_cell() -> Cell\n

    Get the cell contained in self. This should be a low cost method.

    Source code in atomlib/cell.py
    @abc.abstractmethod\ndef get_cell(self) -> Cell:\n    \"\"\"Get the cell contained in ``self``. This should be a low cost method.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.get_transform","title":"get_transform","text":"
    get_transform(\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n

    In the two-argument form, get the transform to frame_to from frame_from. In the one-argument form, get the transform from local coordinates to 'frame'.

    Source code in atomlib/cell.py
    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n    \"\"\"\n    In the two-argument form, get the transform to `frame_to` from `frame_from`.\n    In the one-argument form, get the transform from local coordinates to 'frame'.\n    \"\"\"\n    transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n    transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n    if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n        return AffineTransform3D()\n    return transform_to.inverse() @ transform_from\n
    "},{"location":"api/#atomlib.HasAtomCell.corners","title":"corners","text":"
    corners(frame: CoordinateFrame = 'local') -> ndarray\n
    Source code in atomlib/cell.py
    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n    corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n    return self.get_transform(frame, 'cell_box') @ corners\n
    "},{"location":"api/#atomlib.HasAtomCell.bbox_cell","title":"bbox_cell","text":"
    bbox_cell(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the bounding box of the cell box in the given coordinate system.

    Source code in atomlib/cell.py
    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n    return BBox3D.from_pts(self.corners(frame))\n
    "},{"location":"api/#atomlib.HasAtomCell.is_orthogonal","title":"is_orthogonal","text":"
    is_orthogonal(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal (axes are at right angles.)

    Source code in atomlib/cell.py
    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n    return self.ortho.is_diagonal(tol=tol)\n
    "},{"location":"api/#atomlib.HasAtomCell.is_orthogonal_in_local","title":"is_orthogonal_in_local","text":"
    is_orthogonal_in_local(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal and aligned with the local coordinate system.

    Source code in atomlib/cell.py
    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n    transform = (self.affine @ self.ortho).to_linear()\n    if not transform.is_scaled_orthogonal(tol):\n        return False\n    normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n    # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n    return all(\n        any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n        for row in normed\n    )\n
    "},{"location":"api/#atomlib.HasAtomCell.to_ortho","title":"to_ortho","text":"
    to_ortho() -> AffineTransform3D\n
    Source code in atomlib/cell.py
    def to_ortho(self) -> AffineTransform3D:\n    return self.get_transform('local', 'cell_box')\n
    "},{"location":"api/#atomlib.HasAtomCell.strain_orthogonal","title":"strain_orthogonal","text":"
    strain_orthogonal() -> HasCellT\n

    Orthogonalize using strain.

    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane. For small displacements, no hydrostatic strain is applied (volume is conserved).

    Source code in atomlib/cell.py
    def strain_orthogonal(self: HasCellT) -> HasCellT:\n    \"\"\"\n    Orthogonalize using strain.\n\n    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n    For small displacements, no hydrostatic strain is applied (volume is conserved).\n    \"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=LinearTransform3D(),\n        cell_size=self.cell_size,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.HasAtomCell.explode_z","title":"explode_z","text":"
    explode_z() -> HasCellT\n

    Materialize repeated cells as one supercell in z.

    Source code in atomlib/cell.py
    def explode_z(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n        n_cells=[*self.n_cells[:2], 1],\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.HasAtomCell.change_transform","title":"change_transform","text":"
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n

    Coordinate-change a transformation from frame_from into frame_to.

    Source code in atomlib/cell.py
    def change_transform(self, transform: Transform3D,\n                     frame_to: t.Optional[CoordinateFrame] = None,\n                     frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n    \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n    if frame_to == frame_from and frame_to is not None:\n        return transform\n    coord_change = self.get_transform(frame_to, frame_from)\n    return coord_change @ transform @ coord_change.inverse()\n
    "},{"location":"api/#atomlib.HasAtomCell.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n
    Source code in atomlib/atoms.py
    def assert_equal(self, other: t.Any):\n    assert isinstance(other, HasAtoms)\n    assert dict(self.schema) == dict(other.schema)\n    for col in self.schema.keys():\n        polars.testing.assert_series_equal(self[col], other[col], check_names=False, rtol=1e-3, atol=1e-8)\n
    "},{"location":"api/#atomlib.HasAtomCell.insert_column","title":"insert_column","text":"
    insert_column(index: int, column: Series) -> DataFrame\n
    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n    return self._get_frame().insert_column(index, column)\n
    "},{"location":"api/#atomlib.HasAtomCell.get_column_index","title":"get_column_index","text":"
    get_column_index(name: str) -> int\n

    Get the index of a column by name, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_column_index)\ndef get_column_index(self, name: str) -> int:\n    \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.clone","title":"clone","text":"
    clone() -> DataFrame\n

    Return a copy of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef clone(self) -> polars.DataFrame:\n    \"\"\"Return a copy of `self`.\"\"\"\n    return self._get_frame().clone()\n
    "},{"location":"api/#atomlib.HasAtomCell.drop","title":"drop","text":"
    drop(\n    *columns: Union[str, Iterable[str]], strict: bool = True\n) -> DataFrame\n

    Return self with the specified columns removed.

    Source code in atomlib/atoms.py
    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n    \"\"\"Return `self` with the specified columns removed.\"\"\"\n    return self._get_frame().drop(*columns, strict=strict)\n
    "},{"location":"api/#atomlib.HasAtomCell.drop_nulls","title":"drop_nulls","text":"
    drop_nulls(\n    subset: Union[str, Collection[str], None] = None\n) -> DataFrame\n

    Drop rows that contain nulls in any of columns subset.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n    \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n    return self._get_frame().drop_nulls(subset)\n
    "},{"location":"api/#atomlib.HasAtomCell.concat","title":"concat classmethod","text":"
    concat(\n    atoms: Union[\n        HasAtomsT,\n        IntoAtoms,\n        Iterable[Union[HasAtomsT, IntoAtoms]],\n    ],\n    *,\n    rechunk: bool = True,\n    how: ConcatMethod = \"vertical\"\n) -> HasAtomsT\n

    Concatenate multiple Atoms together, handling metadata appropriately.

    Source code in atomlib/atoms.py
    @classmethod\ndef concat(cls: t.Type[HasAtomsT],\n           atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n           rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n    \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n    # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n    # same type as ``cls``.\n    if _is_abstract(cls):\n        raise TypeError(\"concat() must be called on a concrete class.\")\n\n    if isinstance(atoms, HasAtoms):\n        atoms = (atoms,)\n    dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n    representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n    if len(dfs) == 0:\n        return representative.with_atoms(Atoms.empty(), 'local')\n\n    if how in ('vertical', 'vertical_relaxed'):\n        # get order from first member\n        cols = dfs[0].columns\n        dfs = [df.select(cols) for df in dfs]\n    elif how == 'inner':\n        cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n        schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n        if len(schema) == 0:\n            raise ValueError(\"Atoms have no columns in common\")\n\n        dfs = [_select_schema(df, schema) for df in dfs]\n        how = 'vertical'\n\n    return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n
    "},{"location":"api/#atomlib.HasAtomCell.partition_by","title":"partition_by","text":"
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: bool = False\n) -> Union[List[Self], Dict[Any, Self]]\n

    Group by the given columns and partition into separate dataframes.

    Return the partitions as a dictionary by specifying as_dict=True.

    Source code in atomlib/atoms.py
    def partition_by(\n    self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n    maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n    \"\"\"\n    Group by the given columns and partition into separate dataframes.\n\n    Return the partitions as a dictionary by specifying `as_dict=True`.\n    \"\"\"\n    if as_dict:\n        d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n        return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n    return [\n        self.with_atoms(Atoms(df, _unchecked=True))\n        for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n    ]\n
    "},{"location":"api/#atomlib.HasAtomCell.select_schema","title":"select_schema","text":"
    select_schema(schema: SchemaDict) -> DataFrame\n

    Select columns from self and cast to the given schema. Raises TypeError if a column is not found or if it can't be cast.

    Source code in atomlib/atoms.py
    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n    \"\"\"\n    Select columns from `self` and cast to the given schema.\n    Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n    \"\"\"\n    return _select_schema(self, schema)\n
    "},{"location":"api/#atomlib.HasAtomCell.try_get_column","title":"try_get_column","text":"
    try_get_column(name: str) -> Optional[Series]\n

    Try to get a column from self, returning None if it doesn't exist.

    Source code in atomlib/atoms.py
    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n    \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n    try:\n        return self.get_column(name)\n    except polars.exceptions.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/#atomlib.HasAtomCell.deduplicate","title":"deduplicate","text":"
    deduplicate(\n    tol: float = 0.001,\n    subset: Iterable[str] = (\"x\", \"y\", \"z\", \"symbol\"),\n    keep: UniqueKeepStrategy = \"first\",\n    maintain_order: bool = True,\n) -> Self\n

    De-duplicate atoms in self. Atoms of the same symbol that are closer than tolerance to each other (by Euclidian distance) will be removed, leaving only the atom specified by keep (defaults to the first atom).

    If subset is specified, only those columns will be included while assessing duplicates. Floating point columns other than 'x', 'y', and 'z' will not by toleranced.

    Source code in atomlib/atoms.py
    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n    \"\"\"\n    De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n    to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n    `keep` (defaults to the first atom).\n\n    If `subset` is specified, only those columns will be included while assessing duplicates.\n    Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n    \"\"\"\n    import scipy.spatial\n\n    cols = set((subset,) if isinstance(subset, str) else subset)\n\n    indices = numpy.arange(len(self))\n\n    spatial_cols = cols.intersection(('x', 'y', 'z'))\n    cols -= spatial_cols\n    if len(spatial_cols) > 0:\n        coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n        tree = scipy.spatial.KDTree(coords)\n\n        # TODO This is a bad algorithm\n        while True:\n            changed = False\n            for (i, j) in tree.query_pairs(tol, 2.):\n                # whenever we encounter a pair, ensure their index matches\n                i_i, i_j = indices[[i, j]]\n                if i_i != i_j:\n                    indices[i] = indices[j] = min(i_i, i_j)\n                    changed = True\n            if not changed:\n                break\n\n        self = self.with_column(polars.Series('_unique_pts', indices))\n        cols.add('_unique_pts')\n\n    frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n    if len(spatial_cols) > 0:\n        frame = frame.drop('_unique_pts')\n\n    return self.with_atoms(Atoms(frame, _unchecked=True))\n
    "},{"location":"api/#atomlib.HasAtomCell.with_bounds","title":"with_bounds","text":"
    with_bounds(\n    cell_size: Optional[VecLike] = None,\n    cell_origin: Optional[VecLike] = None,\n) -> \"AtomCell\"\n

    Return a periodic cell with the given orthogonal cell dimensions.

    If cell_size is not specified, it will be assumed (and may be incorrect).

    Source code in atomlib/atoms.py
    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n    \"\"\"\n    Return a periodic cell with the given orthogonal cell dimensions.\n\n    If cell_size is not specified, it will be assumed (and may be incorrect).\n    \"\"\"\n    # TODO: test this\n    from .atomcell import AtomCell\n\n    if cell_size is None:\n        warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n        cell_size = self.bbox().size\n        cell_origin = self.bbox().min\n\n    # TODO test this origin code\n    cell = Cell.from_unit_cell(cell_size)\n    if cell_origin is not None:\n        cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n    return AtomCell(self.get_atoms(), cell, frame='local')\n
    "},{"location":"api/#atomlib.HasAtomCell.x","title":"x","text":"
    x() -> Expr\n
    Source code in atomlib/atoms.py
    def x(self) -> polars.Expr:\n    return polars.col('coords').arr.get(0).alias('x')\n
    "},{"location":"api/#atomlib.HasAtomCell.y","title":"y","text":"
    y() -> Expr\n
    Source code in atomlib/atoms.py
    def y(self) -> polars.Expr:\n    return polars.col('coords').arr.get(1).alias('y')\n
    "},{"location":"api/#atomlib.HasAtomCell.z","title":"z","text":"
    z() -> Expr\n
    Source code in atomlib/atoms.py
    def z(self) -> polars.Expr:\n    return polars.col('coords').arr.get(2).alias('z')\n
    "},{"location":"api/#atomlib.HasAtomCell.types","title":"types","text":"
    types() -> Optional[Series]\n

    Returns a Series of atom types (dtype polars.Int32).

    Source code in atomlib/atoms.py
    def types(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('type')\n
    "},{"location":"api/#atomlib.HasAtomCell.masses","title":"masses","text":"
    masses() -> Optional[Series]\n

    Returns a Series of atom masses (dtype polars.Float32).

    Source code in atomlib/atoms.py
    def masses(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('mass')\n
    "},{"location":"api/#atomlib.HasAtomCell.pos","title":"pos","text":"
    pos(\n    x: Union[Sequence[Optional[float]], float, None] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n

    Select all atoms at a given position.

    Formally, returns all atoms within a cube of radius tol centered at (x,y,z), exclusive of the cube's surface.

    Additional parameters given as kwargs will be checked as additional parameters (with strict equality).

    Source code in atomlib/atoms.py
    def pos(self,\n        x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n        y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n        tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n    \"\"\"\n    Select all atoms at a given position.\n\n    Formally, returns all atoms within a cube of radius ``tol``\n    centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n    Additional parameters given as ``kwargs`` will be checked\n    as additional parameters (with strict equality).\n    \"\"\"\n\n    if isinstance(x, t.Sequence):\n        (x, y, z) = x\n\n    tol = abs(float(tol))\n    selection = polars.lit(True)\n    if x is not None:\n        selection &= self.x().is_between(x - tol, x + tol, closed='none')\n    if y is not None:\n        selection &= self.y().is_between(y - tol, y + tol, closed='none')\n    if z is not None:\n        selection &= self.z().is_between(z - tol, z + tol, closed='none')\n    for (col, val) in kwargs.items():\n        selection &= (polars.col(col) == val)\n\n    return selection\n
    "},{"location":"api/#atomlib.HasAtomCell.apply_occupancy","title":"apply_occupancy","text":"
    apply_occupancy(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    For each atom in self, use its frac_occupancy to randomly decide whether to remove it.

    Source code in atomlib/atoms.py
    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n    \"\"\"\n    if 'frac_occupancy' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    frac = self.select('frac_occupancy').to_series().to_numpy()\n    choice = rng.binomial(1, frac).astype(numpy.bool_)\n    return self.filter(polars.lit(choice))\n
    "},{"location":"api/#atomlib.HasAtomCell.get_frame","title":"get_frame abstractmethod","text":"
    get_frame() -> CoordinateFrame\n

    Get the coordinate frame atoms are stored in.

    Source code in atomlib/atomcell.py
    @abc.abstractmethod\ndef get_frame(self) -> CoordinateFrame:\n    \"\"\"Get the coordinate frame atoms are stored in.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_atoms","title":"with_atoms abstractmethod","text":"
    with_atoms(\n    atoms: HasAtoms, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Replace the atoms in self. If no coordinate frame is specified, keep the coordinate frame unchanged.

    Source code in atomlib/atomcell.py
    @abc.abstractmethod\ndef with_atoms(self, atoms: HasAtoms, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Replace the atoms in `self`. If no coordinate frame is specified, keep the coordinate frame unchanged.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_cell","title":"with_cell","text":"
    with_cell(cell: Cell) -> Self\n

    Replace the cell in self, without touching the atomic coordinates.

    Source code in atomlib/atomcell.py
    def with_cell(self, cell: Cell) -> Self:\n    \"\"\"\n    Replace the cell in `self`, without touching the atomic coordinates.\n    \"\"\"\n    return self.to_frame('local').with_cell(cell)\n
    "},{"location":"api/#atomlib.HasAtomCell.get_atomcell","title":"get_atomcell","text":"
    get_atomcell() -> AtomCell\n
    Source code in atomlib/atomcell.py
    def get_atomcell(self) -> AtomCell:\n    frame = self.get_frame()\n    return AtomCell(self.get_atoms(frame), self.get_cell(), frame=frame, keep_frame=True)\n
    "},{"location":"api/#atomlib.HasAtomCell.get_atoms","title":"get_atoms abstractmethod","text":"
    get_atoms(frame: Optional[CoordinateFrame] = None) -> Atoms\n

    Get atoms contained in self, in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @abc.abstractmethod\ndef get_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> Atoms:\n    \"\"\"Get atoms contained in `self`, in the given coordinate frame.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.bbox_atoms","title":"bbox_atoms","text":"
    bbox_atoms(\n    frame: Optional[CoordinateFrame] = None,\n) -> BBox3D\n

    Return the bounding box of all the atoms in self, in the given coordinate frame.

    Source code in atomlib/atomcell.py
    def bbox_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> BBox3D:\n    \"\"\"Return the bounding box of all the atoms in `self`, in the given coordinate frame.\"\"\"\n    return self.get_atoms(frame).bbox()\n
    "},{"location":"api/#atomlib.HasAtomCell.bbox","title":"bbox","text":"
    bbox(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the combined bounding box of the cell and atoms in the given coordinate system. To get the cell or atoms bounding box only, use bbox_cell or bbox_atoms.

    Source code in atomlib/atomcell.py
    def bbox(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"\n    Return the combined bounding box of the cell and atoms in the given coordinate system.\n    To get the cell or atoms bounding box only, use [`bbox_cell`][atomlib.atomcell.HasAtomCell.bbox_cell] or [`bbox_atoms`][atomlib.atomcell.HasAtomCell.bbox_atoms].\n    \"\"\"\n    return self.bbox_atoms(frame) | self.bbox_cell(frame)\n
    "},{"location":"api/#atomlib.HasAtomCell.to_frame","title":"to_frame","text":"
    to_frame(frame: CoordinateFrame) -> Self\n

    Convert the stored Atoms to the given coordinate frame.

    Source code in atomlib/atomcell.py
    def to_frame(self, frame: CoordinateFrame) -> Self:\n    \"\"\"Convert the stored Atoms to the given coordinate frame.\"\"\"\n    return self.with_atoms(self.get_atoms(frame), frame)\n
    "},{"location":"api/#atomlib.HasAtomCell.transform_atoms","title":"transform_atoms","text":"
    transform_atoms(\n    transform: IntoTransform3D,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: CoordinateFrame = \"local\",\n    transform_velocities: bool = False\n) -> Self\n

    Transform the atoms in self by transform. If selection is given, only transform the atoms in selection.

    Source code in atomlib/atomcell.py
    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                    frame: CoordinateFrame = 'local', transform_velocities: bool = False) -> Self:\n    \"\"\"\n    Transform the atoms in `self` by `transform`.\n    If `selection` is given, only transform the atoms in `selection`.\n    \"\"\"\n    transform = self.change_transform(Transform3D.make(transform), self.get_frame(), frame)\n    return self.with_atoms(self.get_atoms(self.get_frame()).transform(transform, selection, transform_velocities=transform_velocities))\n
    "},{"location":"api/#atomlib.HasAtomCell.transform_cell","title":"transform_cell","text":"
    transform_cell(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> Self\n

    Apply the given transform to the unit cell, without changing atom positions. The transform is applied in coordinate frame 'frame'.

    Source code in atomlib/atomcell.py
    def transform_cell(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n    \"\"\"\n    Apply the given transform to the unit cell, without changing atom positions.\n    The transform is applied in coordinate frame 'frame'.\n    \"\"\"\n    return self.with_cell(self.get_cell().transform_cell(transform, frame=frame))\n
    "},{"location":"api/#atomlib.HasAtomCell.transform","title":"transform","text":"
    transform(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> Self\n
    Source code in atomlib/atomcell.py
    def transform(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n    if isinstance(transform, Transform3D) and not isinstance(transform, AffineTransform3D):\n        raise ValueError(\"Non-affine transforms cannot change the box dimensions. Use 'transform_atoms' instead.\")\n    # TODO: cleanup once tests pass\n    # coordinate change the transform into atomic coordinates\n    new_cell = self.get_cell().transform_cell(transform, frame)\n    transform = self.get_cell().change_transform(transform, self.get_frame(), frame)\n    return self.with_atoms(self.get_atoms().transform(transform), self.get_frame()).with_cell(new_cell)\n
    "},{"location":"api/#atomlib.HasAtomCell.crop","title":"crop","text":"
    crop(\n    x_min: float = -numpy.inf,\n    x_max: float = numpy.inf,\n    y_min: float = -numpy.inf,\n    y_max: float = numpy.inf,\n    z_min: float = -numpy.inf,\n    z_max: float = numpy.inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> Self\n

    Crop atoms and cell to the given extents. For a non-orthogonal cell, this must be specified in cell coordinates. This function implicity explodes the cell as well.

    To crop atoms only, use crop_atoms instead.

    Source code in atomlib/atomcell.py
    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n         frame: CoordinateFrame = 'local') -> Self:\n    \"\"\"\n    Crop atoms and cell to the given extents. For a non-orthogonal\n    cell, this must be specified in cell coordinates. This\n    function implicity `explode`s the cell as well.\n\n    To crop atoms only, use `crop_atoms` instead.\n    \"\"\"\n\n    cell = self.get_cell().crop(x_min, x_max, y_min, y_max, z_min, z_max, frame=frame)\n    atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n    return self.with_cell(cell).with_atoms(atoms)\n
    "},{"location":"api/#atomlib.HasAtomCell.crop_atoms","title":"crop_atoms","text":"
    crop_atoms(\n    x_min: float = -numpy.inf,\n    x_max: float = numpy.inf,\n    y_min: float = -numpy.inf,\n    y_max: float = numpy.inf,\n    z_min: float = -numpy.inf,\n    z_max: float = numpy.inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> Self\n
    Source code in atomlib/atomcell.py
    def crop_atoms(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n               y_min: float = -numpy.inf, y_max: float = numpy.inf,\n               z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n               frame: CoordinateFrame = 'local') -> Self:\n    atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n    return self.with_atoms(atoms)\n
    "},{"location":"api/#atomlib.HasAtomCell.crop_to_box","title":"crop_to_box","text":"
    crop_to_box(eps: float = 1e-05) -> Self\n
    Source code in atomlib/atomcell.py
    def crop_to_box(self, eps: float = 1e-5) -> Self:\n    atoms = self._transform_atoms_in_frame('cell_box', lambda atoms: atoms.crop_atoms(*([-eps, 1-eps]*3)))\n    return self.with_atoms(atoms)\n
    "},{"location":"api/#atomlib.HasAtomCell.wrap","title":"wrap","text":"
    wrap(eps: float = 1e-05) -> Self\n

    Wrap atoms around the cell boundaries.

    Source code in atomlib/atomcell.py
    def wrap(self, eps: float = 1e-5) -> Self:\n    \"\"\"Wrap atoms around the cell boundaries.\"\"\"\n    return self.with_atoms(self._transform_atoms_in_frame('cell_box', lambda a: a._wrap(eps)))\n
    "},{"location":"api/#atomlib.HasAtomCell.repeat","title":"repeat","text":"
    repeat(n: Union[int, VecLike]) -> Self\n

    Tile the cell

    Source code in atomlib/atomcell.py
    def repeat(self, n: t.Union[int, VecLike]) -> Self:\n    \"\"\"Tile the cell\"\"\"\n    ns = numpy.broadcast_to(n, 3)\n    if not numpy.issubdtype(ns.dtype, numpy.integer):\n        raise ValueError(\"repeat() argument must be an integer or integer array.\")\n\n    cells = numpy.stack(numpy.meshgrid(*map(numpy.arange, ns))) \\\n        .reshape(3, -1).T.astype(float)\n    cells = cells * self.box_size\n\n    atoms = self.get_atoms('cell')\n    atoms = Atoms.concat([\n        atoms.transform(AffineTransform3D.translate(cell))\n        for cell in cells\n    ]) #.transform(self.cell.get_transform('local', 'cell_frac'))\n    return self.with_atoms(atoms, 'cell').with_cell(self.get_cell().repeat(ns))\n
    "},{"location":"api/#atomlib.HasAtomCell.repeat_to","title":"repeat_to","text":"
    repeat_to(\n    size: VecLike, crop: Union[bool, Sequence[bool]] = False\n) -> Self\n

    Repeat the cell so it is at least size along the crystal's axes.

    If crop, then crop the cell to exactly size. This may break periodicity. crop may be a vector, in which case you can specify cropping only along some axes.

    Source code in atomlib/atomcell.py
    def repeat_to(self, size: VecLike, crop: t.Union[bool, t.Sequence[bool]] = False) -> Self:\n    \"\"\"\n    Repeat the cell so it is at least `size` along the crystal's axes.\n\n    If `crop`, then crop the cell to exactly `size`. This may break periodicity.\n    `crop` may be a vector, in which case you can specify cropping only along some axes.\n    \"\"\"\n    size = to_vec3(size)\n    cell_size = self.cell_size * self.n_cells\n    repeat = numpy.maximum(numpy.ceil(size / cell_size).astype(int), 1)\n    atom_cell = self.repeat(repeat)\n\n    crop_v = to_vec3(crop, dtype=numpy.bool_)\n    if numpy.any(crop_v):\n        crop_x, crop_y, crop_z = crop_v\n        return atom_cell.crop(\n            x_max = size[0] if crop_x else numpy.inf,\n            y_max = size[1] if crop_y else numpy.inf,\n            z_max = size[2] if crop_z else numpy.inf,\n            frame='cell'\n        )\n\n    return atom_cell\n
    "},{"location":"api/#atomlib.HasAtomCell.repeat_x","title":"repeat_x","text":"
    repeat_x(n: int) -> Self\n

    Tile the cell in the x axis.

    Source code in atomlib/atomcell.py
    def repeat_x(self, n: int) -> Self:\n    \"\"\"Tile the cell in the x axis.\"\"\"\n    return self.repeat((n, 1, 1))\n
    "},{"location":"api/#atomlib.HasAtomCell.repeat_y","title":"repeat_y","text":"
    repeat_y(n: int) -> Self\n

    Tile the cell in the y axis.

    Source code in atomlib/atomcell.py
    def repeat_y(self, n: int) -> Self:\n    \"\"\"Tile the cell in the y axis.\"\"\"\n    return self.repeat((1, n, 1))\n
    "},{"location":"api/#atomlib.HasAtomCell.repeat_z","title":"repeat_z","text":"
    repeat_z(n: int) -> Self\n

    Tile the cell in the z axis.

    Source code in atomlib/atomcell.py
    def repeat_z(self, n: int) -> Self:\n    \"\"\"Tile the cell in the z axis.\"\"\"\n    return self.repeat((1, 1, n))\n
    "},{"location":"api/#atomlib.HasAtomCell.repeat_to_x","title":"repeat_to_x","text":"
    repeat_to_x(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the x axis.

    Source code in atomlib/atomcell.py
    def repeat_to_x(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the x axis.\"\"\"\n    return self.repeat_to([size, 0., 0.], [crop, False, False])\n
    "},{"location":"api/#atomlib.HasAtomCell.repeat_to_y","title":"repeat_to_y","text":"
    repeat_to_y(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the y axis.

    Source code in atomlib/atomcell.py
    def repeat_to_y(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the y axis.\"\"\"\n    return self.repeat_to([0., size, 0.], [False, crop, False])\n
    "},{"location":"api/#atomlib.HasAtomCell.repeat_to_z","title":"repeat_to_z","text":"
    repeat_to_z(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the z axis.

    Source code in atomlib/atomcell.py
    def repeat_to_z(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the z axis.\"\"\"\n    return self.repeat_to([0., 0., size], [False, False, crop])\n
    "},{"location":"api/#atomlib.HasAtomCell.repeat_to_aspect","title":"repeat_to_aspect","text":"
    repeat_to_aspect(\n    plane: Literal[\"xy\", \"xz\", \"yz\"] = \"xy\",\n    *,\n    aspect: float = 1.0,\n    min_size: Optional[VecLike] = None,\n    max_size: Optional[VecLike] = None\n) -> Self\n

    Repeat to optimize the aspect ratio in plane, while staying above min_size and under max_size.

    Source code in atomlib/atomcell.py
    def repeat_to_aspect(self, plane: t.Literal['xy', 'xz', 'yz'] = 'xy', *,\n                     aspect: float = 1., min_size: t.Optional[VecLike] = None,\n                     max_size: t.Optional[VecLike] = None) -> Self:\n    \"\"\"\n    Repeat to optimize the aspect ratio in `plane`,\n    while staying above `min_size` and under `max_size`.\n    \"\"\"\n    if min_size is None:\n        min_n = numpy.array([1, 1, 1], numpy.int_)\n    else:\n        min_n = numpy.maximum(numpy.ceil(to_vec3(min_size) / self.box_size), 1).astype(numpy.int_)\n\n    if max_size is None:\n        max_n = 3 * min_n\n    else:\n        max_n = numpy.maximum(numpy.floor(to_vec3(max_size) / self.box_size), 1).astype(numpy.int_)\n\n    if plane == 'xy':\n        indices = [0, 1]\n    elif plane == 'xz':\n        indices = [0, 2]\n    elif plane == 'yz':\n        indices = [1, 2]\n    else:\n        raise ValueError(f\"Invalid plane '{plane}'. Exepcted 'xy', 'xz', 'or 'yz'.\")\n\n    na = numpy.arange(min_n[indices[0]], max_n[indices[0]])\n    nb = numpy.arange(min_n[indices[1]], max_n[indices[1]])\n    (na, nb) = numpy.meshgrid(na, nb)\n\n    aspects = na * self.box_size[indices[0]] / (nb * self.box_size[indices[1]])\n    # cost function: log(aspect)^2  (so cost(0.5) == cost(2))\n    min_i = numpy.argmin(numpy.log(aspects / aspect)**2)\n    repeat = numpy.array([1, 1, 1], numpy.int_)\n    repeat[indices] = na.flatten()[min_i], nb.flatten()[min_i]\n    return self.repeat(repeat)\n
    "},{"location":"api/#atomlib.HasAtomCell.explode","title":"explode","text":"
    explode() -> Self\n

    Materialize repeated cells as one supercell.

    Source code in atomlib/atomcell.py
    def explode(self) -> Self:\n    \"\"\"Materialize repeated cells as one supercell.\"\"\"\n    frame = self.get_frame()\n\n    return self.with_atoms(self.get_atoms('local'), 'local') \\\n        .with_cell(self.get_cell().explode()) \\\n        .to_frame(frame)\n
    "},{"location":"api/#atomlib.HasAtomCell.periodic_duplicate","title":"periodic_duplicate","text":"
    periodic_duplicate(eps: float = 1e-05) -> Self\n

    Add duplicate copies of atoms near periodic boundaries.

    For instance, an atom at a corner will be duplicated into 8 copies. This is mostly only useful for visualization.

    Source code in atomlib/atomcell.py
    def periodic_duplicate(self, eps: float = 1e-5) -> Self:\n    \"\"\"\n    Add duplicate copies of atoms near periodic boundaries.\n\n    For instance, an atom at a corner will be duplicated into 8 copies.\n    This is mostly only useful for visualization.\n    \"\"\"\n    frame_save = self.get_frame()\n    self = self.to_frame('cell_box').wrap(eps=eps)\n\n    for i in range(3):\n        self = self.concat((self,\n            self.filter(polars.col('coords').arr.get(i).abs() <= eps, frame='cell_box')\n                .transform_atoms(AffineTransform3D.translate([1. if i == j else 0. for j in range(3)]), frame='cell_box')\n        ))\n\n    return self.to_frame(frame_save)\n
    "},{"location":"api/#atomlib.HasAtomCell.describe","title":"describe","text":"
    describe(\n    percentiles: Union[Sequence[float], float, None] = (\n        0.25,\n        0.5,\n        0.75,\n    ),\n    *,\n    interpolation: RollingInterpolationMethod = \"nearest\",\n    frame: Optional[CoordinateFrame] = None\n) -> DataFrame\n

    Return summary statistics for self. See DataFrame.describe for more information.

    PARAMETER DESCRIPTION percentiles

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), 50% (median), and 75% (third quartile).

    TYPE: Union[Sequence[float], float, None] DEFAULT: (0.25, 0.5, 0.75)

    RETURNS DESCRIPTION DataFrame

    A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n             interpolation: RollingInterpolationMethod = 'nearest',\n             frame: t.Optional[CoordinateFrame] = None) -> polars.DataFrame:\n    \"\"\"\n    Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n    Args:\n      percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                   50% (median), and 75% (third quartile).\n\n    Returns:\n      A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_columns","title":"with_columns","text":"
    with_columns(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self\n

    Return a copy of self with the given columns added.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_columns(self,\n                 *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 frame: t.Optional[CoordinateFrame] = None,\n                 **named_exprs: IntoExpr) -> Self:\n    \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.get_column","title":"get_column","text":"
    get_column(\n    name: str, *, frame: Optional[CoordinateFrame] = None\n) -> Series\n

    Get the specified column from self, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef get_column(self, name: str, *, frame: t.Optional[CoordinateFrame] = None) -> polars.Series:\n    \"\"\"\n    Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.get_columns","title":"get_columns","text":"
    get_columns(\n    *, frame: Optional[CoordinateFrame] = None\n) -> List[Series]\n

    Return all columns from self as a list of Series.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef get_columns(self, *, frame: t.Optional[CoordinateFrame] = None) -> t.List[polars.Series]:\n    \"\"\"\n    Return all columns from `self` as a list of [`Series`][polars.Series].\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.group_by","title":"group_by","text":"
    group_by(\n    *by: Union[IntoExpr, Iterable[IntoExpr]],\n    maintain_order: bool = False,\n    frame: Optional[CoordinateFrame] = None,\n    **named_by: IntoExpr\n) -> GroupBy\n

    Start a group by operation. See DataFrame.group_by for more information.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n             maintain_order: bool = False, frame: t.Optional[CoordinateFrame] = None,\n             **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n    \"\"\"\n    Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.pipe","title":"pipe","text":"
    pipe(\n    function: Callable[Concatenate[HasAtomCellT, P], T],\n    *args: args,\n    **kwargs: kwargs\n) -> T\n

    Apply function to self (in method-call syntax).

    Source code in atomlib/atomcell.py
    def pipe(self: HasAtomCellT, function: t.Callable[Concatenate[HasAtomCellT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n    \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n    return function(self, *args, **kwargs)\n
    "},{"location":"api/#atomlib.HasAtomCell.filter","title":"filter","text":"
    filter(\n    *predicates: Union[\n        None,\n        IntoExprColumn,\n        Iterable[IntoExprColumn],\n        bool,\n        List[bool],\n        ndarray,\n    ],\n    frame: Optional[CoordinateFrame] = None,\n    **constraints: Any\n) -> Self\n

    Filter self, removing rows which evaluate to False.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef filter(\n    self,\n    *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n    frame: t.Optional[CoordinateFrame] = None,\n    **constraints: t.Any,\n) -> Self:\n    \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.sort","title":"sort","text":"
    sort(\n    by: Union[IntoExpr, Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: Union[bool, Sequence[bool]] = False,\n    nulls_last: bool = False\n) -> Self\n

    Sort the atoms in self by the given columns/expressions.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef sort(\n    self,\n    by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: t.Union[bool, t.Sequence[bool]] = False,\n    nulls_last: bool = False,\n) -> Self:\n    \"\"\"\n    Sort the atoms in `self` by the given columns/expressions.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.slice","title":"slice","text":"
    slice(\n    offset: int,\n    length: Optional[int] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return a slice of the rows in self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef slice(self, offset: int, length: t.Optional[int] = None, *,\n          frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return a slice of the rows in `self`.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.head","title":"head","text":"
    head(\n    n: int = 5, *, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return the first n rows of self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef head(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return the first `n` rows of `self`.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.tail","title":"tail","text":"
    tail(\n    n: int = 5, *, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return the last n rows of self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef tail(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return the last `n` rows of `self`.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.fill_null","title":"fill_null","text":"
    fill_null(\n    value: Any = None,\n    strategy: Optional[FillNullStrategy] = None,\n    limit: Optional[int] = None,\n    matches_supertype: bool = True,\n) -> Self\n
    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef fill_null(\n    self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n    limit: t.Optional[int] = None, matches_supertype: bool = True,\n) -> Self:\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.fill_nan","title":"fill_nan","text":"
    fill_nan(\n    value: Union[Expr, int, float, None],\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n
    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef fill_nan(self, value: t.Union[polars.Expr, int, float, None], *,\n             frame: t.Optional[CoordinateFrame] = None) -> Self:\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.select","title":"select","text":"
    select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef select(\n    self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> polars.DataFrame:\n    \"\"\"\n    Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.select_props","title":"select_props","text":"
    select_props(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self\n

    Select exprs from self, while keeping required columns. Doesn't affect the cell.

    RETURNS DESCRIPTION Self

    A HasAtomCell filtered to contain

    Self

    the specified properties (as well as required columns).

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef select_props(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self:\n    \"\"\"\n    Select `exprs` from `self`, while keeping required columns.\n    Doesn't affect the cell.\n\n    Returns:\n      A [`HasAtomCell`][atomlib.atomcell.HasAtomCell] filtered to contain\n      the specified properties (as well as required columns).\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.try_select","title":"try_select","text":"
    try_select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Optional[DataFrame]\n

    Try to select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns. Returns None if any columns are missing.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef try_select(\n    self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> t.Optional[polars.DataFrame]:\n    \"\"\"\n    Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns. Returns `None` if any\n    columns are missing.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.round_near_zero","title":"round_near_zero","text":"
    round_near_zero(\n    tol: float = 1e-14,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Round atom position values near zero to zero.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef round_near_zero(self, tol: float = 1e-14, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Round atom position values near zero to zero.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.coords","title":"coords","text":"
    coords(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> NDArray[float64]\n

    Return a (N, 3) ndarray of atom positions (dtype numpy.float64) in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> NDArray[numpy.float64]:\n    \"\"\"\n    Return a `(N, 3)` ndarray of atom positions (dtype [`numpy.float64`][numpy.float64])\n    in the given coordinate frame.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.velocities","title":"velocities","text":"
    velocities(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Optional[NDArray[float64]]\n

    Return a (N, 3) ndarray of atom velocities (dtype numpy.float64) in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef velocities(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> t.Optional[NDArray[numpy.float64]]:\n    \"\"\"\n    Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64])\n    in the given coordinate frame.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.add_atom","title":"add_atom","text":"
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: Union[ArrayLike, float],\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None,\n    **kwargs: Any,\n) -> Self\n

    Return a copy of self with an extra atom.

    By default, all extra columns present in self must be specified as **kwargs.

    Try to avoid calling this in a loop (Use concat instead).

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef add_atom(self, elem: t.Union[int, str], /,  # type: ignore (spurious)\n             x: t.Union[ArrayLike, float],\n             y: t.Optional[float] = None,\n             z: t.Optional[float] = None, *,\n             frame: t.Optional[CoordinateFrame] = None,\n             **kwargs: t.Any) -> Self:\n    \"\"\"\n    Return a copy of `self` with an extra atom.\n\n    By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n    Try to avoid calling this in a loop (Use [`concat`][atomlib.atomcell.HasAtomCell.concat] instead).\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_index","title":"with_index","text":"
    with_index(\n    index: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Returns self with a row index added in column 'i' (dtype polars.Int64). If index is not specified, defaults to an existing index or a new index.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_index(self, index: t.Optional[AtomValues] = None, *,\n               frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n    If `index` is not specified, defaults to an existing index or a new index.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_wobble","title":"with_wobble","text":"
    with_wobble(\n    wobble: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given displacements in column 'wobble' (dtype polars.Float64). If wobble is not specified, defaults to the already-existing wobbles or 0.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_wobble(self, wobble: t.Optional[AtomValues] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_occupancy","title":"with_occupancy","text":"
    with_occupancy(\n    frac_occupancy: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given fractional occupancies (dtype polars.Float64). If frac_occupancy is not specified, defaults to the already-existing occupancies or 1.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None, *,\n                   frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.apply_wobble","title":"apply_wobble","text":"
    apply_wobble(\n    rng: Union[Generator, int, None] = None,\n    frame: Optional[CoordinateFrame] = None,\n) -> Self\n

    Displace the atoms in self by the amount in the wobble column. wobble is interpretated as a mean-squared displacement, which is distributed equally over each axis.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None,\n                 frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Displace the atoms in `self` by the amount in the `wobble` column.\n    `wobble` is interpretated as a mean-squared displacement, which is distributed\n    equally over each axis.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_type","title":"with_type","text":"
    with_type(\n    types: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atom types in column 'type'. If types is not specified, use the already existing types or auto-assign them.

    When auto-assigning, each symbol is given a unique value, case-sensitive. Values are assigned from lowest atomic number to highest. For instance: [\"Ag+\", \"Na\", \"H\", \"Ag\"] => [3, 11, 1, 2]

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_type(self, types: t.Optional[AtomValues] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom types in column 'type'.\n    If `types` is not specified, use the already existing types or auto-assign them.\n\n    When auto-assigning, each symbol is given a unique value, case-sensitive.\n    Values are assigned from lowest atomic number to highest.\n    For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_mass","title":"with_mass","text":"
    with_mass(\n    mass: Optional[ArrayLike] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atom masses in column 'mass'. If mass is not specified, use the already existing masses or auto-assign them.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_mass(self, mass: t.Optional[ArrayLike] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom masses in column `'mass'`.\n    If `mass` is not specified, use the already existing masses or auto-assign them.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_symbol","title":"with_symbol","text":"
    with_symbol(\n    symbols: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atomic symbols.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atomic symbols.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_coords","title":"with_coords","text":"
    with_coords(\n    pts: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self replaced with the given atomic positions.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic positions.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_velocity","title":"with_velocity","text":"
    with_velocity(\n    pts: Optional[ArrayLike] = None,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self replaced with the given atomic velocities. If pts is not specified, use the already existing velocities or zero.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                  selection: t.Optional[AtomSelection] = None, *,\n                  frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic velocities.\n    If `pts` is not specified, use the already existing velocities or zero.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/","title":"atomlib.atomcell","text":"

    Atoms with an associated Cell.

    This module defines HasAtomCell and the concrete AtomCell, which combines the functionality of HasAtoms and HasCell.

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell","title":"HasAtomCell","text":"

    Bases: HasAtoms, HasCell, ABC

    Source code in atomlib/atomcell.py
    class HasAtomCell(HasAtoms, HasCell, abc.ABC):\n    @abc.abstractmethod\n    def get_frame(self) -> CoordinateFrame:\n        \"\"\"Get the coordinate frame atoms are stored in.\"\"\"\n        ...\n\n    @abc.abstractmethod\n    def with_atoms(self, atoms: HasAtoms, frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Replace the atoms in `self`. If no coordinate frame is specified, keep the coordinate frame unchanged.\n        \"\"\"\n        ...\n\n    def with_cell(self, cell: Cell) -> Self:\n        \"\"\"\n        Replace the cell in `self`, without touching the atomic coordinates.\n        \"\"\"\n        return self.to_frame('local').with_cell(cell)\n\n    def get_atomcell(self) -> AtomCell:\n        frame = self.get_frame()\n        return AtomCell(self.get_atoms(frame), self.get_cell(), frame=frame, keep_frame=True)\n\n    @abc.abstractmethod\n    def get_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> Atoms:\n        \"\"\"Get atoms contained in `self`, in the given coordinate frame.\"\"\"\n        ...\n\n    def bbox_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> BBox3D:\n        \"\"\"Return the bounding box of all the atoms in `self`, in the given coordinate frame.\"\"\"\n        return self.get_atoms(frame).bbox()\n\n    def bbox(self, frame: CoordinateFrame = 'local') -> BBox3D:\n        \"\"\"\n        Return the combined bounding box of the cell and atoms in the given coordinate system.\n        To get the cell or atoms bounding box only, use [`bbox_cell`][atomlib.atomcell.HasAtomCell.bbox_cell] or [`bbox_atoms`][atomlib.atomcell.HasAtomCell.bbox_atoms].\n        \"\"\"\n        return self.bbox_atoms(frame) | self.bbox_cell(frame)\n\n    # transformation\n\n    def _transform_atoms_in_frame(self, frame: t.Optional[CoordinateFrame], f: t.Callable[[Atoms], Atoms]) -> Atoms:\n        # ugly code\n        if frame is None or frame == self.get_frame():\n            return f(self.get_atoms())\n        return f(self.get_atoms(frame)).transform(self.get_transform(self.get_frame(), frame))\n\n    def to_frame(self, frame: CoordinateFrame) -> Self:\n        \"\"\"Convert the stored Atoms to the given coordinate frame.\"\"\"\n        return self.with_atoms(self.get_atoms(frame), frame)\n\n    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                        frame: CoordinateFrame = 'local', transform_velocities: bool = False) -> Self:\n        \"\"\"\n        Transform the atoms in `self` by `transform`.\n        If `selection` is given, only transform the atoms in `selection`.\n        \"\"\"\n        transform = self.change_transform(Transform3D.make(transform), self.get_frame(), frame)\n        return self.with_atoms(self.get_atoms(self.get_frame()).transform(transform, selection, transform_velocities=transform_velocities))\n\n    def transform_cell(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n        \"\"\"\n        Apply the given transform to the unit cell, without changing atom positions.\n        The transform is applied in coordinate frame 'frame'.\n        \"\"\"\n        return self.with_cell(self.get_cell().transform_cell(transform, frame=frame))\n\n    def transform(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n        if isinstance(transform, Transform3D) and not isinstance(transform, AffineTransform3D):\n            raise ValueError(\"Non-affine transforms cannot change the box dimensions. Use 'transform_atoms' instead.\")\n        # TODO: cleanup once tests pass\n        # coordinate change the transform into atomic coordinates\n        new_cell = self.get_cell().transform_cell(transform, frame)\n        transform = self.get_cell().change_transform(transform, self.get_frame(), frame)\n        return self.with_atoms(self.get_atoms().transform(transform), self.get_frame()).with_cell(new_cell)\n\n    # crop methods\n\n    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n             y_min: float = -numpy.inf, y_max: float = numpy.inf,\n             z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n             frame: CoordinateFrame = 'local') -> Self:\n        \"\"\"\n        Crop atoms and cell to the given extents. For a non-orthogonal\n        cell, this must be specified in cell coordinates. This\n        function implicity `explode`s the cell as well.\n\n        To crop atoms only, use `crop_atoms` instead.\n        \"\"\"\n\n        cell = self.get_cell().crop(x_min, x_max, y_min, y_max, z_min, z_max, frame=frame)\n        atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n        return self.with_cell(cell).with_atoms(atoms)\n\n    def crop_atoms(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n                   y_min: float = -numpy.inf, y_max: float = numpy.inf,\n                   z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n                   frame: CoordinateFrame = 'local') -> Self:\n        atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n        return self.with_atoms(atoms)\n\n    def crop_to_box(self, eps: float = 1e-5) -> Self:\n        atoms = self._transform_atoms_in_frame('cell_box', lambda atoms: atoms.crop_atoms(*([-eps, 1-eps]*3)))\n        return self.with_atoms(atoms)\n\n    def wrap(self, eps: float = 1e-5) -> Self:\n        \"\"\"Wrap atoms around the cell boundaries.\"\"\"\n        return self.with_atoms(self._transform_atoms_in_frame('cell_box', lambda a: a._wrap(eps)))\n\n    def _repeat_to_contain(self, pts: numpy.ndarray, pad: int = 0, frame: CoordinateFrame = 'cell_frac') -> Self:\n        #print(f\"pts: {pts} in frame {frame}\")\n        pts = self.get_transform('cell_frac', frame) @ pts\n\n        bbox = BBox3D.unit() | BBox3D.from_pts(pts)\n        min_bounds = numpy.floor(bbox.min).astype(int) - pad\n        max_bounds = numpy.ceil(bbox.max).astype(int) + pad\n        #print(f\"tiling to {min_bounds}, {max_bounds}\")\n        repeat = max_bounds - min_bounds\n        cells = numpy.stack(numpy.meshgrid(*map(numpy.arange, repeat))).reshape(3, -1).T.astype(float)\n\n        atoms = self.get_atoms('cell_frac')\n        atoms = Atoms.concat([\n            atoms.transform(AffineTransform3D.translate(cell))\n            for cell in cells\n        ])\n        #print(f\"atoms:\\n{atoms}\")\n        cell = self.get_cell().repeat(repeat) \\\n            .transform_cell(AffineTransform3D.translate(min_bounds), 'cell_frac')\n        return self.with_cell(cell).with_atoms(atoms, 'cell_frac')\n\n    def repeat(self, n: t.Union[int, VecLike]) -> Self:\n        \"\"\"Tile the cell\"\"\"\n        ns = numpy.broadcast_to(n, 3)\n        if not numpy.issubdtype(ns.dtype, numpy.integer):\n            raise ValueError(\"repeat() argument must be an integer or integer array.\")\n\n        cells = numpy.stack(numpy.meshgrid(*map(numpy.arange, ns))) \\\n            .reshape(3, -1).T.astype(float)\n        cells = cells * self.box_size\n\n        atoms = self.get_atoms('cell')\n        atoms = Atoms.concat([\n            atoms.transform(AffineTransform3D.translate(cell))\n            for cell in cells\n        ]) #.transform(self.cell.get_transform('local', 'cell_frac'))\n        return self.with_atoms(atoms, 'cell').with_cell(self.get_cell().repeat(ns))\n\n    def repeat_to(self, size: VecLike, crop: t.Union[bool, t.Sequence[bool]] = False) -> Self:\n        \"\"\"\n        Repeat the cell so it is at least `size` along the crystal's axes.\n\n        If `crop`, then crop the cell to exactly `size`. This may break periodicity.\n        `crop` may be a vector, in which case you can specify cropping only along some axes.\n        \"\"\"\n        size = to_vec3(size)\n        cell_size = self.cell_size * self.n_cells\n        repeat = numpy.maximum(numpy.ceil(size / cell_size).astype(int), 1)\n        atom_cell = self.repeat(repeat)\n\n        crop_v = to_vec3(crop, dtype=numpy.bool_)\n        if numpy.any(crop_v):\n            crop_x, crop_y, crop_z = crop_v\n            return atom_cell.crop(\n                x_max = size[0] if crop_x else numpy.inf,\n                y_max = size[1] if crop_y else numpy.inf,\n                z_max = size[2] if crop_z else numpy.inf,\n                frame='cell'\n            )\n\n        return atom_cell\n\n    def repeat_x(self, n: int) -> Self:\n        \"\"\"Tile the cell in the x axis.\"\"\"\n        return self.repeat((n, 1, 1))\n\n    def repeat_y(self, n: int) -> Self:\n        \"\"\"Tile the cell in the y axis.\"\"\"\n        return self.repeat((1, n, 1))\n\n    def repeat_z(self, n: int) -> Self:\n        \"\"\"Tile the cell in the z axis.\"\"\"\n        return self.repeat((1, 1, n))\n\n    def repeat_to_x(self, size: float, crop: bool = False) -> Self:\n        \"\"\"Repeat the cell so it is at least size `size` along the x axis.\"\"\"\n        return self.repeat_to([size, 0., 0.], [crop, False, False])\n\n    def repeat_to_y(self, size: float, crop: bool = False) -> Self:\n        \"\"\"Repeat the cell so it is at least size `size` along the y axis.\"\"\"\n        return self.repeat_to([0., size, 0.], [False, crop, False])\n\n    def repeat_to_z(self, size: float, crop: bool = False) -> Self:\n        \"\"\"Repeat the cell so it is at least size `size` along the z axis.\"\"\"\n        return self.repeat_to([0., 0., size], [False, False, crop])\n\n    def repeat_to_aspect(self, plane: t.Literal['xy', 'xz', 'yz'] = 'xy', *,\n                         aspect: float = 1., min_size: t.Optional[VecLike] = None,\n                         max_size: t.Optional[VecLike] = None) -> Self:\n        \"\"\"\n        Repeat to optimize the aspect ratio in `plane`,\n        while staying above `min_size` and under `max_size`.\n        \"\"\"\n        if min_size is None:\n            min_n = numpy.array([1, 1, 1], numpy.int_)\n        else:\n            min_n = numpy.maximum(numpy.ceil(to_vec3(min_size) / self.box_size), 1).astype(numpy.int_)\n\n        if max_size is None:\n            max_n = 3 * min_n\n        else:\n            max_n = numpy.maximum(numpy.floor(to_vec3(max_size) / self.box_size), 1).astype(numpy.int_)\n\n        if plane == 'xy':\n            indices = [0, 1]\n        elif plane == 'xz':\n            indices = [0, 2]\n        elif plane == 'yz':\n            indices = [1, 2]\n        else:\n            raise ValueError(f\"Invalid plane '{plane}'. Exepcted 'xy', 'xz', 'or 'yz'.\")\n\n        na = numpy.arange(min_n[indices[0]], max_n[indices[0]])\n        nb = numpy.arange(min_n[indices[1]], max_n[indices[1]])\n        (na, nb) = numpy.meshgrid(na, nb)\n\n        aspects = na * self.box_size[indices[0]] / (nb * self.box_size[indices[1]])\n        # cost function: log(aspect)^2  (so cost(0.5) == cost(2))\n        min_i = numpy.argmin(numpy.log(aspects / aspect)**2)\n        repeat = numpy.array([1, 1, 1], numpy.int_)\n        repeat[indices] = na.flatten()[min_i], nb.flatten()[min_i]\n        return self.repeat(repeat)\n\n    def explode(self) -> Self:\n        \"\"\"Materialize repeated cells as one supercell.\"\"\"\n        frame = self.get_frame()\n\n        return self.with_atoms(self.get_atoms('local'), 'local') \\\n            .with_cell(self.get_cell().explode()) \\\n            .to_frame(frame)\n\n    def periodic_duplicate(self, eps: float = 1e-5) -> Self:\n        \"\"\"\n        Add duplicate copies of atoms near periodic boundaries.\n\n        For instance, an atom at a corner will be duplicated into 8 copies.\n        This is mostly only useful for visualization.\n        \"\"\"\n        frame_save = self.get_frame()\n        self = self.to_frame('cell_box').wrap(eps=eps)\n\n        for i in range(3):\n            self = self.concat((self,\n                self.filter(polars.col('coords').arr.get(i).abs() <= eps, frame='cell_box')\n                    .transform_atoms(AffineTransform3D.translate([1. if i == j else 0. for j in range(3)]), frame='cell_box')\n            ))\n\n        return self.to_frame(frame_save)\n\n    # add frame to some HasAtoms methods\n\n    @_fwd_atoms_get\n    def describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n                 interpolation: RollingInterpolationMethod = 'nearest',\n                 frame: t.Optional[CoordinateFrame] = None) -> polars.DataFrame:\n        \"\"\"\n        Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n        Args:\n          percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                       50% (median), and 75% (third quartile).\n\n        Returns:\n          A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_columns(self,\n                     *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                     frame: t.Optional[CoordinateFrame] = None,\n                     **named_exprs: IntoExpr) -> Self:\n        \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n        ...\n\n    with_column = with_columns\n\n    @_fwd_atoms_get\n    def get_column(self, name: str, *, frame: t.Optional[CoordinateFrame] = None) -> polars.Series:\n        \"\"\"\n        Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        ...\n\n    @_fwd_atoms_get\n    def get_columns(self, *, frame: t.Optional[CoordinateFrame] = None) -> t.List[polars.Series]:\n        \"\"\"\n        Return all columns from `self` as a list of [`Series`][polars.Series].\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        ...\n\n    @_fwd_atoms_get\n    def group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 maintain_order: bool = False, frame: t.Optional[CoordinateFrame] = None,\n                 **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n        \"\"\"\n        Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n        \"\"\"\n        ...\n\n    def pipe(self: HasAtomCellT, function: t.Callable[Concatenate[HasAtomCellT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n        \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n        return function(self, *args, **kwargs)\n\n    @_fwd_atoms_transform\n    def filter(\n        self,\n        *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n        frame: t.Optional[CoordinateFrame] = None,\n        **constraints: t.Any,\n    ) -> Self:\n        \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def sort(\n        self,\n        by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        *more_by: IntoExpr,\n        descending: t.Union[bool, t.Sequence[bool]] = False,\n        nulls_last: bool = False,\n    ) -> Self:\n        \"\"\"\n        Sort the atoms in `self` by the given columns/expressions.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def slice(self, offset: int, length: t.Optional[int] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"Return a slice of the rows in `self`.\"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def head(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"Return the first `n` rows of `self`.\"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def tail(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"Return the last `n` rows of `self`.\"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def fill_null(\n        self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n        limit: t.Optional[int] = None, matches_supertype: bool = True,\n    ) -> Self:\n        ...\n\n    @_fwd_atoms_transform\n    def fill_nan(self, value: t.Union[polars.Expr, int, float, None], *,\n                 frame: t.Optional[CoordinateFrame] = None) -> Self:\n        ...\n\n    # TODO: partition_by\n\n    @_fwd_atoms_get\n    def select(\n        self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        frame: t.Optional[CoordinateFrame] = None,\n        **named_exprs: IntoExpr\n    ) -> polars.DataFrame:\n        \"\"\"\n        Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n        Expressions may either be columns or expressions of columns.\n\n        [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def select_props(\n        self,\n        *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        frame: t.Optional[CoordinateFrame] = None,\n        **named_exprs: IntoExpr\n    ) -> Self:\n        \"\"\"\n        Select `exprs` from `self`, while keeping required columns.\n        Doesn't affect the cell.\n\n        Returns:\n          A [`HasAtomCell`][atomlib.atomcell.HasAtomCell] filtered to contain\n          the specified properties (as well as required columns).\n        \"\"\"\n        ...\n\n    @_fwd_atoms_get\n    def try_select(\n        self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        frame: t.Optional[CoordinateFrame] = None,\n        **named_exprs: IntoExpr\n    ) -> t.Optional[polars.DataFrame]:\n        \"\"\"\n        Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n        Expressions may either be columns or expressions of columns. Returns `None` if any\n        columns are missing.\n\n        [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def round_near_zero(self, tol: float = 1e-14, *,\n                        frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Round atom position values near zero to zero.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_get\n    def coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> NDArray[numpy.float64]:\n        \"\"\"\n        Return a `(N, 3)` ndarray of atom positions (dtype [`numpy.float64`][numpy.float64])\n        in the given coordinate frame.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_get\n    def velocities(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> t.Optional[NDArray[numpy.float64]]:\n        \"\"\"\n        Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64])\n        in the given coordinate frame.\n        \"\"\"\n        ...\n\n    @t.overload\n    def add_atom(self, elem: t.Union[int, str], x: ArrayLike, /, *,\n                 y: None = None, z: None = None, frame: t.Optional[CoordinateFrame] = None,\n                 **kwargs: t.Any) -> Self:\n        ...\n\n    @t.overload\n    def add_atom(self, elem: t.Union[int, str], /,\n                 x: float, y: float, z: float, *,\n                 frame: t.Optional[CoordinateFrame] = None,\n                 **kwargs: t.Any) -> Self:\n        ...\n\n    @_fwd_atoms_transform\n    def add_atom(self, elem: t.Union[int, str], /,  # type: ignore (spurious)\n                 x: t.Union[ArrayLike, float],\n                 y: t.Optional[float] = None,\n                 z: t.Optional[float] = None, *,\n                 frame: t.Optional[CoordinateFrame] = None,\n                 **kwargs: t.Any) -> Self:\n        \"\"\"\n        Return a copy of `self` with an extra atom.\n\n        By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n        Try to avoid calling this in a loop (Use [`concat`][atomlib.atomcell.HasAtomCell.concat] instead).\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_index(self, index: t.Optional[AtomValues] = None, *,\n                   frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n        If `index` is not specified, defaults to an existing index or a new index.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_wobble(self, wobble: t.Optional[AtomValues] = None, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n        If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None, *,\n                       frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n        If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None,\n                     frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Displace the atoms in `self` by the amount in the `wobble` column.\n        `wobble` is interpretated as a mean-squared displacement, which is distributed\n        equally over each axis.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_type(self, types: t.Optional[AtomValues] = None, *,\n                  frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atom types in column 'type'.\n        If `types` is not specified, use the already existing types or auto-assign them.\n\n        When auto-assigning, each symbol is given a unique value, case-sensitive.\n        Values are assigned from lowest atomic number to highest.\n        For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_mass(self, mass: t.Optional[ArrayLike] = None, *,\n                  frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atom masses in column `'mass'`.\n        If `mass` is not specified, use the already existing masses or auto-assign them.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atomic symbols.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` replaced with the given atomic positions.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                      selection: t.Optional[AtomSelection] = None, *,\n                      frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` replaced with the given atomic velocities.\n        If `pts` is not specified, use the already existing velocities or zero.\n        \"\"\"\n        ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.affine","title":"affine property","text":"
    affine: AffineTransform3D\n

    Affine transformation. Holds transformation from 'ortho' to 'local' coordinates, including rotation away from the standard crystal orientation.

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.ortho","title":"ortho property","text":"
    ortho: LinearTransform3D\n

    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.metric","title":"metric property","text":"
    metric: LinearTransform3D\n

    Cell metric tensor

    Returns the dot product between every combination of basis vectors. :math:\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.cell_size","title":"cell_size property","text":"
    cell_size: NDArray[float64]\n

    Unit cell size.

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.cell_angle","title":"cell_angle property","text":"
    cell_angle: NDArray[float64]\n

    Unit cell angles, in radians.

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.n_cells","title":"n_cells property","text":"
    n_cells: NDArray[int_]\n

    Number of unit cells.

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.pbc","title":"pbc property","text":"
    pbc: NDArray[bool_]\n

    Flags indicating the presence of periodic boundary conditions along each axis.

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.ortho_size","title":"ortho_size property","text":"
    ortho_size: NDArray[float64]\n

    Return size of orthogonal unit cell.

    Equivalent to the diagonal of the orthogonalization matrix.

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.box_size","title":"box_size property","text":"
    box_size: NDArray[float64]\n

    Return size of the cell box.

    Equivalent to self.n_cells * self.cell_size.

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.columns","title":"columns property","text":"
    columns: List[str]\n

    Return the column names in self.

    RETURNS DESCRIPTION List[str]

    A sequence of column names

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.dtypes","title":"dtypes property","text":"
    dtypes: List[DataType]\n

    Return the datatypes in self.

    RETURNS DESCRIPTION List[DataType]

    A sequence of column DataTypes

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.schema","title":"schema property","text":"
    schema: Schema\n

    Return the schema of self.

    RETURNS DESCRIPTION Schema

    A dictionary of column names and DataTypes

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.unique","title":"unique class-attribute instance-attribute","text":"
    unique = deduplicate\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_column","title":"with_column class-attribute instance-attribute","text":"
    with_column = with_columns\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.get_cell","title":"get_cell abstractmethod","text":"
    get_cell() -> Cell\n

    Get the cell contained in self. This should be a low cost method.

    Source code in atomlib/cell.py
    @abc.abstractmethod\ndef get_cell(self) -> Cell:\n    \"\"\"Get the cell contained in ``self``. This should be a low cost method.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.get_transform","title":"get_transform","text":"
    get_transform(\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n

    In the two-argument form, get the transform to frame_to from frame_from. In the one-argument form, get the transform from local coordinates to 'frame'.

    Source code in atomlib/cell.py
    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n    \"\"\"\n    In the two-argument form, get the transform to `frame_to` from `frame_from`.\n    In the one-argument form, get the transform from local coordinates to 'frame'.\n    \"\"\"\n    transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n    transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n    if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n        return AffineTransform3D()\n    return transform_to.inverse() @ transform_from\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.corners","title":"corners","text":"
    corners(frame: CoordinateFrame = 'local') -> ndarray\n
    Source code in atomlib/cell.py
    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n    corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n    return self.get_transform(frame, 'cell_box') @ corners\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.bbox_cell","title":"bbox_cell","text":"
    bbox_cell(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the bounding box of the cell box in the given coordinate system.

    Source code in atomlib/cell.py
    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n    return BBox3D.from_pts(self.corners(frame))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.is_orthogonal","title":"is_orthogonal","text":"
    is_orthogonal(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal (axes are at right angles.)

    Source code in atomlib/cell.py
    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n    return self.ortho.is_diagonal(tol=tol)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.is_orthogonal_in_local","title":"is_orthogonal_in_local","text":"
    is_orthogonal_in_local(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal and aligned with the local coordinate system.

    Source code in atomlib/cell.py
    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n    transform = (self.affine @ self.ortho).to_linear()\n    if not transform.is_scaled_orthogonal(tol):\n        return False\n    normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n    # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n    return all(\n        any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n        for row in normed\n    )\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.to_ortho","title":"to_ortho","text":"
    to_ortho() -> AffineTransform3D\n
    Source code in atomlib/cell.py
    def to_ortho(self) -> AffineTransform3D:\n    return self.get_transform('local', 'cell_box')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.strain_orthogonal","title":"strain_orthogonal","text":"
    strain_orthogonal() -> HasCellT\n

    Orthogonalize using strain.

    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane. For small displacements, no hydrostatic strain is applied (volume is conserved).

    Source code in atomlib/cell.py
    def strain_orthogonal(self: HasCellT) -> HasCellT:\n    \"\"\"\n    Orthogonalize using strain.\n\n    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n    For small displacements, no hydrostatic strain is applied (volume is conserved).\n    \"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=LinearTransform3D(),\n        cell_size=self.cell_size,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.explode_z","title":"explode_z","text":"
    explode_z() -> HasCellT\n

    Materialize repeated cells as one supercell in z.

    Source code in atomlib/cell.py
    def explode_z(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n        n_cells=[*self.n_cells[:2], 1],\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.change_transform","title":"change_transform","text":"
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n

    Coordinate-change a transformation from frame_from into frame_to.

    Source code in atomlib/cell.py
    def change_transform(self, transform: Transform3D,\n                     frame_to: t.Optional[CoordinateFrame] = None,\n                     frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n    \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n    if frame_to == frame_from and frame_to is not None:\n        return transform\n    coord_change = self.get_transform(frame_to, frame_from)\n    return coord_change @ transform @ coord_change.inverse()\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n
    Source code in atomlib/atoms.py
    def assert_equal(self, other: t.Any):\n    assert isinstance(other, HasAtoms)\n    assert dict(self.schema) == dict(other.schema)\n    for col in self.schema.keys():\n        polars.testing.assert_series_equal(self[col], other[col], check_names=False, rtol=1e-3, atol=1e-8)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.insert_column","title":"insert_column","text":"
    insert_column(index: int, column: Series) -> DataFrame\n
    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n    return self._get_frame().insert_column(index, column)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.get_column_index","title":"get_column_index","text":"
    get_column_index(name: str) -> int\n

    Get the index of a column by name, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_column_index)\ndef get_column_index(self, name: str) -> int:\n    \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.clone","title":"clone","text":"
    clone() -> DataFrame\n

    Return a copy of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef clone(self) -> polars.DataFrame:\n    \"\"\"Return a copy of `self`.\"\"\"\n    return self._get_frame().clone()\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.drop","title":"drop","text":"
    drop(\n    *columns: Union[str, Iterable[str]], strict: bool = True\n) -> DataFrame\n

    Return self with the specified columns removed.

    Source code in atomlib/atoms.py
    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n    \"\"\"Return `self` with the specified columns removed.\"\"\"\n    return self._get_frame().drop(*columns, strict=strict)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.drop_nulls","title":"drop_nulls","text":"
    drop_nulls(\n    subset: Union[str, Collection[str], None] = None\n) -> DataFrame\n

    Drop rows that contain nulls in any of columns subset.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n    \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n    return self._get_frame().drop_nulls(subset)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.concat","title":"concat classmethod","text":"
    concat(\n    atoms: Union[\n        HasAtomsT,\n        IntoAtoms,\n        Iterable[Union[HasAtomsT, IntoAtoms]],\n    ],\n    *,\n    rechunk: bool = True,\n    how: ConcatMethod = \"vertical\"\n) -> HasAtomsT\n

    Concatenate multiple Atoms together, handling metadata appropriately.

    Source code in atomlib/atoms.py
    @classmethod\ndef concat(cls: t.Type[HasAtomsT],\n           atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n           rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n    \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n    # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n    # same type as ``cls``.\n    if _is_abstract(cls):\n        raise TypeError(\"concat() must be called on a concrete class.\")\n\n    if isinstance(atoms, HasAtoms):\n        atoms = (atoms,)\n    dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n    representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n    if len(dfs) == 0:\n        return representative.with_atoms(Atoms.empty(), 'local')\n\n    if how in ('vertical', 'vertical_relaxed'):\n        # get order from first member\n        cols = dfs[0].columns\n        dfs = [df.select(cols) for df in dfs]\n    elif how == 'inner':\n        cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n        schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n        if len(schema) == 0:\n            raise ValueError(\"Atoms have no columns in common\")\n\n        dfs = [_select_schema(df, schema) for df in dfs]\n        how = 'vertical'\n\n    return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.partition_by","title":"partition_by","text":"
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: bool = False\n) -> Union[List[Self], Dict[Any, Self]]\n

    Group by the given columns and partition into separate dataframes.

    Return the partitions as a dictionary by specifying as_dict=True.

    Source code in atomlib/atoms.py
    def partition_by(\n    self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n    maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n    \"\"\"\n    Group by the given columns and partition into separate dataframes.\n\n    Return the partitions as a dictionary by specifying `as_dict=True`.\n    \"\"\"\n    if as_dict:\n        d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n        return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n    return [\n        self.with_atoms(Atoms(df, _unchecked=True))\n        for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n    ]\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.select_schema","title":"select_schema","text":"
    select_schema(schema: SchemaDict) -> DataFrame\n

    Select columns from self and cast to the given schema. Raises TypeError if a column is not found or if it can't be cast.

    Source code in atomlib/atoms.py
    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n    \"\"\"\n    Select columns from `self` and cast to the given schema.\n    Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n    \"\"\"\n    return _select_schema(self, schema)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.try_get_column","title":"try_get_column","text":"
    try_get_column(name: str) -> Optional[Series]\n

    Try to get a column from self, returning None if it doesn't exist.

    Source code in atomlib/atoms.py
    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n    \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n    try:\n        return self.get_column(name)\n    except polars.exceptions.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.deduplicate","title":"deduplicate","text":"
    deduplicate(\n    tol: float = 0.001,\n    subset: Iterable[str] = (\"x\", \"y\", \"z\", \"symbol\"),\n    keep: UniqueKeepStrategy = \"first\",\n    maintain_order: bool = True,\n) -> Self\n

    De-duplicate atoms in self. Atoms of the same symbol that are closer than tolerance to each other (by Euclidian distance) will be removed, leaving only the atom specified by keep (defaults to the first atom).

    If subset is specified, only those columns will be included while assessing duplicates. Floating point columns other than 'x', 'y', and 'z' will not by toleranced.

    Source code in atomlib/atoms.py
    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n    \"\"\"\n    De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n    to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n    `keep` (defaults to the first atom).\n\n    If `subset` is specified, only those columns will be included while assessing duplicates.\n    Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n    \"\"\"\n    import scipy.spatial\n\n    cols = set((subset,) if isinstance(subset, str) else subset)\n\n    indices = numpy.arange(len(self))\n\n    spatial_cols = cols.intersection(('x', 'y', 'z'))\n    cols -= spatial_cols\n    if len(spatial_cols) > 0:\n        coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n        tree = scipy.spatial.KDTree(coords)\n\n        # TODO This is a bad algorithm\n        while True:\n            changed = False\n            for (i, j) in tree.query_pairs(tol, 2.):\n                # whenever we encounter a pair, ensure their index matches\n                i_i, i_j = indices[[i, j]]\n                if i_i != i_j:\n                    indices[i] = indices[j] = min(i_i, i_j)\n                    changed = True\n            if not changed:\n                break\n\n        self = self.with_column(polars.Series('_unique_pts', indices))\n        cols.add('_unique_pts')\n\n    frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n    if len(spatial_cols) > 0:\n        frame = frame.drop('_unique_pts')\n\n    return self.with_atoms(Atoms(frame, _unchecked=True))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_bounds","title":"with_bounds","text":"
    with_bounds(\n    cell_size: Optional[VecLike] = None,\n    cell_origin: Optional[VecLike] = None,\n) -> \"AtomCell\"\n

    Return a periodic cell with the given orthogonal cell dimensions.

    If cell_size is not specified, it will be assumed (and may be incorrect).

    Source code in atomlib/atoms.py
    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n    \"\"\"\n    Return a periodic cell with the given orthogonal cell dimensions.\n\n    If cell_size is not specified, it will be assumed (and may be incorrect).\n    \"\"\"\n    # TODO: test this\n    from .atomcell import AtomCell\n\n    if cell_size is None:\n        warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n        cell_size = self.bbox().size\n        cell_origin = self.bbox().min\n\n    # TODO test this origin code\n    cell = Cell.from_unit_cell(cell_size)\n    if cell_origin is not None:\n        cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n    return AtomCell(self.get_atoms(), cell, frame='local')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.x","title":"x","text":"
    x() -> Expr\n
    Source code in atomlib/atoms.py
    def x(self) -> polars.Expr:\n    return polars.col('coords').arr.get(0).alias('x')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.y","title":"y","text":"
    y() -> Expr\n
    Source code in atomlib/atoms.py
    def y(self) -> polars.Expr:\n    return polars.col('coords').arr.get(1).alias('y')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.z","title":"z","text":"
    z() -> Expr\n
    Source code in atomlib/atoms.py
    def z(self) -> polars.Expr:\n    return polars.col('coords').arr.get(2).alias('z')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.types","title":"types","text":"
    types() -> Optional[Series]\n

    Returns a Series of atom types (dtype polars.Int32).

    Source code in atomlib/atoms.py
    def types(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('type')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.masses","title":"masses","text":"
    masses() -> Optional[Series]\n

    Returns a Series of atom masses (dtype polars.Float32).

    Source code in atomlib/atoms.py
    def masses(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('mass')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.pos","title":"pos","text":"
    pos(\n    x: Union[Sequence[Optional[float]], float, None] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n

    Select all atoms at a given position.

    Formally, returns all atoms within a cube of radius tol centered at (x,y,z), exclusive of the cube's surface.

    Additional parameters given as kwargs will be checked as additional parameters (with strict equality).

    Source code in atomlib/atoms.py
    def pos(self,\n        x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n        y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n        tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n    \"\"\"\n    Select all atoms at a given position.\n\n    Formally, returns all atoms within a cube of radius ``tol``\n    centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n    Additional parameters given as ``kwargs`` will be checked\n    as additional parameters (with strict equality).\n    \"\"\"\n\n    if isinstance(x, t.Sequence):\n        (x, y, z) = x\n\n    tol = abs(float(tol))\n    selection = polars.lit(True)\n    if x is not None:\n        selection &= self.x().is_between(x - tol, x + tol, closed='none')\n    if y is not None:\n        selection &= self.y().is_between(y - tol, y + tol, closed='none')\n    if z is not None:\n        selection &= self.z().is_between(z - tol, z + tol, closed='none')\n    for (col, val) in kwargs.items():\n        selection &= (polars.col(col) == val)\n\n    return selection\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.apply_occupancy","title":"apply_occupancy","text":"
    apply_occupancy(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    For each atom in self, use its frac_occupancy to randomly decide whether to remove it.

    Source code in atomlib/atoms.py
    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n    \"\"\"\n    if 'frac_occupancy' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    frac = self.select('frac_occupancy').to_series().to_numpy()\n    choice = rng.binomial(1, frac).astype(numpy.bool_)\n    return self.filter(polars.lit(choice))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.get_frame","title":"get_frame abstractmethod","text":"
    get_frame() -> CoordinateFrame\n

    Get the coordinate frame atoms are stored in.

    Source code in atomlib/atomcell.py
    @abc.abstractmethod\ndef get_frame(self) -> CoordinateFrame:\n    \"\"\"Get the coordinate frame atoms are stored in.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_atoms","title":"with_atoms abstractmethod","text":"
    with_atoms(\n    atoms: HasAtoms, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Replace the atoms in self. If no coordinate frame is specified, keep the coordinate frame unchanged.

    Source code in atomlib/atomcell.py
    @abc.abstractmethod\ndef with_atoms(self, atoms: HasAtoms, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Replace the atoms in `self`. If no coordinate frame is specified, keep the coordinate frame unchanged.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_cell","title":"with_cell","text":"
    with_cell(cell: Cell) -> Self\n

    Replace the cell in self, without touching the atomic coordinates.

    Source code in atomlib/atomcell.py
    def with_cell(self, cell: Cell) -> Self:\n    \"\"\"\n    Replace the cell in `self`, without touching the atomic coordinates.\n    \"\"\"\n    return self.to_frame('local').with_cell(cell)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.get_atomcell","title":"get_atomcell","text":"
    get_atomcell() -> AtomCell\n
    Source code in atomlib/atomcell.py
    def get_atomcell(self) -> AtomCell:\n    frame = self.get_frame()\n    return AtomCell(self.get_atoms(frame), self.get_cell(), frame=frame, keep_frame=True)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.get_atoms","title":"get_atoms abstractmethod","text":"
    get_atoms(frame: Optional[CoordinateFrame] = None) -> Atoms\n

    Get atoms contained in self, in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @abc.abstractmethod\ndef get_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> Atoms:\n    \"\"\"Get atoms contained in `self`, in the given coordinate frame.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.bbox_atoms","title":"bbox_atoms","text":"
    bbox_atoms(\n    frame: Optional[CoordinateFrame] = None,\n) -> BBox3D\n

    Return the bounding box of all the atoms in self, in the given coordinate frame.

    Source code in atomlib/atomcell.py
    def bbox_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> BBox3D:\n    \"\"\"Return the bounding box of all the atoms in `self`, in the given coordinate frame.\"\"\"\n    return self.get_atoms(frame).bbox()\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.bbox","title":"bbox","text":"
    bbox(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the combined bounding box of the cell and atoms in the given coordinate system. To get the cell or atoms bounding box only, use bbox_cell or bbox_atoms.

    Source code in atomlib/atomcell.py
    def bbox(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"\n    Return the combined bounding box of the cell and atoms in the given coordinate system.\n    To get the cell or atoms bounding box only, use [`bbox_cell`][atomlib.atomcell.HasAtomCell.bbox_cell] or [`bbox_atoms`][atomlib.atomcell.HasAtomCell.bbox_atoms].\n    \"\"\"\n    return self.bbox_atoms(frame) | self.bbox_cell(frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.to_frame","title":"to_frame","text":"
    to_frame(frame: CoordinateFrame) -> Self\n

    Convert the stored Atoms to the given coordinate frame.

    Source code in atomlib/atomcell.py
    def to_frame(self, frame: CoordinateFrame) -> Self:\n    \"\"\"Convert the stored Atoms to the given coordinate frame.\"\"\"\n    return self.with_atoms(self.get_atoms(frame), frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.transform_atoms","title":"transform_atoms","text":"
    transform_atoms(\n    transform: IntoTransform3D,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: CoordinateFrame = \"local\",\n    transform_velocities: bool = False\n) -> Self\n

    Transform the atoms in self by transform. If selection is given, only transform the atoms in selection.

    Source code in atomlib/atomcell.py
    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                    frame: CoordinateFrame = 'local', transform_velocities: bool = False) -> Self:\n    \"\"\"\n    Transform the atoms in `self` by `transform`.\n    If `selection` is given, only transform the atoms in `selection`.\n    \"\"\"\n    transform = self.change_transform(Transform3D.make(transform), self.get_frame(), frame)\n    return self.with_atoms(self.get_atoms(self.get_frame()).transform(transform, selection, transform_velocities=transform_velocities))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.transform_cell","title":"transform_cell","text":"
    transform_cell(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> Self\n

    Apply the given transform to the unit cell, without changing atom positions. The transform is applied in coordinate frame 'frame'.

    Source code in atomlib/atomcell.py
    def transform_cell(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n    \"\"\"\n    Apply the given transform to the unit cell, without changing atom positions.\n    The transform is applied in coordinate frame 'frame'.\n    \"\"\"\n    return self.with_cell(self.get_cell().transform_cell(transform, frame=frame))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.transform","title":"transform","text":"
    transform(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> Self\n
    Source code in atomlib/atomcell.py
    def transform(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n    if isinstance(transform, Transform3D) and not isinstance(transform, AffineTransform3D):\n        raise ValueError(\"Non-affine transforms cannot change the box dimensions. Use 'transform_atoms' instead.\")\n    # TODO: cleanup once tests pass\n    # coordinate change the transform into atomic coordinates\n    new_cell = self.get_cell().transform_cell(transform, frame)\n    transform = self.get_cell().change_transform(transform, self.get_frame(), frame)\n    return self.with_atoms(self.get_atoms().transform(transform), self.get_frame()).with_cell(new_cell)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.crop","title":"crop","text":"
    crop(\n    x_min: float = -numpy.inf,\n    x_max: float = numpy.inf,\n    y_min: float = -numpy.inf,\n    y_max: float = numpy.inf,\n    z_min: float = -numpy.inf,\n    z_max: float = numpy.inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> Self\n

    Crop atoms and cell to the given extents. For a non-orthogonal cell, this must be specified in cell coordinates. This function implicity explodes the cell as well.

    To crop atoms only, use crop_atoms instead.

    Source code in atomlib/atomcell.py
    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n         frame: CoordinateFrame = 'local') -> Self:\n    \"\"\"\n    Crop atoms and cell to the given extents. For a non-orthogonal\n    cell, this must be specified in cell coordinates. This\n    function implicity `explode`s the cell as well.\n\n    To crop atoms only, use `crop_atoms` instead.\n    \"\"\"\n\n    cell = self.get_cell().crop(x_min, x_max, y_min, y_max, z_min, z_max, frame=frame)\n    atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n    return self.with_cell(cell).with_atoms(atoms)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.crop_atoms","title":"crop_atoms","text":"
    crop_atoms(\n    x_min: float = -numpy.inf,\n    x_max: float = numpy.inf,\n    y_min: float = -numpy.inf,\n    y_max: float = numpy.inf,\n    z_min: float = -numpy.inf,\n    z_max: float = numpy.inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> Self\n
    Source code in atomlib/atomcell.py
    def crop_atoms(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n               y_min: float = -numpy.inf, y_max: float = numpy.inf,\n               z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n               frame: CoordinateFrame = 'local') -> Self:\n    atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n    return self.with_atoms(atoms)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.crop_to_box","title":"crop_to_box","text":"
    crop_to_box(eps: float = 1e-05) -> Self\n
    Source code in atomlib/atomcell.py
    def crop_to_box(self, eps: float = 1e-5) -> Self:\n    atoms = self._transform_atoms_in_frame('cell_box', lambda atoms: atoms.crop_atoms(*([-eps, 1-eps]*3)))\n    return self.with_atoms(atoms)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.wrap","title":"wrap","text":"
    wrap(eps: float = 1e-05) -> Self\n

    Wrap atoms around the cell boundaries.

    Source code in atomlib/atomcell.py
    def wrap(self, eps: float = 1e-5) -> Self:\n    \"\"\"Wrap atoms around the cell boundaries.\"\"\"\n    return self.with_atoms(self._transform_atoms_in_frame('cell_box', lambda a: a._wrap(eps)))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.repeat","title":"repeat","text":"
    repeat(n: Union[int, VecLike]) -> Self\n

    Tile the cell

    Source code in atomlib/atomcell.py
    def repeat(self, n: t.Union[int, VecLike]) -> Self:\n    \"\"\"Tile the cell\"\"\"\n    ns = numpy.broadcast_to(n, 3)\n    if not numpy.issubdtype(ns.dtype, numpy.integer):\n        raise ValueError(\"repeat() argument must be an integer or integer array.\")\n\n    cells = numpy.stack(numpy.meshgrid(*map(numpy.arange, ns))) \\\n        .reshape(3, -1).T.astype(float)\n    cells = cells * self.box_size\n\n    atoms = self.get_atoms('cell')\n    atoms = Atoms.concat([\n        atoms.transform(AffineTransform3D.translate(cell))\n        for cell in cells\n    ]) #.transform(self.cell.get_transform('local', 'cell_frac'))\n    return self.with_atoms(atoms, 'cell').with_cell(self.get_cell().repeat(ns))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.repeat_to","title":"repeat_to","text":"
    repeat_to(\n    size: VecLike, crop: Union[bool, Sequence[bool]] = False\n) -> Self\n

    Repeat the cell so it is at least size along the crystal's axes.

    If crop, then crop the cell to exactly size. This may break periodicity. crop may be a vector, in which case you can specify cropping only along some axes.

    Source code in atomlib/atomcell.py
    def repeat_to(self, size: VecLike, crop: t.Union[bool, t.Sequence[bool]] = False) -> Self:\n    \"\"\"\n    Repeat the cell so it is at least `size` along the crystal's axes.\n\n    If `crop`, then crop the cell to exactly `size`. This may break periodicity.\n    `crop` may be a vector, in which case you can specify cropping only along some axes.\n    \"\"\"\n    size = to_vec3(size)\n    cell_size = self.cell_size * self.n_cells\n    repeat = numpy.maximum(numpy.ceil(size / cell_size).astype(int), 1)\n    atom_cell = self.repeat(repeat)\n\n    crop_v = to_vec3(crop, dtype=numpy.bool_)\n    if numpy.any(crop_v):\n        crop_x, crop_y, crop_z = crop_v\n        return atom_cell.crop(\n            x_max = size[0] if crop_x else numpy.inf,\n            y_max = size[1] if crop_y else numpy.inf,\n            z_max = size[2] if crop_z else numpy.inf,\n            frame='cell'\n        )\n\n    return atom_cell\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.repeat_x","title":"repeat_x","text":"
    repeat_x(n: int) -> Self\n

    Tile the cell in the x axis.

    Source code in atomlib/atomcell.py
    def repeat_x(self, n: int) -> Self:\n    \"\"\"Tile the cell in the x axis.\"\"\"\n    return self.repeat((n, 1, 1))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.repeat_y","title":"repeat_y","text":"
    repeat_y(n: int) -> Self\n

    Tile the cell in the y axis.

    Source code in atomlib/atomcell.py
    def repeat_y(self, n: int) -> Self:\n    \"\"\"Tile the cell in the y axis.\"\"\"\n    return self.repeat((1, n, 1))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.repeat_z","title":"repeat_z","text":"
    repeat_z(n: int) -> Self\n

    Tile the cell in the z axis.

    Source code in atomlib/atomcell.py
    def repeat_z(self, n: int) -> Self:\n    \"\"\"Tile the cell in the z axis.\"\"\"\n    return self.repeat((1, 1, n))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.repeat_to_x","title":"repeat_to_x","text":"
    repeat_to_x(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the x axis.

    Source code in atomlib/atomcell.py
    def repeat_to_x(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the x axis.\"\"\"\n    return self.repeat_to([size, 0., 0.], [crop, False, False])\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.repeat_to_y","title":"repeat_to_y","text":"
    repeat_to_y(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the y axis.

    Source code in atomlib/atomcell.py
    def repeat_to_y(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the y axis.\"\"\"\n    return self.repeat_to([0., size, 0.], [False, crop, False])\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.repeat_to_z","title":"repeat_to_z","text":"
    repeat_to_z(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the z axis.

    Source code in atomlib/atomcell.py
    def repeat_to_z(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the z axis.\"\"\"\n    return self.repeat_to([0., 0., size], [False, False, crop])\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.repeat_to_aspect","title":"repeat_to_aspect","text":"
    repeat_to_aspect(\n    plane: Literal[\"xy\", \"xz\", \"yz\"] = \"xy\",\n    *,\n    aspect: float = 1.0,\n    min_size: Optional[VecLike] = None,\n    max_size: Optional[VecLike] = None\n) -> Self\n

    Repeat to optimize the aspect ratio in plane, while staying above min_size and under max_size.

    Source code in atomlib/atomcell.py
    def repeat_to_aspect(self, plane: t.Literal['xy', 'xz', 'yz'] = 'xy', *,\n                     aspect: float = 1., min_size: t.Optional[VecLike] = None,\n                     max_size: t.Optional[VecLike] = None) -> Self:\n    \"\"\"\n    Repeat to optimize the aspect ratio in `plane`,\n    while staying above `min_size` and under `max_size`.\n    \"\"\"\n    if min_size is None:\n        min_n = numpy.array([1, 1, 1], numpy.int_)\n    else:\n        min_n = numpy.maximum(numpy.ceil(to_vec3(min_size) / self.box_size), 1).astype(numpy.int_)\n\n    if max_size is None:\n        max_n = 3 * min_n\n    else:\n        max_n = numpy.maximum(numpy.floor(to_vec3(max_size) / self.box_size), 1).astype(numpy.int_)\n\n    if plane == 'xy':\n        indices = [0, 1]\n    elif plane == 'xz':\n        indices = [0, 2]\n    elif plane == 'yz':\n        indices = [1, 2]\n    else:\n        raise ValueError(f\"Invalid plane '{plane}'. Exepcted 'xy', 'xz', 'or 'yz'.\")\n\n    na = numpy.arange(min_n[indices[0]], max_n[indices[0]])\n    nb = numpy.arange(min_n[indices[1]], max_n[indices[1]])\n    (na, nb) = numpy.meshgrid(na, nb)\n\n    aspects = na * self.box_size[indices[0]] / (nb * self.box_size[indices[1]])\n    # cost function: log(aspect)^2  (so cost(0.5) == cost(2))\n    min_i = numpy.argmin(numpy.log(aspects / aspect)**2)\n    repeat = numpy.array([1, 1, 1], numpy.int_)\n    repeat[indices] = na.flatten()[min_i], nb.flatten()[min_i]\n    return self.repeat(repeat)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.explode","title":"explode","text":"
    explode() -> Self\n

    Materialize repeated cells as one supercell.

    Source code in atomlib/atomcell.py
    def explode(self) -> Self:\n    \"\"\"Materialize repeated cells as one supercell.\"\"\"\n    frame = self.get_frame()\n\n    return self.with_atoms(self.get_atoms('local'), 'local') \\\n        .with_cell(self.get_cell().explode()) \\\n        .to_frame(frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.periodic_duplicate","title":"periodic_duplicate","text":"
    periodic_duplicate(eps: float = 1e-05) -> Self\n

    Add duplicate copies of atoms near periodic boundaries.

    For instance, an atom at a corner will be duplicated into 8 copies. This is mostly only useful for visualization.

    Source code in atomlib/atomcell.py
    def periodic_duplicate(self, eps: float = 1e-5) -> Self:\n    \"\"\"\n    Add duplicate copies of atoms near periodic boundaries.\n\n    For instance, an atom at a corner will be duplicated into 8 copies.\n    This is mostly only useful for visualization.\n    \"\"\"\n    frame_save = self.get_frame()\n    self = self.to_frame('cell_box').wrap(eps=eps)\n\n    for i in range(3):\n        self = self.concat((self,\n            self.filter(polars.col('coords').arr.get(i).abs() <= eps, frame='cell_box')\n                .transform_atoms(AffineTransform3D.translate([1. if i == j else 0. for j in range(3)]), frame='cell_box')\n        ))\n\n    return self.to_frame(frame_save)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.describe","title":"describe","text":"
    describe(\n    percentiles: Union[Sequence[float], float, None] = (\n        0.25,\n        0.5,\n        0.75,\n    ),\n    *,\n    interpolation: RollingInterpolationMethod = \"nearest\",\n    frame: Optional[CoordinateFrame] = None\n) -> DataFrame\n

    Return summary statistics for self. See DataFrame.describe for more information.

    PARAMETER DESCRIPTION percentiles

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), 50% (median), and 75% (third quartile).

    TYPE: Union[Sequence[float], float, None] DEFAULT: (0.25, 0.5, 0.75)

    RETURNS DESCRIPTION DataFrame

    A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n             interpolation: RollingInterpolationMethod = 'nearest',\n             frame: t.Optional[CoordinateFrame] = None) -> polars.DataFrame:\n    \"\"\"\n    Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n    Args:\n      percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                   50% (median), and 75% (third quartile).\n\n    Returns:\n      A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_columns","title":"with_columns","text":"
    with_columns(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self\n

    Return a copy of self with the given columns added.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_columns(self,\n                 *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 frame: t.Optional[CoordinateFrame] = None,\n                 **named_exprs: IntoExpr) -> Self:\n    \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.get_column","title":"get_column","text":"
    get_column(\n    name: str, *, frame: Optional[CoordinateFrame] = None\n) -> Series\n

    Get the specified column from self, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef get_column(self, name: str, *, frame: t.Optional[CoordinateFrame] = None) -> polars.Series:\n    \"\"\"\n    Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.get_columns","title":"get_columns","text":"
    get_columns(\n    *, frame: Optional[CoordinateFrame] = None\n) -> List[Series]\n

    Return all columns from self as a list of Series.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef get_columns(self, *, frame: t.Optional[CoordinateFrame] = None) -> t.List[polars.Series]:\n    \"\"\"\n    Return all columns from `self` as a list of [`Series`][polars.Series].\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.group_by","title":"group_by","text":"
    group_by(\n    *by: Union[IntoExpr, Iterable[IntoExpr]],\n    maintain_order: bool = False,\n    frame: Optional[CoordinateFrame] = None,\n    **named_by: IntoExpr\n) -> GroupBy\n

    Start a group by operation. See DataFrame.group_by for more information.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n             maintain_order: bool = False, frame: t.Optional[CoordinateFrame] = None,\n             **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n    \"\"\"\n    Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.pipe","title":"pipe","text":"
    pipe(\n    function: Callable[Concatenate[HasAtomCellT, P], T],\n    *args: args,\n    **kwargs: kwargs\n) -> T\n

    Apply function to self (in method-call syntax).

    Source code in atomlib/atomcell.py
    def pipe(self: HasAtomCellT, function: t.Callable[Concatenate[HasAtomCellT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n    \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n    return function(self, *args, **kwargs)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.filter","title":"filter","text":"
    filter(\n    *predicates: Union[\n        None,\n        IntoExprColumn,\n        Iterable[IntoExprColumn],\n        bool,\n        List[bool],\n        ndarray,\n    ],\n    frame: Optional[CoordinateFrame] = None,\n    **constraints: Any\n) -> Self\n

    Filter self, removing rows which evaluate to False.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef filter(\n    self,\n    *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n    frame: t.Optional[CoordinateFrame] = None,\n    **constraints: t.Any,\n) -> Self:\n    \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.sort","title":"sort","text":"
    sort(\n    by: Union[IntoExpr, Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: Union[bool, Sequence[bool]] = False,\n    nulls_last: bool = False\n) -> Self\n

    Sort the atoms in self by the given columns/expressions.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef sort(\n    self,\n    by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: t.Union[bool, t.Sequence[bool]] = False,\n    nulls_last: bool = False,\n) -> Self:\n    \"\"\"\n    Sort the atoms in `self` by the given columns/expressions.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.slice","title":"slice","text":"
    slice(\n    offset: int,\n    length: Optional[int] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return a slice of the rows in self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef slice(self, offset: int, length: t.Optional[int] = None, *,\n          frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return a slice of the rows in `self`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.head","title":"head","text":"
    head(\n    n: int = 5, *, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return the first n rows of self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef head(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return the first `n` rows of `self`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.tail","title":"tail","text":"
    tail(\n    n: int = 5, *, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return the last n rows of self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef tail(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return the last `n` rows of `self`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.fill_null","title":"fill_null","text":"
    fill_null(\n    value: Any = None,\n    strategy: Optional[FillNullStrategy] = None,\n    limit: Optional[int] = None,\n    matches_supertype: bool = True,\n) -> Self\n
    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef fill_null(\n    self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n    limit: t.Optional[int] = None, matches_supertype: bool = True,\n) -> Self:\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.fill_nan","title":"fill_nan","text":"
    fill_nan(\n    value: Union[Expr, int, float, None],\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n
    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef fill_nan(self, value: t.Union[polars.Expr, int, float, None], *,\n             frame: t.Optional[CoordinateFrame] = None) -> Self:\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.select","title":"select","text":"
    select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef select(\n    self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> polars.DataFrame:\n    \"\"\"\n    Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.select_props","title":"select_props","text":"
    select_props(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self\n

    Select exprs from self, while keeping required columns. Doesn't affect the cell.

    RETURNS DESCRIPTION Self

    A HasAtomCell filtered to contain

    Self

    the specified properties (as well as required columns).

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef select_props(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self:\n    \"\"\"\n    Select `exprs` from `self`, while keeping required columns.\n    Doesn't affect the cell.\n\n    Returns:\n      A [`HasAtomCell`][atomlib.atomcell.HasAtomCell] filtered to contain\n      the specified properties (as well as required columns).\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.try_select","title":"try_select","text":"
    try_select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Optional[DataFrame]\n

    Try to select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns. Returns None if any columns are missing.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef try_select(\n    self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> t.Optional[polars.DataFrame]:\n    \"\"\"\n    Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns. Returns `None` if any\n    columns are missing.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.round_near_zero","title":"round_near_zero","text":"
    round_near_zero(\n    tol: float = 1e-14,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Round atom position values near zero to zero.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef round_near_zero(self, tol: float = 1e-14, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Round atom position values near zero to zero.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.coords","title":"coords","text":"
    coords(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> NDArray[float64]\n

    Return a (N, 3) ndarray of atom positions (dtype numpy.float64) in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> NDArray[numpy.float64]:\n    \"\"\"\n    Return a `(N, 3)` ndarray of atom positions (dtype [`numpy.float64`][numpy.float64])\n    in the given coordinate frame.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.velocities","title":"velocities","text":"
    velocities(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Optional[NDArray[float64]]\n

    Return a (N, 3) ndarray of atom velocities (dtype numpy.float64) in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef velocities(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> t.Optional[NDArray[numpy.float64]]:\n    \"\"\"\n    Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64])\n    in the given coordinate frame.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.add_atom","title":"add_atom","text":"
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: Union[ArrayLike, float],\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None,\n    **kwargs: Any,\n) -> Self\n

    Return a copy of self with an extra atom.

    By default, all extra columns present in self must be specified as **kwargs.

    Try to avoid calling this in a loop (Use concat instead).

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef add_atom(self, elem: t.Union[int, str], /,  # type: ignore (spurious)\n             x: t.Union[ArrayLike, float],\n             y: t.Optional[float] = None,\n             z: t.Optional[float] = None, *,\n             frame: t.Optional[CoordinateFrame] = None,\n             **kwargs: t.Any) -> Self:\n    \"\"\"\n    Return a copy of `self` with an extra atom.\n\n    By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n    Try to avoid calling this in a loop (Use [`concat`][atomlib.atomcell.HasAtomCell.concat] instead).\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_index","title":"with_index","text":"
    with_index(\n    index: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Returns self with a row index added in column 'i' (dtype polars.Int64). If index is not specified, defaults to an existing index or a new index.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_index(self, index: t.Optional[AtomValues] = None, *,\n               frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n    If `index` is not specified, defaults to an existing index or a new index.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_wobble","title":"with_wobble","text":"
    with_wobble(\n    wobble: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given displacements in column 'wobble' (dtype polars.Float64). If wobble is not specified, defaults to the already-existing wobbles or 0.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_wobble(self, wobble: t.Optional[AtomValues] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_occupancy","title":"with_occupancy","text":"
    with_occupancy(\n    frac_occupancy: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given fractional occupancies (dtype polars.Float64). If frac_occupancy is not specified, defaults to the already-existing occupancies or 1.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None, *,\n                   frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.apply_wobble","title":"apply_wobble","text":"
    apply_wobble(\n    rng: Union[Generator, int, None] = None,\n    frame: Optional[CoordinateFrame] = None,\n) -> Self\n

    Displace the atoms in self by the amount in the wobble column. wobble is interpretated as a mean-squared displacement, which is distributed equally over each axis.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None,\n                 frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Displace the atoms in `self` by the amount in the `wobble` column.\n    `wobble` is interpretated as a mean-squared displacement, which is distributed\n    equally over each axis.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_type","title":"with_type","text":"
    with_type(\n    types: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atom types in column 'type'. If types is not specified, use the already existing types or auto-assign them.

    When auto-assigning, each symbol is given a unique value, case-sensitive. Values are assigned from lowest atomic number to highest. For instance: [\"Ag+\", \"Na\", \"H\", \"Ag\"] => [3, 11, 1, 2]

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_type(self, types: t.Optional[AtomValues] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom types in column 'type'.\n    If `types` is not specified, use the already existing types or auto-assign them.\n\n    When auto-assigning, each symbol is given a unique value, case-sensitive.\n    Values are assigned from lowest atomic number to highest.\n    For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_mass","title":"with_mass","text":"
    with_mass(\n    mass: Optional[ArrayLike] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atom masses in column 'mass'. If mass is not specified, use the already existing masses or auto-assign them.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_mass(self, mass: t.Optional[ArrayLike] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom masses in column `'mass'`.\n    If `mass` is not specified, use the already existing masses or auto-assign them.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_symbol","title":"with_symbol","text":"
    with_symbol(\n    symbols: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atomic symbols.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atomic symbols.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_coords","title":"with_coords","text":"
    with_coords(\n    pts: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self replaced with the given atomic positions.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic positions.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_velocity","title":"with_velocity","text":"
    with_velocity(\n    pts: Optional[ArrayLike] = None,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self replaced with the given atomic velocities. If pts is not specified, use the already existing velocities or zero.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                  selection: t.Optional[AtomSelection] = None, *,\n                  frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic velocities.\n    If `pts` is not specified, use the already existing velocities or zero.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell","title":"AtomCell dataclass","text":"

    Bases: AtomCellIOMixin, HasAtomCell

    Cell of atoms with known size and periodic boundary conditions.

    Source code in atomlib/atomcell.py
    @dataclass(init=False, repr=False, frozen=True)\nclass AtomCell(AtomCellIOMixin, HasAtomCell):\n    \"\"\"\n    Cell of atoms with known size and periodic boundary conditions.\n    \"\"\"\n\n    atoms: Atoms\n    \"\"\"Atoms in the cell. Stored in 'local' coordinates (i.e. relative to the enclosing group but not relative to box dimensions).\"\"\"\n\n    cell: Cell\n    \"\"\"Cell coordinate system.\"\"\"\n\n    frame: CoordinateFrame = 'local'\n    \"\"\"Coordinate frame 'atoms' are stored in.\"\"\"\n\n    def get_cell(self) -> Cell:\n        return self.cell\n\n    def with_cell(self, cell: Cell) -> Self:\n        return self.__class__(self.atoms, cell, frame=self.frame, keep_frame=True)\n\n    def get_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> Atoms:\n        \"\"\"Get atoms contained in ``self``, in the given coordinate frame.\"\"\"\n\n        if frame is None or frame == self.get_frame():\n            return self.atoms\n        return self.atoms.transform(self.get_transform(frame, self.get_frame()))\n\n    def with_atoms(self, atoms: HasAtoms, frame: t.Optional[CoordinateFrame] = None) -> Self:\n        frame = frame if frame is not None else self.frame\n        return self.__class__(atoms.get_atoms(), cell=self.cell, frame=frame, keep_frame=True)\n        #return replace(self, atoms=atoms, frame = frame if frame is not None else self.frame, keep_frame=True)\n\n    def get_frame(self) -> CoordinateFrame:\n        \"\"\"Get the coordinate frame atoms are stored in.\"\"\"\n        return self.frame\n\n    @classmethod\n    def _combine_metadata(cls: t.Type[AtomCellT], *atoms: HasAtoms, n: t.Optional[int] = None) -> AtomCellT:\n        \"\"\"\n        When combining multiple [`HasAtoms`][atomlib.atoms.HasAtoms], check that they are compatible with each other,\n        and return a 'representative' which best represents the combined metadata.\n        Implementors should treat [`Atoms`][atomlib.atoms.Atoms] as acceptable, but having no metadata.\n        \"\"\"\n        if n is not None:\n            rep = atoms[n]\n            if not isinstance(rep, AtomCell):\n                raise ValueError(f\"Atoms #{n} has no cell\")\n        else:\n            atom_cells = [a for a in atoms if isinstance(a, AtomCell)]\n            if len(atom_cells) == 0:\n                raise TypeError(\"No AtomCells to combine\")\n            rep = atom_cells[0]\n            if not all(a.cell == rep.cell for a in atom_cells[1:]):\n                raise TypeError(\"Can't combine AtomCells with different cells\")\n\n        return cls(Atoms.empty(), frame=rep.frame, cell=rep.cell)\n\n    @classmethod\n    def from_ortho(cls, atoms: IntoAtoms, ortho: LinearTransform3D, *,\n                   n_cells: t.Optional[VecLike] = None,\n                   frame: CoordinateFrame = 'local',\n                   keep_frame: bool = False):\n        \"\"\"\n        Make an atom cell given a list of atoms and an orthogonalization matrix.\n        Atoms are assumed to be in the coordinate system `frame`.\n        \"\"\"\n        cell = Cell.from_ortho(ortho, n_cells)\n        return cls(atoms, cell, frame=frame, keep_frame=keep_frame)\n\n    @classmethod\n    def from_unit_cell(cls, atoms: IntoAtoms, cell_size: VecLike,\n                       cell_angle: t.Optional[VecLike] = None, *,\n                       n_cells: t.Optional[VecLike] = None,\n                       frame: CoordinateFrame = 'local',\n                       keep_frame: bool = False):\n        \"\"\"\n        Make a cell given a list of atoms and unit cell parameters.\n        Atoms are assumed to be in the coordinate system `frame`.\n        \"\"\"\n        cell = Cell.from_unit_cell(cell_size, cell_angle, n_cells=n_cells)\n        return cls(atoms, cell, frame=frame, keep_frame=keep_frame)\n\n    def __init__(self, atoms: IntoAtoms, cell: Cell, *,\n                 frame: CoordinateFrame = 'local',\n                 keep_frame: bool = False):\n        atoms = Atoms(atoms)\n        # by default, store in local coordinates\n        if not keep_frame and frame != 'local':\n            atoms = atoms.transform(cell.get_transform('local', frame))\n            frame = 'local'\n\n        object.__setattr__(self, 'atoms', atoms)\n        object.__setattr__(self, 'cell', cell)\n        object.__setattr__(self, 'frame', frame)\n\n        self.__post_init__()\n\n    def __post_init__(self):\n        pass\n\n    def orthogonalize(self) -> OrthoCell:\n        if self.is_orthogonal():\n            return OrthoCell(self.atoms, self.cell, frame=self.frame)\n        raise NotImplementedError()\n\n    def clone(self: AtomCellT) -> AtomCellT:\n        \"\"\"Make a deep copy of `self`.\"\"\"\n        return self.__class__(**{field.name: copy.deepcopy(getattr(self, field.name)) for field in fields(self)})\n\n    def assert_equal(self, other: t.Any):\n        \"\"\"Assert this structure is equal to `other`\"\"\"\n        assert isinstance(other, AtomCell)\n        self.cell.assert_equal(other.cell)\n        self.get_atoms('local').assert_equal(other.get_atoms('local'))\n\n    def _str_parts(self) -> t.Iterable[t.Any]:\n        return (\n            f\"Cell size:  {self.cell.cell_size!s}\",\n            f\"Cell angle: {self.cell.cell_angle!s}\",\n            f\"# Cells: {self.cell.n_cells!s}\",\n            f\"Frame: {self.frame}\",\n            self.atoms,\n        )\n\n    def __str__(self) -> str:\n        return \"\\n\".join(map(str, self._str_parts()))\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}({self.atoms!r}, cell={self.cell!r}, frame={self.frame})\"\n\n    def _repr_pretty_(self, p: t.Any, cycle: bool) -> None:\n        p.text(f'{self.__class__.__name__}(...)') if cycle else p.text(str(self))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.affine","title":"affine property","text":"
    affine: AffineTransform3D\n

    Affine transformation. Holds transformation from 'ortho' to 'local' coordinates, including rotation away from the standard crystal orientation.

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.ortho","title":"ortho property","text":"
    ortho: LinearTransform3D\n

    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.metric","title":"metric property","text":"
    metric: LinearTransform3D\n

    Cell metric tensor

    Returns the dot product between every combination of basis vectors. :math:\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.cell_size","title":"cell_size property","text":"
    cell_size: NDArray[float64]\n

    Unit cell size.

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.cell_angle","title":"cell_angle property","text":"
    cell_angle: NDArray[float64]\n

    Unit cell angles, in radians.

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.n_cells","title":"n_cells property","text":"
    n_cells: NDArray[int_]\n

    Number of unit cells.

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.pbc","title":"pbc property","text":"
    pbc: NDArray[bool_]\n

    Flags indicating the presence of periodic boundary conditions along each axis.

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.ortho_size","title":"ortho_size property","text":"
    ortho_size: NDArray[float64]\n

    Return size of orthogonal unit cell.

    Equivalent to the diagonal of the orthogonalization matrix.

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.box_size","title":"box_size property","text":"
    box_size: NDArray[float64]\n

    Return size of the cell box.

    Equivalent to self.n_cells * self.cell_size.

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.columns","title":"columns property","text":"
    columns: List[str]\n

    Return the column names in self.

    RETURNS DESCRIPTION List[str]

    A sequence of column names

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.dtypes","title":"dtypes property","text":"
    dtypes: List[DataType]\n

    Return the datatypes in self.

    RETURNS DESCRIPTION List[DataType]

    A sequence of column DataTypes

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.schema","title":"schema property","text":"
    schema: Schema\n

    Return the schema of self.

    RETURNS DESCRIPTION Schema

    A dictionary of column names and DataTypes

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_column","title":"with_column class-attribute instance-attribute","text":"
    with_column = with_columns\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.unique","title":"unique class-attribute instance-attribute","text":"
    unique = deduplicate\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.atoms","title":"atoms instance-attribute","text":"
    atoms: Atoms\n

    Atoms in the cell. Stored in 'local' coordinates (i.e. relative to the enclosing group but not relative to box dimensions).

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.cell","title":"cell instance-attribute","text":"
    cell: Cell\n

    Cell coordinate system.

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.frame","title":"frame class-attribute instance-attribute","text":"
    frame: CoordinateFrame = 'local'\n

    Coordinate frame 'atoms' are stored in.

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.get_transform","title":"get_transform","text":"
    get_transform(\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n

    In the two-argument form, get the transform to frame_to from frame_from. In the one-argument form, get the transform from local coordinates to 'frame'.

    Source code in atomlib/cell.py
    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n    \"\"\"\n    In the two-argument form, get the transform to `frame_to` from `frame_from`.\n    In the one-argument form, get the transform from local coordinates to 'frame'.\n    \"\"\"\n    transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n    transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n    if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n        return AffineTransform3D()\n    return transform_to.inverse() @ transform_from\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.corners","title":"corners","text":"
    corners(frame: CoordinateFrame = 'local') -> ndarray\n
    Source code in atomlib/cell.py
    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n    corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n    return self.get_transform(frame, 'cell_box') @ corners\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.bbox_cell","title":"bbox_cell","text":"
    bbox_cell(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the bounding box of the cell box in the given coordinate system.

    Source code in atomlib/cell.py
    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n    return BBox3D.from_pts(self.corners(frame))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.bbox","title":"bbox","text":"
    bbox(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the combined bounding box of the cell and atoms in the given coordinate system. To get the cell or atoms bounding box only, use bbox_cell or bbox_atoms.

    Source code in atomlib/atomcell.py
    def bbox(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"\n    Return the combined bounding box of the cell and atoms in the given coordinate system.\n    To get the cell or atoms bounding box only, use [`bbox_cell`][atomlib.atomcell.HasAtomCell.bbox_cell] or [`bbox_atoms`][atomlib.atomcell.HasAtomCell.bbox_atoms].\n    \"\"\"\n    return self.bbox_atoms(frame) | self.bbox_cell(frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.is_orthogonal","title":"is_orthogonal","text":"
    is_orthogonal(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal (axes are at right angles.)

    Source code in atomlib/cell.py
    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n    return self.ortho.is_diagonal(tol=tol)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.is_orthogonal_in_local","title":"is_orthogonal_in_local","text":"
    is_orthogonal_in_local(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal and aligned with the local coordinate system.

    Source code in atomlib/cell.py
    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n    transform = (self.affine @ self.ortho).to_linear()\n    if not transform.is_scaled_orthogonal(tol):\n        return False\n    normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n    # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n    return all(\n        any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n        for row in normed\n    )\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.to_ortho","title":"to_ortho","text":"
    to_ortho() -> AffineTransform3D\n
    Source code in atomlib/cell.py
    def to_ortho(self) -> AffineTransform3D:\n    return self.get_transform('local', 'cell_box')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.transform_cell","title":"transform_cell","text":"
    transform_cell(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> Self\n

    Apply the given transform to the unit cell, without changing atom positions. The transform is applied in coordinate frame 'frame'.

    Source code in atomlib/atomcell.py
    def transform_cell(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n    \"\"\"\n    Apply the given transform to the unit cell, without changing atom positions.\n    The transform is applied in coordinate frame 'frame'.\n    \"\"\"\n    return self.with_cell(self.get_cell().transform_cell(transform, frame=frame))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.strain_orthogonal","title":"strain_orthogonal","text":"
    strain_orthogonal() -> HasCellT\n

    Orthogonalize using strain.

    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane. For small displacements, no hydrostatic strain is applied (volume is conserved).

    Source code in atomlib/cell.py
    def strain_orthogonal(self: HasCellT) -> HasCellT:\n    \"\"\"\n    Orthogonalize using strain.\n\n    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n    For small displacements, no hydrostatic strain is applied (volume is conserved).\n    \"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=LinearTransform3D(),\n        cell_size=self.cell_size,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.repeat","title":"repeat","text":"
    repeat(n: Union[int, VecLike]) -> Self\n

    Tile the cell

    Source code in atomlib/atomcell.py
    def repeat(self, n: t.Union[int, VecLike]) -> Self:\n    \"\"\"Tile the cell\"\"\"\n    ns = numpy.broadcast_to(n, 3)\n    if not numpy.issubdtype(ns.dtype, numpy.integer):\n        raise ValueError(\"repeat() argument must be an integer or integer array.\")\n\n    cells = numpy.stack(numpy.meshgrid(*map(numpy.arange, ns))) \\\n        .reshape(3, -1).T.astype(float)\n    cells = cells * self.box_size\n\n    atoms = self.get_atoms('cell')\n    atoms = Atoms.concat([\n        atoms.transform(AffineTransform3D.translate(cell))\n        for cell in cells\n    ]) #.transform(self.cell.get_transform('local', 'cell_frac'))\n    return self.with_atoms(atoms, 'cell').with_cell(self.get_cell().repeat(ns))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.explode","title":"explode","text":"
    explode() -> Self\n

    Materialize repeated cells as one supercell.

    Source code in atomlib/atomcell.py
    def explode(self) -> Self:\n    \"\"\"Materialize repeated cells as one supercell.\"\"\"\n    frame = self.get_frame()\n\n    return self.with_atoms(self.get_atoms('local'), 'local') \\\n        .with_cell(self.get_cell().explode()) \\\n        .to_frame(frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.explode_z","title":"explode_z","text":"
    explode_z() -> HasCellT\n

    Materialize repeated cells as one supercell in z.

    Source code in atomlib/cell.py
    def explode_z(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n        n_cells=[*self.n_cells[:2], 1],\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.crop","title":"crop","text":"
    crop(\n    x_min: float = -numpy.inf,\n    x_max: float = numpy.inf,\n    y_min: float = -numpy.inf,\n    y_max: float = numpy.inf,\n    z_min: float = -numpy.inf,\n    z_max: float = numpy.inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> Self\n

    Crop atoms and cell to the given extents. For a non-orthogonal cell, this must be specified in cell coordinates. This function implicity explodes the cell as well.

    To crop atoms only, use crop_atoms instead.

    Source code in atomlib/atomcell.py
    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n         frame: CoordinateFrame = 'local') -> Self:\n    \"\"\"\n    Crop atoms and cell to the given extents. For a non-orthogonal\n    cell, this must be specified in cell coordinates. This\n    function implicity `explode`s the cell as well.\n\n    To crop atoms only, use `crop_atoms` instead.\n    \"\"\"\n\n    cell = self.get_cell().crop(x_min, x_max, y_min, y_max, z_min, z_max, frame=frame)\n    atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n    return self.with_cell(cell).with_atoms(atoms)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.change_transform","title":"change_transform","text":"
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n

    Coordinate-change a transformation from frame_from into frame_to.

    Source code in atomlib/cell.py
    def change_transform(self, transform: Transform3D,\n                     frame_to: t.Optional[CoordinateFrame] = None,\n                     frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n    \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n    if frame_to == frame_from and frame_to is not None:\n        return transform\n    coord_change = self.get_transform(frame_to, frame_from)\n    return coord_change @ transform @ coord_change.inverse()\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.describe","title":"describe","text":"
    describe(\n    percentiles: Union[Sequence[float], float, None] = (\n        0.25,\n        0.5,\n        0.75,\n    ),\n    *,\n    interpolation: RollingInterpolationMethod = \"nearest\",\n    frame: Optional[CoordinateFrame] = None\n) -> DataFrame\n

    Return summary statistics for self. See DataFrame.describe for more information.

    PARAMETER DESCRIPTION percentiles

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), 50% (median), and 75% (third quartile).

    TYPE: Union[Sequence[float], float, None] DEFAULT: (0.25, 0.5, 0.75)

    RETURNS DESCRIPTION DataFrame

    A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n             interpolation: RollingInterpolationMethod = 'nearest',\n             frame: t.Optional[CoordinateFrame] = None) -> polars.DataFrame:\n    \"\"\"\n    Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n    Args:\n      percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                   50% (median), and 75% (third quartile).\n\n    Returns:\n      A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_columns","title":"with_columns","text":"
    with_columns(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self\n

    Return a copy of self with the given columns added.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_columns(self,\n                 *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 frame: t.Optional[CoordinateFrame] = None,\n                 **named_exprs: IntoExpr) -> Self:\n    \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.insert_column","title":"insert_column","text":"
    insert_column(index: int, column: Series) -> DataFrame\n
    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n    return self._get_frame().insert_column(index, column)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.get_column","title":"get_column","text":"
    get_column(\n    name: str, *, frame: Optional[CoordinateFrame] = None\n) -> Series\n

    Get the specified column from self, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef get_column(self, name: str, *, frame: t.Optional[CoordinateFrame] = None) -> polars.Series:\n    \"\"\"\n    Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.get_columns","title":"get_columns","text":"
    get_columns(\n    *, frame: Optional[CoordinateFrame] = None\n) -> List[Series]\n

    Return all columns from self as a list of Series.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef get_columns(self, *, frame: t.Optional[CoordinateFrame] = None) -> t.List[polars.Series]:\n    \"\"\"\n    Return all columns from `self` as a list of [`Series`][polars.Series].\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.get_column_index","title":"get_column_index","text":"
    get_column_index(name: str) -> int\n

    Get the index of a column by name, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_column_index)\ndef get_column_index(self, name: str) -> int:\n    \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.group_by","title":"group_by","text":"
    group_by(\n    *by: Union[IntoExpr, Iterable[IntoExpr]],\n    maintain_order: bool = False,\n    frame: Optional[CoordinateFrame] = None,\n    **named_by: IntoExpr\n) -> GroupBy\n

    Start a group by operation. See DataFrame.group_by for more information.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n             maintain_order: bool = False, frame: t.Optional[CoordinateFrame] = None,\n             **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n    \"\"\"\n    Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.pipe","title":"pipe","text":"
    pipe(\n    function: Callable[Concatenate[HasAtomCellT, P], T],\n    *args: args,\n    **kwargs: kwargs\n) -> T\n

    Apply function to self (in method-call syntax).

    Source code in atomlib/atomcell.py
    def pipe(self: HasAtomCellT, function: t.Callable[Concatenate[HasAtomCellT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n    \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n    return function(self, *args, **kwargs)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.drop","title":"drop","text":"
    drop(\n    *columns: Union[str, Iterable[str]], strict: bool = True\n) -> DataFrame\n

    Return self with the specified columns removed.

    Source code in atomlib/atoms.py
    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n    \"\"\"Return `self` with the specified columns removed.\"\"\"\n    return self._get_frame().drop(*columns, strict=strict)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.filter","title":"filter","text":"
    filter(\n    *predicates: Union[\n        None,\n        IntoExprColumn,\n        Iterable[IntoExprColumn],\n        bool,\n        List[bool],\n        ndarray,\n    ],\n    frame: Optional[CoordinateFrame] = None,\n    **constraints: Any\n) -> Self\n

    Filter self, removing rows which evaluate to False.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef filter(\n    self,\n    *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n    frame: t.Optional[CoordinateFrame] = None,\n    **constraints: t.Any,\n) -> Self:\n    \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.sort","title":"sort","text":"
    sort(\n    by: Union[IntoExpr, Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: Union[bool, Sequence[bool]] = False,\n    nulls_last: bool = False\n) -> Self\n

    Sort the atoms in self by the given columns/expressions.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef sort(\n    self,\n    by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: t.Union[bool, t.Sequence[bool]] = False,\n    nulls_last: bool = False,\n) -> Self:\n    \"\"\"\n    Sort the atoms in `self` by the given columns/expressions.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.slice","title":"slice","text":"
    slice(\n    offset: int,\n    length: Optional[int] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return a slice of the rows in self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef slice(self, offset: int, length: t.Optional[int] = None, *,\n          frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return a slice of the rows in `self`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.head","title":"head","text":"
    head(\n    n: int = 5, *, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return the first n rows of self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef head(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return the first `n` rows of `self`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.tail","title":"tail","text":"
    tail(\n    n: int = 5, *, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return the last n rows of self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef tail(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return the last `n` rows of `self`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.drop_nulls","title":"drop_nulls","text":"
    drop_nulls(\n    subset: Union[str, Collection[str], None] = None\n) -> DataFrame\n

    Drop rows that contain nulls in any of columns subset.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n    \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n    return self._get_frame().drop_nulls(subset)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.fill_null","title":"fill_null","text":"
    fill_null(\n    value: Any = None,\n    strategy: Optional[FillNullStrategy] = None,\n    limit: Optional[int] = None,\n    matches_supertype: bool = True,\n) -> Self\n
    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef fill_null(\n    self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n    limit: t.Optional[int] = None, matches_supertype: bool = True,\n) -> Self:\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.fill_nan","title":"fill_nan","text":"
    fill_nan(\n    value: Union[Expr, int, float, None],\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n
    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef fill_nan(self, value: t.Union[polars.Expr, int, float, None], *,\n             frame: t.Optional[CoordinateFrame] = None) -> Self:\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.concat","title":"concat classmethod","text":"
    concat(\n    atoms: Union[\n        HasAtomsT,\n        IntoAtoms,\n        Iterable[Union[HasAtomsT, IntoAtoms]],\n    ],\n    *,\n    rechunk: bool = True,\n    how: ConcatMethod = \"vertical\"\n) -> HasAtomsT\n

    Concatenate multiple Atoms together, handling metadata appropriately.

    Source code in atomlib/atoms.py
    @classmethod\ndef concat(cls: t.Type[HasAtomsT],\n           atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n           rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n    \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n    # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n    # same type as ``cls``.\n    if _is_abstract(cls):\n        raise TypeError(\"concat() must be called on a concrete class.\")\n\n    if isinstance(atoms, HasAtoms):\n        atoms = (atoms,)\n    dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n    representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n    if len(dfs) == 0:\n        return representative.with_atoms(Atoms.empty(), 'local')\n\n    if how in ('vertical', 'vertical_relaxed'):\n        # get order from first member\n        cols = dfs[0].columns\n        dfs = [df.select(cols) for df in dfs]\n    elif how == 'inner':\n        cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n        schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n        if len(schema) == 0:\n            raise ValueError(\"Atoms have no columns in common\")\n\n        dfs = [_select_schema(df, schema) for df in dfs]\n        how = 'vertical'\n\n    return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.partition_by","title":"partition_by","text":"
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: bool = False\n) -> Union[List[Self], Dict[Any, Self]]\n

    Group by the given columns and partition into separate dataframes.

    Return the partitions as a dictionary by specifying as_dict=True.

    Source code in atomlib/atoms.py
    def partition_by(\n    self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n    maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n    \"\"\"\n    Group by the given columns and partition into separate dataframes.\n\n    Return the partitions as a dictionary by specifying `as_dict=True`.\n    \"\"\"\n    if as_dict:\n        d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n        return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n    return [\n        self.with_atoms(Atoms(df, _unchecked=True))\n        for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n    ]\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.select","title":"select","text":"
    select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef select(\n    self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> polars.DataFrame:\n    \"\"\"\n    Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.select_schema","title":"select_schema","text":"
    select_schema(schema: SchemaDict) -> DataFrame\n

    Select columns from self and cast to the given schema. Raises TypeError if a column is not found or if it can't be cast.

    Source code in atomlib/atoms.py
    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n    \"\"\"\n    Select columns from `self` and cast to the given schema.\n    Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n    \"\"\"\n    return _select_schema(self, schema)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.select_props","title":"select_props","text":"
    select_props(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self\n

    Select exprs from self, while keeping required columns. Doesn't affect the cell.

    RETURNS DESCRIPTION Self

    A HasAtomCell filtered to contain

    Self

    the specified properties (as well as required columns).

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef select_props(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self:\n    \"\"\"\n    Select `exprs` from `self`, while keeping required columns.\n    Doesn't affect the cell.\n\n    Returns:\n      A [`HasAtomCell`][atomlib.atomcell.HasAtomCell] filtered to contain\n      the specified properties (as well as required columns).\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.try_select","title":"try_select","text":"
    try_select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Optional[DataFrame]\n

    Try to select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns. Returns None if any columns are missing.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef try_select(\n    self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> t.Optional[polars.DataFrame]:\n    \"\"\"\n    Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns. Returns `None` if any\n    columns are missing.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.try_get_column","title":"try_get_column","text":"
    try_get_column(name: str) -> Optional[Series]\n

    Try to get a column from self, returning None if it doesn't exist.

    Source code in atomlib/atoms.py
    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n    \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n    try:\n        return self.get_column(name)\n    except polars.exceptions.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.bbox_atoms","title":"bbox_atoms","text":"
    bbox_atoms(\n    frame: Optional[CoordinateFrame] = None,\n) -> BBox3D\n

    Return the bounding box of all the atoms in self, in the given coordinate frame.

    Source code in atomlib/atomcell.py
    def bbox_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> BBox3D:\n    \"\"\"Return the bounding box of all the atoms in `self`, in the given coordinate frame.\"\"\"\n    return self.get_atoms(frame).bbox()\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.transform_atoms","title":"transform_atoms","text":"
    transform_atoms(\n    transform: IntoTransform3D,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: CoordinateFrame = \"local\",\n    transform_velocities: bool = False\n) -> Self\n

    Transform the atoms in self by transform. If selection is given, only transform the atoms in selection.

    Source code in atomlib/atomcell.py
    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                    frame: CoordinateFrame = 'local', transform_velocities: bool = False) -> Self:\n    \"\"\"\n    Transform the atoms in `self` by `transform`.\n    If `selection` is given, only transform the atoms in `selection`.\n    \"\"\"\n    transform = self.change_transform(Transform3D.make(transform), self.get_frame(), frame)\n    return self.with_atoms(self.get_atoms(self.get_frame()).transform(transform, selection, transform_velocities=transform_velocities))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.transform","title":"transform","text":"
    transform(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> Self\n
    Source code in atomlib/atomcell.py
    def transform(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n    if isinstance(transform, Transform3D) and not isinstance(transform, AffineTransform3D):\n        raise ValueError(\"Non-affine transforms cannot change the box dimensions. Use 'transform_atoms' instead.\")\n    # TODO: cleanup once tests pass\n    # coordinate change the transform into atomic coordinates\n    new_cell = self.get_cell().transform_cell(transform, frame)\n    transform = self.get_cell().change_transform(transform, self.get_frame(), frame)\n    return self.with_atoms(self.get_atoms().transform(transform), self.get_frame()).with_cell(new_cell)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.round_near_zero","title":"round_near_zero","text":"
    round_near_zero(\n    tol: float = 1e-14,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Round atom position values near zero to zero.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef round_near_zero(self, tol: float = 1e-14, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Round atom position values near zero to zero.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.crop_atoms","title":"crop_atoms","text":"
    crop_atoms(\n    x_min: float = -numpy.inf,\n    x_max: float = numpy.inf,\n    y_min: float = -numpy.inf,\n    y_max: float = numpy.inf,\n    z_min: float = -numpy.inf,\n    z_max: float = numpy.inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> Self\n
    Source code in atomlib/atomcell.py
    def crop_atoms(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n               y_min: float = -numpy.inf, y_max: float = numpy.inf,\n               z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n               frame: CoordinateFrame = 'local') -> Self:\n    atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n    return self.with_atoms(atoms)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.deduplicate","title":"deduplicate","text":"
    deduplicate(\n    tol: float = 0.001,\n    subset: Iterable[str] = (\"x\", \"y\", \"z\", \"symbol\"),\n    keep: UniqueKeepStrategy = \"first\",\n    maintain_order: bool = True,\n) -> Self\n

    De-duplicate atoms in self. Atoms of the same symbol that are closer than tolerance to each other (by Euclidian distance) will be removed, leaving only the atom specified by keep (defaults to the first atom).

    If subset is specified, only those columns will be included while assessing duplicates. Floating point columns other than 'x', 'y', and 'z' will not by toleranced.

    Source code in atomlib/atoms.py
    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n    \"\"\"\n    De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n    to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n    `keep` (defaults to the first atom).\n\n    If `subset` is specified, only those columns will be included while assessing duplicates.\n    Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n    \"\"\"\n    import scipy.spatial\n\n    cols = set((subset,) if isinstance(subset, str) else subset)\n\n    indices = numpy.arange(len(self))\n\n    spatial_cols = cols.intersection(('x', 'y', 'z'))\n    cols -= spatial_cols\n    if len(spatial_cols) > 0:\n        coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n        tree = scipy.spatial.KDTree(coords)\n\n        # TODO This is a bad algorithm\n        while True:\n            changed = False\n            for (i, j) in tree.query_pairs(tol, 2.):\n                # whenever we encounter a pair, ensure their index matches\n                i_i, i_j = indices[[i, j]]\n                if i_i != i_j:\n                    indices[i] = indices[j] = min(i_i, i_j)\n                    changed = True\n            if not changed:\n                break\n\n        self = self.with_column(polars.Series('_unique_pts', indices))\n        cols.add('_unique_pts')\n\n    frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n    if len(spatial_cols) > 0:\n        frame = frame.drop('_unique_pts')\n\n    return self.with_atoms(Atoms(frame, _unchecked=True))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_bounds","title":"with_bounds","text":"
    with_bounds(\n    cell_size: Optional[VecLike] = None,\n    cell_origin: Optional[VecLike] = None,\n) -> \"AtomCell\"\n

    Return a periodic cell with the given orthogonal cell dimensions.

    If cell_size is not specified, it will be assumed (and may be incorrect).

    Source code in atomlib/atoms.py
    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n    \"\"\"\n    Return a periodic cell with the given orthogonal cell dimensions.\n\n    If cell_size is not specified, it will be assumed (and may be incorrect).\n    \"\"\"\n    # TODO: test this\n    from .atomcell import AtomCell\n\n    if cell_size is None:\n        warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n        cell_size = self.bbox().size\n        cell_origin = self.bbox().min\n\n    # TODO test this origin code\n    cell = Cell.from_unit_cell(cell_size)\n    if cell_origin is not None:\n        cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n    return AtomCell(self.get_atoms(), cell, frame='local')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.coords","title":"coords","text":"
    coords(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> NDArray[float64]\n

    Return a (N, 3) ndarray of atom positions (dtype numpy.float64) in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> NDArray[numpy.float64]:\n    \"\"\"\n    Return a `(N, 3)` ndarray of atom positions (dtype [`numpy.float64`][numpy.float64])\n    in the given coordinate frame.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.x","title":"x","text":"
    x() -> Expr\n
    Source code in atomlib/atoms.py
    def x(self) -> polars.Expr:\n    return polars.col('coords').arr.get(0).alias('x')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.y","title":"y","text":"
    y() -> Expr\n
    Source code in atomlib/atoms.py
    def y(self) -> polars.Expr:\n    return polars.col('coords').arr.get(1).alias('y')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.z","title":"z","text":"
    z() -> Expr\n
    Source code in atomlib/atoms.py
    def z(self) -> polars.Expr:\n    return polars.col('coords').arr.get(2).alias('z')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.velocities","title":"velocities","text":"
    velocities(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Optional[NDArray[float64]]\n

    Return a (N, 3) ndarray of atom velocities (dtype numpy.float64) in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef velocities(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> t.Optional[NDArray[numpy.float64]]:\n    \"\"\"\n    Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64])\n    in the given coordinate frame.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.types","title":"types","text":"
    types() -> Optional[Series]\n

    Returns a Series of atom types (dtype polars.Int32).

    Source code in atomlib/atoms.py
    def types(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('type')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.masses","title":"masses","text":"
    masses() -> Optional[Series]\n

    Returns a Series of atom masses (dtype polars.Float32).

    Source code in atomlib/atoms.py
    def masses(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('mass')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.add_atom","title":"add_atom","text":"
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: Union[ArrayLike, float],\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None,\n    **kwargs: Any,\n) -> Self\n

    Return a copy of self with an extra atom.

    By default, all extra columns present in self must be specified as **kwargs.

    Try to avoid calling this in a loop (Use concat instead).

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef add_atom(self, elem: t.Union[int, str], /,  # type: ignore (spurious)\n             x: t.Union[ArrayLike, float],\n             y: t.Optional[float] = None,\n             z: t.Optional[float] = None, *,\n             frame: t.Optional[CoordinateFrame] = None,\n             **kwargs: t.Any) -> Self:\n    \"\"\"\n    Return a copy of `self` with an extra atom.\n\n    By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n    Try to avoid calling this in a loop (Use [`concat`][atomlib.atomcell.HasAtomCell.concat] instead).\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.pos","title":"pos","text":"
    pos(\n    x: Union[Sequence[Optional[float]], float, None] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n

    Select all atoms at a given position.

    Formally, returns all atoms within a cube of radius tol centered at (x,y,z), exclusive of the cube's surface.

    Additional parameters given as kwargs will be checked as additional parameters (with strict equality).

    Source code in atomlib/atoms.py
    def pos(self,\n        x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n        y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n        tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n    \"\"\"\n    Select all atoms at a given position.\n\n    Formally, returns all atoms within a cube of radius ``tol``\n    centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n    Additional parameters given as ``kwargs`` will be checked\n    as additional parameters (with strict equality).\n    \"\"\"\n\n    if isinstance(x, t.Sequence):\n        (x, y, z) = x\n\n    tol = abs(float(tol))\n    selection = polars.lit(True)\n    if x is not None:\n        selection &= self.x().is_between(x - tol, x + tol, closed='none')\n    if y is not None:\n        selection &= self.y().is_between(y - tol, y + tol, closed='none')\n    if z is not None:\n        selection &= self.z().is_between(z - tol, z + tol, closed='none')\n    for (col, val) in kwargs.items():\n        selection &= (polars.col(col) == val)\n\n    return selection\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_index","title":"with_index","text":"
    with_index(\n    index: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Returns self with a row index added in column 'i' (dtype polars.Int64). If index is not specified, defaults to an existing index or a new index.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_index(self, index: t.Optional[AtomValues] = None, *,\n               frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n    If `index` is not specified, defaults to an existing index or a new index.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_wobble","title":"with_wobble","text":"
    with_wobble(\n    wobble: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given displacements in column 'wobble' (dtype polars.Float64). If wobble is not specified, defaults to the already-existing wobbles or 0.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_wobble(self, wobble: t.Optional[AtomValues] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_occupancy","title":"with_occupancy","text":"
    with_occupancy(\n    frac_occupancy: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given fractional occupancies (dtype polars.Float64). If frac_occupancy is not specified, defaults to the already-existing occupancies or 1.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None, *,\n                   frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.apply_wobble","title":"apply_wobble","text":"
    apply_wobble(\n    rng: Union[Generator, int, None] = None,\n    frame: Optional[CoordinateFrame] = None,\n) -> Self\n

    Displace the atoms in self by the amount in the wobble column. wobble is interpretated as a mean-squared displacement, which is distributed equally over each axis.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None,\n                 frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Displace the atoms in `self` by the amount in the `wobble` column.\n    `wobble` is interpretated as a mean-squared displacement, which is distributed\n    equally over each axis.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.apply_occupancy","title":"apply_occupancy","text":"
    apply_occupancy(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    For each atom in self, use its frac_occupancy to randomly decide whether to remove it.

    Source code in atomlib/atoms.py
    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n    \"\"\"\n    if 'frac_occupancy' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    frac = self.select('frac_occupancy').to_series().to_numpy()\n    choice = rng.binomial(1, frac).astype(numpy.bool_)\n    return self.filter(polars.lit(choice))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_type","title":"with_type","text":"
    with_type(\n    types: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atom types in column 'type'. If types is not specified, use the already existing types or auto-assign them.

    When auto-assigning, each symbol is given a unique value, case-sensitive. Values are assigned from lowest atomic number to highest. For instance: [\"Ag+\", \"Na\", \"H\", \"Ag\"] => [3, 11, 1, 2]

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_type(self, types: t.Optional[AtomValues] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom types in column 'type'.\n    If `types` is not specified, use the already existing types or auto-assign them.\n\n    When auto-assigning, each symbol is given a unique value, case-sensitive.\n    Values are assigned from lowest atomic number to highest.\n    For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_mass","title":"with_mass","text":"
    with_mass(\n    mass: Optional[ArrayLike] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atom masses in column 'mass'. If mass is not specified, use the already existing masses or auto-assign them.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_mass(self, mass: t.Optional[ArrayLike] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom masses in column `'mass'`.\n    If `mass` is not specified, use the already existing masses or auto-assign them.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_symbol","title":"with_symbol","text":"
    with_symbol(\n    symbols: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atomic symbols.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atomic symbols.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_coords","title":"with_coords","text":"
    with_coords(\n    pts: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self replaced with the given atomic positions.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic positions.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_velocity","title":"with_velocity","text":"
    with_velocity(\n    pts: Optional[ArrayLike] = None,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self replaced with the given atomic velocities. If pts is not specified, use the already existing velocities or zero.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                  selection: t.Optional[AtomSelection] = None, *,\n                  frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic velocities.\n    If `pts` is not specified, use the already existing velocities or zero.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.get_atomcell","title":"get_atomcell","text":"
    get_atomcell() -> AtomCell\n
    Source code in atomlib/atomcell.py
    def get_atomcell(self) -> AtomCell:\n    frame = self.get_frame()\n    return AtomCell(self.get_atoms(frame), self.get_cell(), frame=frame, keep_frame=True)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.to_frame","title":"to_frame","text":"
    to_frame(frame: CoordinateFrame) -> Self\n

    Convert the stored Atoms to the given coordinate frame.

    Source code in atomlib/atomcell.py
    def to_frame(self, frame: CoordinateFrame) -> Self:\n    \"\"\"Convert the stored Atoms to the given coordinate frame.\"\"\"\n    return self.with_atoms(self.get_atoms(frame), frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.crop_to_box","title":"crop_to_box","text":"
    crop_to_box(eps: float = 1e-05) -> Self\n
    Source code in atomlib/atomcell.py
    def crop_to_box(self, eps: float = 1e-5) -> Self:\n    atoms = self._transform_atoms_in_frame('cell_box', lambda atoms: atoms.crop_atoms(*([-eps, 1-eps]*3)))\n    return self.with_atoms(atoms)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.wrap","title":"wrap","text":"
    wrap(eps: float = 1e-05) -> Self\n

    Wrap atoms around the cell boundaries.

    Source code in atomlib/atomcell.py
    def wrap(self, eps: float = 1e-5) -> Self:\n    \"\"\"Wrap atoms around the cell boundaries.\"\"\"\n    return self.with_atoms(self._transform_atoms_in_frame('cell_box', lambda a: a._wrap(eps)))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.repeat_to","title":"repeat_to","text":"
    repeat_to(\n    size: VecLike, crop: Union[bool, Sequence[bool]] = False\n) -> Self\n

    Repeat the cell so it is at least size along the crystal's axes.

    If crop, then crop the cell to exactly size. This may break periodicity. crop may be a vector, in which case you can specify cropping only along some axes.

    Source code in atomlib/atomcell.py
    def repeat_to(self, size: VecLike, crop: t.Union[bool, t.Sequence[bool]] = False) -> Self:\n    \"\"\"\n    Repeat the cell so it is at least `size` along the crystal's axes.\n\n    If `crop`, then crop the cell to exactly `size`. This may break periodicity.\n    `crop` may be a vector, in which case you can specify cropping only along some axes.\n    \"\"\"\n    size = to_vec3(size)\n    cell_size = self.cell_size * self.n_cells\n    repeat = numpy.maximum(numpy.ceil(size / cell_size).astype(int), 1)\n    atom_cell = self.repeat(repeat)\n\n    crop_v = to_vec3(crop, dtype=numpy.bool_)\n    if numpy.any(crop_v):\n        crop_x, crop_y, crop_z = crop_v\n        return atom_cell.crop(\n            x_max = size[0] if crop_x else numpy.inf,\n            y_max = size[1] if crop_y else numpy.inf,\n            z_max = size[2] if crop_z else numpy.inf,\n            frame='cell'\n        )\n\n    return atom_cell\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.repeat_x","title":"repeat_x","text":"
    repeat_x(n: int) -> Self\n

    Tile the cell in the x axis.

    Source code in atomlib/atomcell.py
    def repeat_x(self, n: int) -> Self:\n    \"\"\"Tile the cell in the x axis.\"\"\"\n    return self.repeat((n, 1, 1))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.repeat_y","title":"repeat_y","text":"
    repeat_y(n: int) -> Self\n

    Tile the cell in the y axis.

    Source code in atomlib/atomcell.py
    def repeat_y(self, n: int) -> Self:\n    \"\"\"Tile the cell in the y axis.\"\"\"\n    return self.repeat((1, n, 1))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.repeat_z","title":"repeat_z","text":"
    repeat_z(n: int) -> Self\n

    Tile the cell in the z axis.

    Source code in atomlib/atomcell.py
    def repeat_z(self, n: int) -> Self:\n    \"\"\"Tile the cell in the z axis.\"\"\"\n    return self.repeat((1, 1, n))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.repeat_to_x","title":"repeat_to_x","text":"
    repeat_to_x(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the x axis.

    Source code in atomlib/atomcell.py
    def repeat_to_x(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the x axis.\"\"\"\n    return self.repeat_to([size, 0., 0.], [crop, False, False])\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.repeat_to_y","title":"repeat_to_y","text":"
    repeat_to_y(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the y axis.

    Source code in atomlib/atomcell.py
    def repeat_to_y(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the y axis.\"\"\"\n    return self.repeat_to([0., size, 0.], [False, crop, False])\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.repeat_to_z","title":"repeat_to_z","text":"
    repeat_to_z(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the z axis.

    Source code in atomlib/atomcell.py
    def repeat_to_z(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the z axis.\"\"\"\n    return self.repeat_to([0., 0., size], [False, False, crop])\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.repeat_to_aspect","title":"repeat_to_aspect","text":"
    repeat_to_aspect(\n    plane: Literal[\"xy\", \"xz\", \"yz\"] = \"xy\",\n    *,\n    aspect: float = 1.0,\n    min_size: Optional[VecLike] = None,\n    max_size: Optional[VecLike] = None\n) -> Self\n

    Repeat to optimize the aspect ratio in plane, while staying above min_size and under max_size.

    Source code in atomlib/atomcell.py
    def repeat_to_aspect(self, plane: t.Literal['xy', 'xz', 'yz'] = 'xy', *,\n                     aspect: float = 1., min_size: t.Optional[VecLike] = None,\n                     max_size: t.Optional[VecLike] = None) -> Self:\n    \"\"\"\n    Repeat to optimize the aspect ratio in `plane`,\n    while staying above `min_size` and under `max_size`.\n    \"\"\"\n    if min_size is None:\n        min_n = numpy.array([1, 1, 1], numpy.int_)\n    else:\n        min_n = numpy.maximum(numpy.ceil(to_vec3(min_size) / self.box_size), 1).astype(numpy.int_)\n\n    if max_size is None:\n        max_n = 3 * min_n\n    else:\n        max_n = numpy.maximum(numpy.floor(to_vec3(max_size) / self.box_size), 1).astype(numpy.int_)\n\n    if plane == 'xy':\n        indices = [0, 1]\n    elif plane == 'xz':\n        indices = [0, 2]\n    elif plane == 'yz':\n        indices = [1, 2]\n    else:\n        raise ValueError(f\"Invalid plane '{plane}'. Exepcted 'xy', 'xz', 'or 'yz'.\")\n\n    na = numpy.arange(min_n[indices[0]], max_n[indices[0]])\n    nb = numpy.arange(min_n[indices[1]], max_n[indices[1]])\n    (na, nb) = numpy.meshgrid(na, nb)\n\n    aspects = na * self.box_size[indices[0]] / (nb * self.box_size[indices[1]])\n    # cost function: log(aspect)^2  (so cost(0.5) == cost(2))\n    min_i = numpy.argmin(numpy.log(aspects / aspect)**2)\n    repeat = numpy.array([1, 1, 1], numpy.int_)\n    repeat[indices] = na.flatten()[min_i], nb.flatten()[min_i]\n    return self.repeat(repeat)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.periodic_duplicate","title":"periodic_duplicate","text":"
    periodic_duplicate(eps: float = 1e-05) -> Self\n

    Add duplicate copies of atoms near periodic boundaries.

    For instance, an atom at a corner will be duplicated into 8 copies. This is mostly only useful for visualization.

    Source code in atomlib/atomcell.py
    def periodic_duplicate(self, eps: float = 1e-5) -> Self:\n    \"\"\"\n    Add duplicate copies of atoms near periodic boundaries.\n\n    For instance, an atom at a corner will be duplicated into 8 copies.\n    This is mostly only useful for visualization.\n    \"\"\"\n    frame_save = self.get_frame()\n    self = self.to_frame('cell_box').wrap(eps=eps)\n\n    for i in range(3):\n        self = self.concat((self,\n            self.filter(polars.col('coords').arr.get(i).abs() <= eps, frame='cell_box')\n                .transform_atoms(AffineTransform3D.translate([1. if i == j else 0. for j in range(3)]), frame='cell_box')\n        ))\n\n    return self.to_frame(frame_save)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.read","title":"read classmethod","text":"
    read(\n    path: FileOrPath, ty: Optional[FileType] = None\n) -> HasAtomsT\n

    Read a structure from a file.

    Supported types can be found in the io module. If no ty is specified, it is inferred from the file's extension.

    Source code in atomlib/mixins.py
    @classmethod\ndef read(cls: t.Type[HasAtomsT], path: FileOrPath, ty: t.Optional[FileType] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a file.\n\n    Supported types can be found in the [io][atomlib.io] module.\n    If no `ty` is specified, it is inferred from the file's extension.\n    \"\"\"\n    from .io import read\n    return _cast_atoms(read(path, ty), cls)  # type: ignore\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.read_cif","title":"read_cif classmethod","text":"
    read_cif(\n    f: Union[FileOrPath, CIF, CIFDataBlock],\n    block: Union[int, str, None] = None,\n) -> HasAtomsT\n

    Read a structure from a CIF file.

    If block is specified, read data from the given block of the CIF file (index or name).

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cif(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CIF, CIFDataBlock], block: t.Union[int, str, None] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a CIF file.\n\n    If `block` is specified, read data from the given block of the CIF file (index or name).\n    \"\"\"\n    from .io import read_cif\n    return _cast_atoms(read_cif(f, block), cls)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.read_xyz","title":"read_xyz classmethod","text":"
    read_xyz(f: Union[FileOrPath, XYZ]) -> HasAtomsT\n

    Read a structure from an XYZ file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xyz(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XYZ]) -> HasAtomsT:\n    \"\"\"Read a structure from an XYZ file.\"\"\"\n    from .io import read_xyz\n    return _cast_atoms(read_xyz(f), cls)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.read_xsf","title":"read_xsf classmethod","text":"
    read_xsf(f: Union[FileOrPath, XSF]) -> HasAtomsT\n

    Read a structure from an XSF file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xsf(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XSF]) -> HasAtomsT:\n    \"\"\"Read a structure from an XSF file.\"\"\"\n    from .io import read_xsf\n    return _cast_atoms(read_xsf(f), cls)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.read_cfg","title":"read_cfg classmethod","text":"
    read_cfg(f: Union[FileOrPath, CFG]) -> HasAtomsT\n

    Read a structure from a CFG file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cfg(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CFG]) -> HasAtomsT:\n    \"\"\"Read a structure from a CFG file.\"\"\"\n    from .io import read_cfg\n    return _cast_atoms(read_cfg(f), cls)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.read_lmp","title":"read_lmp classmethod","text":"
    read_lmp(\n    f: Union[FileOrPath, LMP],\n    type_map: Optional[Dict[int, Union[str, int]]] = None,\n) -> HasAtomsT\n

    Read a structure from a LAAMPS data file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_lmp(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, LMP], type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> HasAtomsT:\n    \"\"\"Read a structure from a LAAMPS data file.\"\"\"\n    from .io import read_lmp\n    return _cast_atoms(read_lmp(f, type_map=type_map), cls)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.write_cif","title":"write_cif","text":"
    write_cif(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cif(self, f: FileOrPath):\n    from .io import write_cif\n    write_cif(self, f)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.write_xyz","title":"write_xyz","text":"
    write_xyz(f: FileOrPath, fmt: XYZFormat = 'exyz')\n
    Source code in atomlib/mixins.py
    def write_xyz(self, f: FileOrPath, fmt: XYZFormat = 'exyz'):\n    from .io import write_xyz\n    write_xyz(self, f, fmt)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.write_xsf","title":"write_xsf","text":"
    write_xsf(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_xsf(self, f: FileOrPath):\n    from .io import write_xsf\n    write_xsf(self, f)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.write_cfg","title":"write_cfg","text":"
    write_cfg(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cfg(self, f: FileOrPath):\n    from .io import write_cfg\n    write_cfg(self, f)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.write_lmp","title":"write_lmp","text":"
    write_lmp(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_lmp(self, f: FileOrPath):\n    from .io import write_lmp\n    write_lmp(self, f)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.write","title":"write","text":"
    write(path: FileOrPath, ty: Optional[FileType] = None)\n

    Write this structure to a file.

    A file type may be specified using ty. If no ty is specified, it is inferred from the path's extension.

    Source code in atomlib/mixins.py
    def write(self, path: FileOrPath, ty: t.Optional[FileType] = None):\n    \"\"\"\n    Write this structure to a file.\n\n    A file type may be specified using `ty`.\n    If no `ty` is specified, it is inferred from the path's extension.\n    \"\"\"\n    from .io import write\n    write(self, path, ty)  # type: ignore\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.write_mslice","title":"write_mslice","text":"
    write_mslice(\n    f: BinaryFileOrPath,\n    template: Optional[MSliceFile] = None,\n    *,\n    slice_thickness: Optional[float] = None,\n    scan_points: Optional[ArrayLike] = None,\n    scan_extent: Optional[ArrayLike] = None,\n    noise_sigma: Optional[float] = None,\n    conv_angle: Optional[float] = None,\n    energy: Optional[float] = None,\n    defocus: Optional[float] = None,\n    tilt: Optional[Tuple[float, float]] = None,\n    tds: Optional[bool] = None,\n    n_cells: Optional[ArrayLike] = None\n)\n

    Write a structure to an mslice file.

    template may be a file, path, or ElementTree containing an existing mslice file. Its structure will be modified to make the final output. If not specified, a default template will be used.

    Additional options modify simulation properties. If an option is not specified, the template's properties are used.

    Source code in atomlib/mixins.py
    def write_mslice(self, f: BinaryFileOrPath, template: t.Optional[MSliceFile] = None, *,\n             slice_thickness: t.Optional[float] = None,  # angstrom\n             scan_points: t.Optional[ArrayLike] = None,\n             scan_extent: t.Optional[ArrayLike] = None,\n             noise_sigma: t.Optional[float] = None,  # angstrom\n             conv_angle: t.Optional[float] = None,  # mrad\n             energy: t.Optional[float] = None,  # keV\n             defocus: t.Optional[float] = None,  # angstrom\n             tilt: t.Optional[t.Tuple[float, float]] = None,  # (mrad, mrad)\n             tds: t.Optional[bool] = None,\n             n_cells: t.Optional[ArrayLike] = None):\n    \"\"\"\n    Write a structure to an mslice file.\n\n    `template` may be a file, path, or `ElementTree` containing an existing mslice file.\n    Its structure will be modified to make the final output. If not specified, a default\n    template will be used.\n\n    Additional options modify simulation properties. If an option is not specified, the\n    template's properties are used.\n    \"\"\"\n    from .io import write_mslice\n    return write_mslice(self, f, template, slice_thickness=slice_thickness,\n                        scan_points=scan_points, scan_extent=scan_extent,\n                        conv_angle=conv_angle, energy=energy, defocus=defocus,\n                        noise_sigma=noise_sigma, tilt=tilt, tds=tds, n_cells=n_cells)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.write_qe","title":"write_qe","text":"
    write_qe(\n    f: FileOrPath,\n    pseudo: Optional[Mapping[str, str]] = None,\n)\n

    Write a structure to a Quantum Espresso pw.x file.

    PARAMETER DESCRIPTION f

    File or path to write to

    TYPE: FileOrPath

    pseudo

    Mapping from atom symbol

    TYPE: Optional[Mapping[str, str]] DEFAULT: None

    Source code in atomlib/mixins.py
    def write_qe(self, f: FileOrPath, pseudo: t.Optional[t.Mapping[str, str]] = None):\n    \"\"\"\n    Write a structure to a Quantum Espresso pw.x file.\n\n    Args:\n      f: File or path to write to\n      pseudo: Mapping from atom symbol\n    \"\"\"\n    from .io import write_qe\n    write_qe(self, f, pseudo)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.get_cell","title":"get_cell","text":"
    get_cell() -> Cell\n
    Source code in atomlib/atomcell.py
    def get_cell(self) -> Cell:\n    return self.cell\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_cell","title":"with_cell","text":"
    with_cell(cell: Cell) -> Self\n
    Source code in atomlib/atomcell.py
    def with_cell(self, cell: Cell) -> Self:\n    return self.__class__(self.atoms, cell, frame=self.frame, keep_frame=True)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.get_atoms","title":"get_atoms","text":"
    get_atoms(frame: Optional[CoordinateFrame] = None) -> Atoms\n

    Get atoms contained in self, in the given coordinate frame.

    Source code in atomlib/atomcell.py
    def get_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> Atoms:\n    \"\"\"Get atoms contained in ``self``, in the given coordinate frame.\"\"\"\n\n    if frame is None or frame == self.get_frame():\n        return self.atoms\n    return self.atoms.transform(self.get_transform(frame, self.get_frame()))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_atoms","title":"with_atoms","text":"
    with_atoms(\n    atoms: HasAtoms, frame: Optional[CoordinateFrame] = None\n) -> Self\n
    Source code in atomlib/atomcell.py
    def with_atoms(self, atoms: HasAtoms, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    frame = frame if frame is not None else self.frame\n    return self.__class__(atoms.get_atoms(), cell=self.cell, frame=frame, keep_frame=True)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.get_frame","title":"get_frame","text":"
    get_frame() -> CoordinateFrame\n

    Get the coordinate frame atoms are stored in.

    Source code in atomlib/atomcell.py
    def get_frame(self) -> CoordinateFrame:\n    \"\"\"Get the coordinate frame atoms are stored in.\"\"\"\n    return self.frame\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.from_ortho","title":"from_ortho classmethod","text":"
    from_ortho(\n    atoms: IntoAtoms,\n    ortho: LinearTransform3D,\n    *,\n    n_cells: Optional[VecLike] = None,\n    frame: CoordinateFrame = \"local\",\n    keep_frame: bool = False\n)\n

    Make an atom cell given a list of atoms and an orthogonalization matrix. Atoms are assumed to be in the coordinate system frame.

    Source code in atomlib/atomcell.py
    @classmethod\ndef from_ortho(cls, atoms: IntoAtoms, ortho: LinearTransform3D, *,\n               n_cells: t.Optional[VecLike] = None,\n               frame: CoordinateFrame = 'local',\n               keep_frame: bool = False):\n    \"\"\"\n    Make an atom cell given a list of atoms and an orthogonalization matrix.\n    Atoms are assumed to be in the coordinate system `frame`.\n    \"\"\"\n    cell = Cell.from_ortho(ortho, n_cells)\n    return cls(atoms, cell, frame=frame, keep_frame=keep_frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.from_unit_cell","title":"from_unit_cell classmethod","text":"
    from_unit_cell(\n    atoms: IntoAtoms,\n    cell_size: VecLike,\n    cell_angle: Optional[VecLike] = None,\n    *,\n    n_cells: Optional[VecLike] = None,\n    frame: CoordinateFrame = \"local\",\n    keep_frame: bool = False\n)\n

    Make a cell given a list of atoms and unit cell parameters. Atoms are assumed to be in the coordinate system frame.

    Source code in atomlib/atomcell.py
    @classmethod\ndef from_unit_cell(cls, atoms: IntoAtoms, cell_size: VecLike,\n                   cell_angle: t.Optional[VecLike] = None, *,\n                   n_cells: t.Optional[VecLike] = None,\n                   frame: CoordinateFrame = 'local',\n                   keep_frame: bool = False):\n    \"\"\"\n    Make a cell given a list of atoms and unit cell parameters.\n    Atoms are assumed to be in the coordinate system `frame`.\n    \"\"\"\n    cell = Cell.from_unit_cell(cell_size, cell_angle, n_cells=n_cells)\n    return cls(atoms, cell, frame=frame, keep_frame=keep_frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.orthogonalize","title":"orthogonalize","text":"
    orthogonalize() -> OrthoCell\n
    Source code in atomlib/atomcell.py
    def orthogonalize(self) -> OrthoCell:\n    if self.is_orthogonal():\n        return OrthoCell(self.atoms, self.cell, frame=self.frame)\n    raise NotImplementedError()\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.clone","title":"clone","text":"
    clone() -> AtomCellT\n

    Make a deep copy of self.

    Source code in atomlib/atomcell.py
    def clone(self: AtomCellT) -> AtomCellT:\n    \"\"\"Make a deep copy of `self`.\"\"\"\n    return self.__class__(**{field.name: copy.deepcopy(getattr(self, field.name)) for field in fields(self)})\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n

    Assert this structure is equal to other

    Source code in atomlib/atomcell.py
    def assert_equal(self, other: t.Any):\n    \"\"\"Assert this structure is equal to `other`\"\"\"\n    assert isinstance(other, AtomCell)\n    self.cell.assert_equal(other.cell)\n    self.get_atoms('local').assert_equal(other.get_atoms('local'))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell","title":"OrthoCell dataclass","text":"

    Bases: AtomCell

    Source code in atomlib/atomcell.py
    class OrthoCell(AtomCell):\n    def __post_init__(self):\n        if not numpy.allclose(self.cell.cell_angle, numpy.pi/2.):\n            raise ValueError(f\"OrthoCell constructed with non-orthogonal angles: {self.cell.cell_angle}\")\n\n    def is_orthogonal(self, tol: float = 1e-8) -> t.Literal[True]:\n        \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n        return True\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.affine","title":"affine property","text":"
    affine: AffineTransform3D\n

    Affine transformation. Holds transformation from 'ortho' to 'local' coordinates, including rotation away from the standard crystal orientation.

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.ortho","title":"ortho property","text":"
    ortho: LinearTransform3D\n

    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.metric","title":"metric property","text":"
    metric: LinearTransform3D\n

    Cell metric tensor

    Returns the dot product between every combination of basis vectors. :math:\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.cell_size","title":"cell_size property","text":"
    cell_size: NDArray[float64]\n

    Unit cell size.

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.cell_angle","title":"cell_angle property","text":"
    cell_angle: NDArray[float64]\n

    Unit cell angles, in radians.

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.n_cells","title":"n_cells property","text":"
    n_cells: NDArray[int_]\n

    Number of unit cells.

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.pbc","title":"pbc property","text":"
    pbc: NDArray[bool_]\n

    Flags indicating the presence of periodic boundary conditions along each axis.

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.ortho_size","title":"ortho_size property","text":"
    ortho_size: NDArray[float64]\n

    Return size of orthogonal unit cell.

    Equivalent to the diagonal of the orthogonalization matrix.

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.box_size","title":"box_size property","text":"
    box_size: NDArray[float64]\n

    Return size of the cell box.

    Equivalent to self.n_cells * self.cell_size.

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.columns","title":"columns property","text":"
    columns: List[str]\n

    Return the column names in self.

    RETURNS DESCRIPTION List[str]

    A sequence of column names

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.dtypes","title":"dtypes property","text":"
    dtypes: List[DataType]\n

    Return the datatypes in self.

    RETURNS DESCRIPTION List[DataType]

    A sequence of column DataTypes

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.schema","title":"schema property","text":"
    schema: Schema\n

    Return the schema of self.

    RETURNS DESCRIPTION Schema

    A dictionary of column names and DataTypes

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_column","title":"with_column class-attribute instance-attribute","text":"
    with_column = with_columns\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.unique","title":"unique class-attribute instance-attribute","text":"
    unique = deduplicate\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.atoms","title":"atoms instance-attribute","text":"
    atoms: Atoms\n

    Atoms in the cell. Stored in 'local' coordinates (i.e. relative to the enclosing group but not relative to box dimensions).

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.cell","title":"cell instance-attribute","text":"
    cell: Cell\n

    Cell coordinate system.

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.frame","title":"frame class-attribute instance-attribute","text":"
    frame: CoordinateFrame = 'local'\n

    Coordinate frame 'atoms' are stored in.

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.get_cell","title":"get_cell","text":"
    get_cell() -> Cell\n
    Source code in atomlib/atomcell.py
    def get_cell(self) -> Cell:\n    return self.cell\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_cell","title":"with_cell","text":"
    with_cell(cell: Cell) -> Self\n
    Source code in atomlib/atomcell.py
    def with_cell(self, cell: Cell) -> Self:\n    return self.__class__(self.atoms, cell, frame=self.frame, keep_frame=True)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.get_transform","title":"get_transform","text":"
    get_transform(\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n

    In the two-argument form, get the transform to frame_to from frame_from. In the one-argument form, get the transform from local coordinates to 'frame'.

    Source code in atomlib/cell.py
    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n    \"\"\"\n    In the two-argument form, get the transform to `frame_to` from `frame_from`.\n    In the one-argument form, get the transform from local coordinates to 'frame'.\n    \"\"\"\n    transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n    transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n    if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n        return AffineTransform3D()\n    return transform_to.inverse() @ transform_from\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.corners","title":"corners","text":"
    corners(frame: CoordinateFrame = 'local') -> ndarray\n
    Source code in atomlib/cell.py
    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n    corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n    return self.get_transform(frame, 'cell_box') @ corners\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.bbox_cell","title":"bbox_cell","text":"
    bbox_cell(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the bounding box of the cell box in the given coordinate system.

    Source code in atomlib/cell.py
    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n    return BBox3D.from_pts(self.corners(frame))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.bbox","title":"bbox","text":"
    bbox(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the combined bounding box of the cell and atoms in the given coordinate system. To get the cell or atoms bounding box only, use bbox_cell or bbox_atoms.

    Source code in atomlib/atomcell.py
    def bbox(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"\n    Return the combined bounding box of the cell and atoms in the given coordinate system.\n    To get the cell or atoms bounding box only, use [`bbox_cell`][atomlib.atomcell.HasAtomCell.bbox_cell] or [`bbox_atoms`][atomlib.atomcell.HasAtomCell.bbox_atoms].\n    \"\"\"\n    return self.bbox_atoms(frame) | self.bbox_cell(frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.is_orthogonal_in_local","title":"is_orthogonal_in_local","text":"
    is_orthogonal_in_local(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal and aligned with the local coordinate system.

    Source code in atomlib/cell.py
    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n    transform = (self.affine @ self.ortho).to_linear()\n    if not transform.is_scaled_orthogonal(tol):\n        return False\n    normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n    # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n    return all(\n        any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n        for row in normed\n    )\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.to_ortho","title":"to_ortho","text":"
    to_ortho() -> AffineTransform3D\n
    Source code in atomlib/cell.py
    def to_ortho(self) -> AffineTransform3D:\n    return self.get_transform('local', 'cell_box')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.transform_cell","title":"transform_cell","text":"
    transform_cell(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> Self\n

    Apply the given transform to the unit cell, without changing atom positions. The transform is applied in coordinate frame 'frame'.

    Source code in atomlib/atomcell.py
    def transform_cell(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n    \"\"\"\n    Apply the given transform to the unit cell, without changing atom positions.\n    The transform is applied in coordinate frame 'frame'.\n    \"\"\"\n    return self.with_cell(self.get_cell().transform_cell(transform, frame=frame))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.strain_orthogonal","title":"strain_orthogonal","text":"
    strain_orthogonal() -> HasCellT\n

    Orthogonalize using strain.

    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane. For small displacements, no hydrostatic strain is applied (volume is conserved).

    Source code in atomlib/cell.py
    def strain_orthogonal(self: HasCellT) -> HasCellT:\n    \"\"\"\n    Orthogonalize using strain.\n\n    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n    For small displacements, no hydrostatic strain is applied (volume is conserved).\n    \"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=LinearTransform3D(),\n        cell_size=self.cell_size,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.repeat","title":"repeat","text":"
    repeat(n: Union[int, VecLike]) -> Self\n

    Tile the cell

    Source code in atomlib/atomcell.py
    def repeat(self, n: t.Union[int, VecLike]) -> Self:\n    \"\"\"Tile the cell\"\"\"\n    ns = numpy.broadcast_to(n, 3)\n    if not numpy.issubdtype(ns.dtype, numpy.integer):\n        raise ValueError(\"repeat() argument must be an integer or integer array.\")\n\n    cells = numpy.stack(numpy.meshgrid(*map(numpy.arange, ns))) \\\n        .reshape(3, -1).T.astype(float)\n    cells = cells * self.box_size\n\n    atoms = self.get_atoms('cell')\n    atoms = Atoms.concat([\n        atoms.transform(AffineTransform3D.translate(cell))\n        for cell in cells\n    ]) #.transform(self.cell.get_transform('local', 'cell_frac'))\n    return self.with_atoms(atoms, 'cell').with_cell(self.get_cell().repeat(ns))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.explode","title":"explode","text":"
    explode() -> Self\n

    Materialize repeated cells as one supercell.

    Source code in atomlib/atomcell.py
    def explode(self) -> Self:\n    \"\"\"Materialize repeated cells as one supercell.\"\"\"\n    frame = self.get_frame()\n\n    return self.with_atoms(self.get_atoms('local'), 'local') \\\n        .with_cell(self.get_cell().explode()) \\\n        .to_frame(frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.explode_z","title":"explode_z","text":"
    explode_z() -> HasCellT\n

    Materialize repeated cells as one supercell in z.

    Source code in atomlib/cell.py
    def explode_z(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n        n_cells=[*self.n_cells[:2], 1],\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.crop","title":"crop","text":"
    crop(\n    x_min: float = -numpy.inf,\n    x_max: float = numpy.inf,\n    y_min: float = -numpy.inf,\n    y_max: float = numpy.inf,\n    z_min: float = -numpy.inf,\n    z_max: float = numpy.inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> Self\n

    Crop atoms and cell to the given extents. For a non-orthogonal cell, this must be specified in cell coordinates. This function implicity explodes the cell as well.

    To crop atoms only, use crop_atoms instead.

    Source code in atomlib/atomcell.py
    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n         frame: CoordinateFrame = 'local') -> Self:\n    \"\"\"\n    Crop atoms and cell to the given extents. For a non-orthogonal\n    cell, this must be specified in cell coordinates. This\n    function implicity `explode`s the cell as well.\n\n    To crop atoms only, use `crop_atoms` instead.\n    \"\"\"\n\n    cell = self.get_cell().crop(x_min, x_max, y_min, y_max, z_min, z_max, frame=frame)\n    atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n    return self.with_cell(cell).with_atoms(atoms)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.change_transform","title":"change_transform","text":"
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n

    Coordinate-change a transformation from frame_from into frame_to.

    Source code in atomlib/cell.py
    def change_transform(self, transform: Transform3D,\n                     frame_to: t.Optional[CoordinateFrame] = None,\n                     frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n    \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n    if frame_to == frame_from and frame_to is not None:\n        return transform\n    coord_change = self.get_transform(frame_to, frame_from)\n    return coord_change @ transform @ coord_change.inverse()\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n

    Assert this structure is equal to other

    Source code in atomlib/atomcell.py
    def assert_equal(self, other: t.Any):\n    \"\"\"Assert this structure is equal to `other`\"\"\"\n    assert isinstance(other, AtomCell)\n    self.cell.assert_equal(other.cell)\n    self.get_atoms('local').assert_equal(other.get_atoms('local'))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.get_atoms","title":"get_atoms","text":"
    get_atoms(frame: Optional[CoordinateFrame] = None) -> Atoms\n

    Get atoms contained in self, in the given coordinate frame.

    Source code in atomlib/atomcell.py
    def get_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> Atoms:\n    \"\"\"Get atoms contained in ``self``, in the given coordinate frame.\"\"\"\n\n    if frame is None or frame == self.get_frame():\n        return self.atoms\n    return self.atoms.transform(self.get_transform(frame, self.get_frame()))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_atoms","title":"with_atoms","text":"
    with_atoms(\n    atoms: HasAtoms, frame: Optional[CoordinateFrame] = None\n) -> Self\n
    Source code in atomlib/atomcell.py
    def with_atoms(self, atoms: HasAtoms, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    frame = frame if frame is not None else self.frame\n    return self.__class__(atoms.get_atoms(), cell=self.cell, frame=frame, keep_frame=True)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.describe","title":"describe","text":"
    describe(\n    percentiles: Union[Sequence[float], float, None] = (\n        0.25,\n        0.5,\n        0.75,\n    ),\n    *,\n    interpolation: RollingInterpolationMethod = \"nearest\",\n    frame: Optional[CoordinateFrame] = None\n) -> DataFrame\n

    Return summary statistics for self. See DataFrame.describe for more information.

    PARAMETER DESCRIPTION percentiles

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), 50% (median), and 75% (third quartile).

    TYPE: Union[Sequence[float], float, None] DEFAULT: (0.25, 0.5, 0.75)

    RETURNS DESCRIPTION DataFrame

    A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n             interpolation: RollingInterpolationMethod = 'nearest',\n             frame: t.Optional[CoordinateFrame] = None) -> polars.DataFrame:\n    \"\"\"\n    Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n    Args:\n      percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                   50% (median), and 75% (third quartile).\n\n    Returns:\n      A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_columns","title":"with_columns","text":"
    with_columns(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self\n

    Return a copy of self with the given columns added.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_columns(self,\n                 *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 frame: t.Optional[CoordinateFrame] = None,\n                 **named_exprs: IntoExpr) -> Self:\n    \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.insert_column","title":"insert_column","text":"
    insert_column(index: int, column: Series) -> DataFrame\n
    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n    return self._get_frame().insert_column(index, column)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.get_column","title":"get_column","text":"
    get_column(\n    name: str, *, frame: Optional[CoordinateFrame] = None\n) -> Series\n

    Get the specified column from self, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef get_column(self, name: str, *, frame: t.Optional[CoordinateFrame] = None) -> polars.Series:\n    \"\"\"\n    Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.get_columns","title":"get_columns","text":"
    get_columns(\n    *, frame: Optional[CoordinateFrame] = None\n) -> List[Series]\n

    Return all columns from self as a list of Series.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef get_columns(self, *, frame: t.Optional[CoordinateFrame] = None) -> t.List[polars.Series]:\n    \"\"\"\n    Return all columns from `self` as a list of [`Series`][polars.Series].\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.get_column_index","title":"get_column_index","text":"
    get_column_index(name: str) -> int\n

    Get the index of a column by name, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_column_index)\ndef get_column_index(self, name: str) -> int:\n    \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.group_by","title":"group_by","text":"
    group_by(\n    *by: Union[IntoExpr, Iterable[IntoExpr]],\n    maintain_order: bool = False,\n    frame: Optional[CoordinateFrame] = None,\n    **named_by: IntoExpr\n) -> GroupBy\n

    Start a group by operation. See DataFrame.group_by for more information.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n             maintain_order: bool = False, frame: t.Optional[CoordinateFrame] = None,\n             **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n    \"\"\"\n    Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.pipe","title":"pipe","text":"
    pipe(\n    function: Callable[Concatenate[HasAtomCellT, P], T],\n    *args: args,\n    **kwargs: kwargs\n) -> T\n

    Apply function to self (in method-call syntax).

    Source code in atomlib/atomcell.py
    def pipe(self: HasAtomCellT, function: t.Callable[Concatenate[HasAtomCellT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n    \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n    return function(self, *args, **kwargs)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.clone","title":"clone","text":"
    clone() -> AtomCellT\n

    Make a deep copy of self.

    Source code in atomlib/atomcell.py
    def clone(self: AtomCellT) -> AtomCellT:\n    \"\"\"Make a deep copy of `self`.\"\"\"\n    return self.__class__(**{field.name: copy.deepcopy(getattr(self, field.name)) for field in fields(self)})\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.drop","title":"drop","text":"
    drop(\n    *columns: Union[str, Iterable[str]], strict: bool = True\n) -> DataFrame\n

    Return self with the specified columns removed.

    Source code in atomlib/atoms.py
    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n    \"\"\"Return `self` with the specified columns removed.\"\"\"\n    return self._get_frame().drop(*columns, strict=strict)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.filter","title":"filter","text":"
    filter(\n    *predicates: Union[\n        None,\n        IntoExprColumn,\n        Iterable[IntoExprColumn],\n        bool,\n        List[bool],\n        ndarray,\n    ],\n    frame: Optional[CoordinateFrame] = None,\n    **constraints: Any\n) -> Self\n

    Filter self, removing rows which evaluate to False.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef filter(\n    self,\n    *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n    frame: t.Optional[CoordinateFrame] = None,\n    **constraints: t.Any,\n) -> Self:\n    \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.sort","title":"sort","text":"
    sort(\n    by: Union[IntoExpr, Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: Union[bool, Sequence[bool]] = False,\n    nulls_last: bool = False\n) -> Self\n

    Sort the atoms in self by the given columns/expressions.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef sort(\n    self,\n    by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: t.Union[bool, t.Sequence[bool]] = False,\n    nulls_last: bool = False,\n) -> Self:\n    \"\"\"\n    Sort the atoms in `self` by the given columns/expressions.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.slice","title":"slice","text":"
    slice(\n    offset: int,\n    length: Optional[int] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return a slice of the rows in self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef slice(self, offset: int, length: t.Optional[int] = None, *,\n          frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return a slice of the rows in `self`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.head","title":"head","text":"
    head(\n    n: int = 5, *, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return the first n rows of self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef head(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return the first `n` rows of `self`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.tail","title":"tail","text":"
    tail(\n    n: int = 5, *, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return the last n rows of self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef tail(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return the last `n` rows of `self`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.drop_nulls","title":"drop_nulls","text":"
    drop_nulls(\n    subset: Union[str, Collection[str], None] = None\n) -> DataFrame\n

    Drop rows that contain nulls in any of columns subset.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n    \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n    return self._get_frame().drop_nulls(subset)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.fill_null","title":"fill_null","text":"
    fill_null(\n    value: Any = None,\n    strategy: Optional[FillNullStrategy] = None,\n    limit: Optional[int] = None,\n    matches_supertype: bool = True,\n) -> Self\n
    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef fill_null(\n    self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n    limit: t.Optional[int] = None, matches_supertype: bool = True,\n) -> Self:\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.fill_nan","title":"fill_nan","text":"
    fill_nan(\n    value: Union[Expr, int, float, None],\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n
    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef fill_nan(self, value: t.Union[polars.Expr, int, float, None], *,\n             frame: t.Optional[CoordinateFrame] = None) -> Self:\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.concat","title":"concat classmethod","text":"
    concat(\n    atoms: Union[\n        HasAtomsT,\n        IntoAtoms,\n        Iterable[Union[HasAtomsT, IntoAtoms]],\n    ],\n    *,\n    rechunk: bool = True,\n    how: ConcatMethod = \"vertical\"\n) -> HasAtomsT\n

    Concatenate multiple Atoms together, handling metadata appropriately.

    Source code in atomlib/atoms.py
    @classmethod\ndef concat(cls: t.Type[HasAtomsT],\n           atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n           rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n    \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n    # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n    # same type as ``cls``.\n    if _is_abstract(cls):\n        raise TypeError(\"concat() must be called on a concrete class.\")\n\n    if isinstance(atoms, HasAtoms):\n        atoms = (atoms,)\n    dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n    representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n    if len(dfs) == 0:\n        return representative.with_atoms(Atoms.empty(), 'local')\n\n    if how in ('vertical', 'vertical_relaxed'):\n        # get order from first member\n        cols = dfs[0].columns\n        dfs = [df.select(cols) for df in dfs]\n    elif how == 'inner':\n        cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n        schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n        if len(schema) == 0:\n            raise ValueError(\"Atoms have no columns in common\")\n\n        dfs = [_select_schema(df, schema) for df in dfs]\n        how = 'vertical'\n\n    return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.partition_by","title":"partition_by","text":"
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: bool = False\n) -> Union[List[Self], Dict[Any, Self]]\n

    Group by the given columns and partition into separate dataframes.

    Return the partitions as a dictionary by specifying as_dict=True.

    Source code in atomlib/atoms.py
    def partition_by(\n    self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n    maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n    \"\"\"\n    Group by the given columns and partition into separate dataframes.\n\n    Return the partitions as a dictionary by specifying `as_dict=True`.\n    \"\"\"\n    if as_dict:\n        d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n        return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n    return [\n        self.with_atoms(Atoms(df, _unchecked=True))\n        for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n    ]\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.select","title":"select","text":"
    select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef select(\n    self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> polars.DataFrame:\n    \"\"\"\n    Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.select_schema","title":"select_schema","text":"
    select_schema(schema: SchemaDict) -> DataFrame\n

    Select columns from self and cast to the given schema. Raises TypeError if a column is not found or if it can't be cast.

    Source code in atomlib/atoms.py
    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n    \"\"\"\n    Select columns from `self` and cast to the given schema.\n    Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n    \"\"\"\n    return _select_schema(self, schema)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.select_props","title":"select_props","text":"
    select_props(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self\n

    Select exprs from self, while keeping required columns. Doesn't affect the cell.

    RETURNS DESCRIPTION Self

    A HasAtomCell filtered to contain

    Self

    the specified properties (as well as required columns).

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef select_props(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self:\n    \"\"\"\n    Select `exprs` from `self`, while keeping required columns.\n    Doesn't affect the cell.\n\n    Returns:\n      A [`HasAtomCell`][atomlib.atomcell.HasAtomCell] filtered to contain\n      the specified properties (as well as required columns).\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.try_select","title":"try_select","text":"
    try_select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Optional[DataFrame]\n

    Try to select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns. Returns None if any columns are missing.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef try_select(\n    self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> t.Optional[polars.DataFrame]:\n    \"\"\"\n    Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns. Returns `None` if any\n    columns are missing.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.try_get_column","title":"try_get_column","text":"
    try_get_column(name: str) -> Optional[Series]\n

    Try to get a column from self, returning None if it doesn't exist.

    Source code in atomlib/atoms.py
    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n    \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n    try:\n        return self.get_column(name)\n    except polars.exceptions.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.bbox_atoms","title":"bbox_atoms","text":"
    bbox_atoms(\n    frame: Optional[CoordinateFrame] = None,\n) -> BBox3D\n

    Return the bounding box of all the atoms in self, in the given coordinate frame.

    Source code in atomlib/atomcell.py
    def bbox_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> BBox3D:\n    \"\"\"Return the bounding box of all the atoms in `self`, in the given coordinate frame.\"\"\"\n    return self.get_atoms(frame).bbox()\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.transform_atoms","title":"transform_atoms","text":"
    transform_atoms(\n    transform: IntoTransform3D,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: CoordinateFrame = \"local\",\n    transform_velocities: bool = False\n) -> Self\n

    Transform the atoms in self by transform. If selection is given, only transform the atoms in selection.

    Source code in atomlib/atomcell.py
    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                    frame: CoordinateFrame = 'local', transform_velocities: bool = False) -> Self:\n    \"\"\"\n    Transform the atoms in `self` by `transform`.\n    If `selection` is given, only transform the atoms in `selection`.\n    \"\"\"\n    transform = self.change_transform(Transform3D.make(transform), self.get_frame(), frame)\n    return self.with_atoms(self.get_atoms(self.get_frame()).transform(transform, selection, transform_velocities=transform_velocities))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.transform","title":"transform","text":"
    transform(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> Self\n
    Source code in atomlib/atomcell.py
    def transform(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n    if isinstance(transform, Transform3D) and not isinstance(transform, AffineTransform3D):\n        raise ValueError(\"Non-affine transforms cannot change the box dimensions. Use 'transform_atoms' instead.\")\n    # TODO: cleanup once tests pass\n    # coordinate change the transform into atomic coordinates\n    new_cell = self.get_cell().transform_cell(transform, frame)\n    transform = self.get_cell().change_transform(transform, self.get_frame(), frame)\n    return self.with_atoms(self.get_atoms().transform(transform), self.get_frame()).with_cell(new_cell)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.round_near_zero","title":"round_near_zero","text":"
    round_near_zero(\n    tol: float = 1e-14,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Round atom position values near zero to zero.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef round_near_zero(self, tol: float = 1e-14, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Round atom position values near zero to zero.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.crop_atoms","title":"crop_atoms","text":"
    crop_atoms(\n    x_min: float = -numpy.inf,\n    x_max: float = numpy.inf,\n    y_min: float = -numpy.inf,\n    y_max: float = numpy.inf,\n    z_min: float = -numpy.inf,\n    z_max: float = numpy.inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> Self\n
    Source code in atomlib/atomcell.py
    def crop_atoms(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n               y_min: float = -numpy.inf, y_max: float = numpy.inf,\n               z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n               frame: CoordinateFrame = 'local') -> Self:\n    atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n    return self.with_atoms(atoms)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.deduplicate","title":"deduplicate","text":"
    deduplicate(\n    tol: float = 0.001,\n    subset: Iterable[str] = (\"x\", \"y\", \"z\", \"symbol\"),\n    keep: UniqueKeepStrategy = \"first\",\n    maintain_order: bool = True,\n) -> Self\n

    De-duplicate atoms in self. Atoms of the same symbol that are closer than tolerance to each other (by Euclidian distance) will be removed, leaving only the atom specified by keep (defaults to the first atom).

    If subset is specified, only those columns will be included while assessing duplicates. Floating point columns other than 'x', 'y', and 'z' will not by toleranced.

    Source code in atomlib/atoms.py
    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n    \"\"\"\n    De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n    to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n    `keep` (defaults to the first atom).\n\n    If `subset` is specified, only those columns will be included while assessing duplicates.\n    Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n    \"\"\"\n    import scipy.spatial\n\n    cols = set((subset,) if isinstance(subset, str) else subset)\n\n    indices = numpy.arange(len(self))\n\n    spatial_cols = cols.intersection(('x', 'y', 'z'))\n    cols -= spatial_cols\n    if len(spatial_cols) > 0:\n        coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n        tree = scipy.spatial.KDTree(coords)\n\n        # TODO This is a bad algorithm\n        while True:\n            changed = False\n            for (i, j) in tree.query_pairs(tol, 2.):\n                # whenever we encounter a pair, ensure their index matches\n                i_i, i_j = indices[[i, j]]\n                if i_i != i_j:\n                    indices[i] = indices[j] = min(i_i, i_j)\n                    changed = True\n            if not changed:\n                break\n\n        self = self.with_column(polars.Series('_unique_pts', indices))\n        cols.add('_unique_pts')\n\n    frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n    if len(spatial_cols) > 0:\n        frame = frame.drop('_unique_pts')\n\n    return self.with_atoms(Atoms(frame, _unchecked=True))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_bounds","title":"with_bounds","text":"
    with_bounds(\n    cell_size: Optional[VecLike] = None,\n    cell_origin: Optional[VecLike] = None,\n) -> \"AtomCell\"\n

    Return a periodic cell with the given orthogonal cell dimensions.

    If cell_size is not specified, it will be assumed (and may be incorrect).

    Source code in atomlib/atoms.py
    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n    \"\"\"\n    Return a periodic cell with the given orthogonal cell dimensions.\n\n    If cell_size is not specified, it will be assumed (and may be incorrect).\n    \"\"\"\n    # TODO: test this\n    from .atomcell import AtomCell\n\n    if cell_size is None:\n        warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n        cell_size = self.bbox().size\n        cell_origin = self.bbox().min\n\n    # TODO test this origin code\n    cell = Cell.from_unit_cell(cell_size)\n    if cell_origin is not None:\n        cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n    return AtomCell(self.get_atoms(), cell, frame='local')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.coords","title":"coords","text":"
    coords(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> NDArray[float64]\n

    Return a (N, 3) ndarray of atom positions (dtype numpy.float64) in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> NDArray[numpy.float64]:\n    \"\"\"\n    Return a `(N, 3)` ndarray of atom positions (dtype [`numpy.float64`][numpy.float64])\n    in the given coordinate frame.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.x","title":"x","text":"
    x() -> Expr\n
    Source code in atomlib/atoms.py
    def x(self) -> polars.Expr:\n    return polars.col('coords').arr.get(0).alias('x')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.y","title":"y","text":"
    y() -> Expr\n
    Source code in atomlib/atoms.py
    def y(self) -> polars.Expr:\n    return polars.col('coords').arr.get(1).alias('y')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.z","title":"z","text":"
    z() -> Expr\n
    Source code in atomlib/atoms.py
    def z(self) -> polars.Expr:\n    return polars.col('coords').arr.get(2).alias('z')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.velocities","title":"velocities","text":"
    velocities(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Optional[NDArray[float64]]\n

    Return a (N, 3) ndarray of atom velocities (dtype numpy.float64) in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef velocities(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> t.Optional[NDArray[numpy.float64]]:\n    \"\"\"\n    Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64])\n    in the given coordinate frame.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.types","title":"types","text":"
    types() -> Optional[Series]\n

    Returns a Series of atom types (dtype polars.Int32).

    Source code in atomlib/atoms.py
    def types(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('type')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.masses","title":"masses","text":"
    masses() -> Optional[Series]\n

    Returns a Series of atom masses (dtype polars.Float32).

    Source code in atomlib/atoms.py
    def masses(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('mass')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.add_atom","title":"add_atom","text":"
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: Union[ArrayLike, float],\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None,\n    **kwargs: Any,\n) -> Self\n

    Return a copy of self with an extra atom.

    By default, all extra columns present in self must be specified as **kwargs.

    Try to avoid calling this in a loop (Use concat instead).

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef add_atom(self, elem: t.Union[int, str], /,  # type: ignore (spurious)\n             x: t.Union[ArrayLike, float],\n             y: t.Optional[float] = None,\n             z: t.Optional[float] = None, *,\n             frame: t.Optional[CoordinateFrame] = None,\n             **kwargs: t.Any) -> Self:\n    \"\"\"\n    Return a copy of `self` with an extra atom.\n\n    By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n    Try to avoid calling this in a loop (Use [`concat`][atomlib.atomcell.HasAtomCell.concat] instead).\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.pos","title":"pos","text":"
    pos(\n    x: Union[Sequence[Optional[float]], float, None] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n

    Select all atoms at a given position.

    Formally, returns all atoms within a cube of radius tol centered at (x,y,z), exclusive of the cube's surface.

    Additional parameters given as kwargs will be checked as additional parameters (with strict equality).

    Source code in atomlib/atoms.py
    def pos(self,\n        x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n        y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n        tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n    \"\"\"\n    Select all atoms at a given position.\n\n    Formally, returns all atoms within a cube of radius ``tol``\n    centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n    Additional parameters given as ``kwargs`` will be checked\n    as additional parameters (with strict equality).\n    \"\"\"\n\n    if isinstance(x, t.Sequence):\n        (x, y, z) = x\n\n    tol = abs(float(tol))\n    selection = polars.lit(True)\n    if x is not None:\n        selection &= self.x().is_between(x - tol, x + tol, closed='none')\n    if y is not None:\n        selection &= self.y().is_between(y - tol, y + tol, closed='none')\n    if z is not None:\n        selection &= self.z().is_between(z - tol, z + tol, closed='none')\n    for (col, val) in kwargs.items():\n        selection &= (polars.col(col) == val)\n\n    return selection\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_index","title":"with_index","text":"
    with_index(\n    index: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Returns self with a row index added in column 'i' (dtype polars.Int64). If index is not specified, defaults to an existing index or a new index.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_index(self, index: t.Optional[AtomValues] = None, *,\n               frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n    If `index` is not specified, defaults to an existing index or a new index.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_wobble","title":"with_wobble","text":"
    with_wobble(\n    wobble: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given displacements in column 'wobble' (dtype polars.Float64). If wobble is not specified, defaults to the already-existing wobbles or 0.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_wobble(self, wobble: t.Optional[AtomValues] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_occupancy","title":"with_occupancy","text":"
    with_occupancy(\n    frac_occupancy: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given fractional occupancies (dtype polars.Float64). If frac_occupancy is not specified, defaults to the already-existing occupancies or 1.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None, *,\n                   frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.apply_wobble","title":"apply_wobble","text":"
    apply_wobble(\n    rng: Union[Generator, int, None] = None,\n    frame: Optional[CoordinateFrame] = None,\n) -> Self\n

    Displace the atoms in self by the amount in the wobble column. wobble is interpretated as a mean-squared displacement, which is distributed equally over each axis.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None,\n                 frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Displace the atoms in `self` by the amount in the `wobble` column.\n    `wobble` is interpretated as a mean-squared displacement, which is distributed\n    equally over each axis.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.apply_occupancy","title":"apply_occupancy","text":"
    apply_occupancy(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    For each atom in self, use its frac_occupancy to randomly decide whether to remove it.

    Source code in atomlib/atoms.py
    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n    \"\"\"\n    if 'frac_occupancy' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    frac = self.select('frac_occupancy').to_series().to_numpy()\n    choice = rng.binomial(1, frac).astype(numpy.bool_)\n    return self.filter(polars.lit(choice))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_type","title":"with_type","text":"
    with_type(\n    types: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atom types in column 'type'. If types is not specified, use the already existing types or auto-assign them.

    When auto-assigning, each symbol is given a unique value, case-sensitive. Values are assigned from lowest atomic number to highest. For instance: [\"Ag+\", \"Na\", \"H\", \"Ag\"] => [3, 11, 1, 2]

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_type(self, types: t.Optional[AtomValues] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom types in column 'type'.\n    If `types` is not specified, use the already existing types or auto-assign them.\n\n    When auto-assigning, each symbol is given a unique value, case-sensitive.\n    Values are assigned from lowest atomic number to highest.\n    For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_mass","title":"with_mass","text":"
    with_mass(\n    mass: Optional[ArrayLike] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atom masses in column 'mass'. If mass is not specified, use the already existing masses or auto-assign them.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_mass(self, mass: t.Optional[ArrayLike] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom masses in column `'mass'`.\n    If `mass` is not specified, use the already existing masses or auto-assign them.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_symbol","title":"with_symbol","text":"
    with_symbol(\n    symbols: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atomic symbols.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atomic symbols.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_coords","title":"with_coords","text":"
    with_coords(\n    pts: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self replaced with the given atomic positions.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic positions.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_velocity","title":"with_velocity","text":"
    with_velocity(\n    pts: Optional[ArrayLike] = None,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self replaced with the given atomic velocities. If pts is not specified, use the already existing velocities or zero.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                  selection: t.Optional[AtomSelection] = None, *,\n                  frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic velocities.\n    If `pts` is not specified, use the already existing velocities or zero.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.get_frame","title":"get_frame","text":"
    get_frame() -> CoordinateFrame\n

    Get the coordinate frame atoms are stored in.

    Source code in atomlib/atomcell.py
    def get_frame(self) -> CoordinateFrame:\n    \"\"\"Get the coordinate frame atoms are stored in.\"\"\"\n    return self.frame\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.get_atomcell","title":"get_atomcell","text":"
    get_atomcell() -> AtomCell\n
    Source code in atomlib/atomcell.py
    def get_atomcell(self) -> AtomCell:\n    frame = self.get_frame()\n    return AtomCell(self.get_atoms(frame), self.get_cell(), frame=frame, keep_frame=True)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.to_frame","title":"to_frame","text":"
    to_frame(frame: CoordinateFrame) -> Self\n

    Convert the stored Atoms to the given coordinate frame.

    Source code in atomlib/atomcell.py
    def to_frame(self, frame: CoordinateFrame) -> Self:\n    \"\"\"Convert the stored Atoms to the given coordinate frame.\"\"\"\n    return self.with_atoms(self.get_atoms(frame), frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.crop_to_box","title":"crop_to_box","text":"
    crop_to_box(eps: float = 1e-05) -> Self\n
    Source code in atomlib/atomcell.py
    def crop_to_box(self, eps: float = 1e-5) -> Self:\n    atoms = self._transform_atoms_in_frame('cell_box', lambda atoms: atoms.crop_atoms(*([-eps, 1-eps]*3)))\n    return self.with_atoms(atoms)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.wrap","title":"wrap","text":"
    wrap(eps: float = 1e-05) -> Self\n

    Wrap atoms around the cell boundaries.

    Source code in atomlib/atomcell.py
    def wrap(self, eps: float = 1e-5) -> Self:\n    \"\"\"Wrap atoms around the cell boundaries.\"\"\"\n    return self.with_atoms(self._transform_atoms_in_frame('cell_box', lambda a: a._wrap(eps)))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.repeat_to","title":"repeat_to","text":"
    repeat_to(\n    size: VecLike, crop: Union[bool, Sequence[bool]] = False\n) -> Self\n

    Repeat the cell so it is at least size along the crystal's axes.

    If crop, then crop the cell to exactly size. This may break periodicity. crop may be a vector, in which case you can specify cropping only along some axes.

    Source code in atomlib/atomcell.py
    def repeat_to(self, size: VecLike, crop: t.Union[bool, t.Sequence[bool]] = False) -> Self:\n    \"\"\"\n    Repeat the cell so it is at least `size` along the crystal's axes.\n\n    If `crop`, then crop the cell to exactly `size`. This may break periodicity.\n    `crop` may be a vector, in which case you can specify cropping only along some axes.\n    \"\"\"\n    size = to_vec3(size)\n    cell_size = self.cell_size * self.n_cells\n    repeat = numpy.maximum(numpy.ceil(size / cell_size).astype(int), 1)\n    atom_cell = self.repeat(repeat)\n\n    crop_v = to_vec3(crop, dtype=numpy.bool_)\n    if numpy.any(crop_v):\n        crop_x, crop_y, crop_z = crop_v\n        return atom_cell.crop(\n            x_max = size[0] if crop_x else numpy.inf,\n            y_max = size[1] if crop_y else numpy.inf,\n            z_max = size[2] if crop_z else numpy.inf,\n            frame='cell'\n        )\n\n    return atom_cell\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.repeat_x","title":"repeat_x","text":"
    repeat_x(n: int) -> Self\n

    Tile the cell in the x axis.

    Source code in atomlib/atomcell.py
    def repeat_x(self, n: int) -> Self:\n    \"\"\"Tile the cell in the x axis.\"\"\"\n    return self.repeat((n, 1, 1))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.repeat_y","title":"repeat_y","text":"
    repeat_y(n: int) -> Self\n

    Tile the cell in the y axis.

    Source code in atomlib/atomcell.py
    def repeat_y(self, n: int) -> Self:\n    \"\"\"Tile the cell in the y axis.\"\"\"\n    return self.repeat((1, n, 1))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.repeat_z","title":"repeat_z","text":"
    repeat_z(n: int) -> Self\n

    Tile the cell in the z axis.

    Source code in atomlib/atomcell.py
    def repeat_z(self, n: int) -> Self:\n    \"\"\"Tile the cell in the z axis.\"\"\"\n    return self.repeat((1, 1, n))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.repeat_to_x","title":"repeat_to_x","text":"
    repeat_to_x(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the x axis.

    Source code in atomlib/atomcell.py
    def repeat_to_x(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the x axis.\"\"\"\n    return self.repeat_to([size, 0., 0.], [crop, False, False])\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.repeat_to_y","title":"repeat_to_y","text":"
    repeat_to_y(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the y axis.

    Source code in atomlib/atomcell.py
    def repeat_to_y(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the y axis.\"\"\"\n    return self.repeat_to([0., size, 0.], [False, crop, False])\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.repeat_to_z","title":"repeat_to_z","text":"
    repeat_to_z(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the z axis.

    Source code in atomlib/atomcell.py
    def repeat_to_z(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the z axis.\"\"\"\n    return self.repeat_to([0., 0., size], [False, False, crop])\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.repeat_to_aspect","title":"repeat_to_aspect","text":"
    repeat_to_aspect(\n    plane: Literal[\"xy\", \"xz\", \"yz\"] = \"xy\",\n    *,\n    aspect: float = 1.0,\n    min_size: Optional[VecLike] = None,\n    max_size: Optional[VecLike] = None\n) -> Self\n

    Repeat to optimize the aspect ratio in plane, while staying above min_size and under max_size.

    Source code in atomlib/atomcell.py
    def repeat_to_aspect(self, plane: t.Literal['xy', 'xz', 'yz'] = 'xy', *,\n                     aspect: float = 1., min_size: t.Optional[VecLike] = None,\n                     max_size: t.Optional[VecLike] = None) -> Self:\n    \"\"\"\n    Repeat to optimize the aspect ratio in `plane`,\n    while staying above `min_size` and under `max_size`.\n    \"\"\"\n    if min_size is None:\n        min_n = numpy.array([1, 1, 1], numpy.int_)\n    else:\n        min_n = numpy.maximum(numpy.ceil(to_vec3(min_size) / self.box_size), 1).astype(numpy.int_)\n\n    if max_size is None:\n        max_n = 3 * min_n\n    else:\n        max_n = numpy.maximum(numpy.floor(to_vec3(max_size) / self.box_size), 1).astype(numpy.int_)\n\n    if plane == 'xy':\n        indices = [0, 1]\n    elif plane == 'xz':\n        indices = [0, 2]\n    elif plane == 'yz':\n        indices = [1, 2]\n    else:\n        raise ValueError(f\"Invalid plane '{plane}'. Exepcted 'xy', 'xz', 'or 'yz'.\")\n\n    na = numpy.arange(min_n[indices[0]], max_n[indices[0]])\n    nb = numpy.arange(min_n[indices[1]], max_n[indices[1]])\n    (na, nb) = numpy.meshgrid(na, nb)\n\n    aspects = na * self.box_size[indices[0]] / (nb * self.box_size[indices[1]])\n    # cost function: log(aspect)^2  (so cost(0.5) == cost(2))\n    min_i = numpy.argmin(numpy.log(aspects / aspect)**2)\n    repeat = numpy.array([1, 1, 1], numpy.int_)\n    repeat[indices] = na.flatten()[min_i], nb.flatten()[min_i]\n    return self.repeat(repeat)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.periodic_duplicate","title":"periodic_duplicate","text":"
    periodic_duplicate(eps: float = 1e-05) -> Self\n

    Add duplicate copies of atoms near periodic boundaries.

    For instance, an atom at a corner will be duplicated into 8 copies. This is mostly only useful for visualization.

    Source code in atomlib/atomcell.py
    def periodic_duplicate(self, eps: float = 1e-5) -> Self:\n    \"\"\"\n    Add duplicate copies of atoms near periodic boundaries.\n\n    For instance, an atom at a corner will be duplicated into 8 copies.\n    This is mostly only useful for visualization.\n    \"\"\"\n    frame_save = self.get_frame()\n    self = self.to_frame('cell_box').wrap(eps=eps)\n\n    for i in range(3):\n        self = self.concat((self,\n            self.filter(polars.col('coords').arr.get(i).abs() <= eps, frame='cell_box')\n                .transform_atoms(AffineTransform3D.translate([1. if i == j else 0. for j in range(3)]), frame='cell_box')\n        ))\n\n    return self.to_frame(frame_save)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.read","title":"read classmethod","text":"
    read(\n    path: FileOrPath, ty: Optional[FileType] = None\n) -> HasAtomsT\n

    Read a structure from a file.

    Supported types can be found in the io module. If no ty is specified, it is inferred from the file's extension.

    Source code in atomlib/mixins.py
    @classmethod\ndef read(cls: t.Type[HasAtomsT], path: FileOrPath, ty: t.Optional[FileType] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a file.\n\n    Supported types can be found in the [io][atomlib.io] module.\n    If no `ty` is specified, it is inferred from the file's extension.\n    \"\"\"\n    from .io import read\n    return _cast_atoms(read(path, ty), cls)  # type: ignore\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.read_cif","title":"read_cif classmethod","text":"
    read_cif(\n    f: Union[FileOrPath, CIF, CIFDataBlock],\n    block: Union[int, str, None] = None,\n) -> HasAtomsT\n

    Read a structure from a CIF file.

    If block is specified, read data from the given block of the CIF file (index or name).

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cif(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CIF, CIFDataBlock], block: t.Union[int, str, None] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a CIF file.\n\n    If `block` is specified, read data from the given block of the CIF file (index or name).\n    \"\"\"\n    from .io import read_cif\n    return _cast_atoms(read_cif(f, block), cls)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.read_xyz","title":"read_xyz classmethod","text":"
    read_xyz(f: Union[FileOrPath, XYZ]) -> HasAtomsT\n

    Read a structure from an XYZ file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xyz(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XYZ]) -> HasAtomsT:\n    \"\"\"Read a structure from an XYZ file.\"\"\"\n    from .io import read_xyz\n    return _cast_atoms(read_xyz(f), cls)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.read_xsf","title":"read_xsf classmethod","text":"
    read_xsf(f: Union[FileOrPath, XSF]) -> HasAtomsT\n

    Read a structure from an XSF file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xsf(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XSF]) -> HasAtomsT:\n    \"\"\"Read a structure from an XSF file.\"\"\"\n    from .io import read_xsf\n    return _cast_atoms(read_xsf(f), cls)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.read_cfg","title":"read_cfg classmethod","text":"
    read_cfg(f: Union[FileOrPath, CFG]) -> HasAtomsT\n

    Read a structure from a CFG file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cfg(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CFG]) -> HasAtomsT:\n    \"\"\"Read a structure from a CFG file.\"\"\"\n    from .io import read_cfg\n    return _cast_atoms(read_cfg(f), cls)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.read_lmp","title":"read_lmp classmethod","text":"
    read_lmp(\n    f: Union[FileOrPath, LMP],\n    type_map: Optional[Dict[int, Union[str, int]]] = None,\n) -> HasAtomsT\n

    Read a structure from a LAAMPS data file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_lmp(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, LMP], type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> HasAtomsT:\n    \"\"\"Read a structure from a LAAMPS data file.\"\"\"\n    from .io import read_lmp\n    return _cast_atoms(read_lmp(f, type_map=type_map), cls)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.write_cif","title":"write_cif","text":"
    write_cif(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cif(self, f: FileOrPath):\n    from .io import write_cif\n    write_cif(self, f)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.write_xyz","title":"write_xyz","text":"
    write_xyz(f: FileOrPath, fmt: XYZFormat = 'exyz')\n
    Source code in atomlib/mixins.py
    def write_xyz(self, f: FileOrPath, fmt: XYZFormat = 'exyz'):\n    from .io import write_xyz\n    write_xyz(self, f, fmt)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.write_xsf","title":"write_xsf","text":"
    write_xsf(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_xsf(self, f: FileOrPath):\n    from .io import write_xsf\n    write_xsf(self, f)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.write_cfg","title":"write_cfg","text":"
    write_cfg(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cfg(self, f: FileOrPath):\n    from .io import write_cfg\n    write_cfg(self, f)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.write_lmp","title":"write_lmp","text":"
    write_lmp(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_lmp(self, f: FileOrPath):\n    from .io import write_lmp\n    write_lmp(self, f)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.write","title":"write","text":"
    write(path: FileOrPath, ty: Optional[FileType] = None)\n

    Write this structure to a file.

    A file type may be specified using ty. If no ty is specified, it is inferred from the path's extension.

    Source code in atomlib/mixins.py
    def write(self, path: FileOrPath, ty: t.Optional[FileType] = None):\n    \"\"\"\n    Write this structure to a file.\n\n    A file type may be specified using `ty`.\n    If no `ty` is specified, it is inferred from the path's extension.\n    \"\"\"\n    from .io import write\n    write(self, path, ty)  # type: ignore\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.write_mslice","title":"write_mslice","text":"
    write_mslice(\n    f: BinaryFileOrPath,\n    template: Optional[MSliceFile] = None,\n    *,\n    slice_thickness: Optional[float] = None,\n    scan_points: Optional[ArrayLike] = None,\n    scan_extent: Optional[ArrayLike] = None,\n    noise_sigma: Optional[float] = None,\n    conv_angle: Optional[float] = None,\n    energy: Optional[float] = None,\n    defocus: Optional[float] = None,\n    tilt: Optional[Tuple[float, float]] = None,\n    tds: Optional[bool] = None,\n    n_cells: Optional[ArrayLike] = None\n)\n

    Write a structure to an mslice file.

    template may be a file, path, or ElementTree containing an existing mslice file. Its structure will be modified to make the final output. If not specified, a default template will be used.

    Additional options modify simulation properties. If an option is not specified, the template's properties are used.

    Source code in atomlib/mixins.py
    def write_mslice(self, f: BinaryFileOrPath, template: t.Optional[MSliceFile] = None, *,\n             slice_thickness: t.Optional[float] = None,  # angstrom\n             scan_points: t.Optional[ArrayLike] = None,\n             scan_extent: t.Optional[ArrayLike] = None,\n             noise_sigma: t.Optional[float] = None,  # angstrom\n             conv_angle: t.Optional[float] = None,  # mrad\n             energy: t.Optional[float] = None,  # keV\n             defocus: t.Optional[float] = None,  # angstrom\n             tilt: t.Optional[t.Tuple[float, float]] = None,  # (mrad, mrad)\n             tds: t.Optional[bool] = None,\n             n_cells: t.Optional[ArrayLike] = None):\n    \"\"\"\n    Write a structure to an mslice file.\n\n    `template` may be a file, path, or `ElementTree` containing an existing mslice file.\n    Its structure will be modified to make the final output. If not specified, a default\n    template will be used.\n\n    Additional options modify simulation properties. If an option is not specified, the\n    template's properties are used.\n    \"\"\"\n    from .io import write_mslice\n    return write_mslice(self, f, template, slice_thickness=slice_thickness,\n                        scan_points=scan_points, scan_extent=scan_extent,\n                        conv_angle=conv_angle, energy=energy, defocus=defocus,\n                        noise_sigma=noise_sigma, tilt=tilt, tds=tds, n_cells=n_cells)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.write_qe","title":"write_qe","text":"
    write_qe(\n    f: FileOrPath,\n    pseudo: Optional[Mapping[str, str]] = None,\n)\n

    Write a structure to a Quantum Espresso pw.x file.

    PARAMETER DESCRIPTION f

    File or path to write to

    TYPE: FileOrPath

    pseudo

    Mapping from atom symbol

    TYPE: Optional[Mapping[str, str]] DEFAULT: None

    Source code in atomlib/mixins.py
    def write_qe(self, f: FileOrPath, pseudo: t.Optional[t.Mapping[str, str]] = None):\n    \"\"\"\n    Write a structure to a Quantum Espresso pw.x file.\n\n    Args:\n      f: File or path to write to\n      pseudo: Mapping from atom symbol\n    \"\"\"\n    from .io import write_qe\n    write_qe(self, f, pseudo)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.from_ortho","title":"from_ortho classmethod","text":"
    from_ortho(\n    atoms: IntoAtoms,\n    ortho: LinearTransform3D,\n    *,\n    n_cells: Optional[VecLike] = None,\n    frame: CoordinateFrame = \"local\",\n    keep_frame: bool = False\n)\n

    Make an atom cell given a list of atoms and an orthogonalization matrix. Atoms are assumed to be in the coordinate system frame.

    Source code in atomlib/atomcell.py
    @classmethod\ndef from_ortho(cls, atoms: IntoAtoms, ortho: LinearTransform3D, *,\n               n_cells: t.Optional[VecLike] = None,\n               frame: CoordinateFrame = 'local',\n               keep_frame: bool = False):\n    \"\"\"\n    Make an atom cell given a list of atoms and an orthogonalization matrix.\n    Atoms are assumed to be in the coordinate system `frame`.\n    \"\"\"\n    cell = Cell.from_ortho(ortho, n_cells)\n    return cls(atoms, cell, frame=frame, keep_frame=keep_frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.from_unit_cell","title":"from_unit_cell classmethod","text":"
    from_unit_cell(\n    atoms: IntoAtoms,\n    cell_size: VecLike,\n    cell_angle: Optional[VecLike] = None,\n    *,\n    n_cells: Optional[VecLike] = None,\n    frame: CoordinateFrame = \"local\",\n    keep_frame: bool = False\n)\n

    Make a cell given a list of atoms and unit cell parameters. Atoms are assumed to be in the coordinate system frame.

    Source code in atomlib/atomcell.py
    @classmethod\ndef from_unit_cell(cls, atoms: IntoAtoms, cell_size: VecLike,\n                   cell_angle: t.Optional[VecLike] = None, *,\n                   n_cells: t.Optional[VecLike] = None,\n                   frame: CoordinateFrame = 'local',\n                   keep_frame: bool = False):\n    \"\"\"\n    Make a cell given a list of atoms and unit cell parameters.\n    Atoms are assumed to be in the coordinate system `frame`.\n    \"\"\"\n    cell = Cell.from_unit_cell(cell_size, cell_angle, n_cells=n_cells)\n    return cls(atoms, cell, frame=frame, keep_frame=keep_frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.orthogonalize","title":"orthogonalize","text":"
    orthogonalize() -> OrthoCell\n
    Source code in atomlib/atomcell.py
    def orthogonalize(self) -> OrthoCell:\n    if self.is_orthogonal():\n        return OrthoCell(self.atoms, self.cell, frame=self.frame)\n    raise NotImplementedError()\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.is_orthogonal","title":"is_orthogonal","text":"
    is_orthogonal(tol: float = 1e-08) -> Literal[True]\n

    Returns whether this cell is orthogonal (axes are at right angles.)

    Source code in atomlib/atomcell.py
    def is_orthogonal(self, tol: float = 1e-8) -> t.Literal[True]:\n    \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n    return True\n
    "},{"location":"api/atoms/","title":"atomlib.atoms","text":"

    Raw atoms collection

    This module defines HasAtoms and the concrete Atoms, which holds a collection of atoms with no cell or periodicity. Atoms is essentially a wrapper around a polars.DataFrame.

    "},{"location":"api/atoms/#atomlib.atoms.SchemaDict","title":"SchemaDict module-attribute","text":"
    SchemaDict: TypeAlias = OrderedDict[str, DataType]\n
    "},{"location":"api/atoms/#atomlib.atoms.IntoExprColumn","title":"IntoExprColumn module-attribute","text":"
    IntoExprColumn: TypeAlias = IntoExprColumn\n
    "},{"location":"api/atoms/#atomlib.atoms.IntoExpr","title":"IntoExpr module-attribute","text":"
    IntoExpr: TypeAlias = IntoExpr\n
    "},{"location":"api/atoms/#atomlib.atoms.UniqueKeepStrategy","title":"UniqueKeepStrategy module-attribute","text":"
    UniqueKeepStrategy: TypeAlias = UniqueKeepStrategy\n
    "},{"location":"api/atoms/#atomlib.atoms.FillNullStrategy","title":"FillNullStrategy module-attribute","text":"
    FillNullStrategy: TypeAlias = FillNullStrategy\n
    "},{"location":"api/atoms/#atomlib.atoms.RollingInterpolationMethod","title":"RollingInterpolationMethod module-attribute","text":"
    RollingInterpolationMethod: TypeAlias = (\n    RollingInterpolationMethod\n)\n
    "},{"location":"api/atoms/#atomlib.atoms.ConcatMethod","title":"ConcatMethod module-attribute","text":"
    ConcatMethod: TypeAlias = Literal[\n    \"horizontal\", \"vertical\", \"diagonal\", \"inner\", \"align\"\n]\n
    "},{"location":"api/atoms/#atomlib.atoms.IntoAtoms","title":"IntoAtoms module-attribute","text":"
    IntoAtoms: TypeAlias = Union[\n    Dict[str, Sequence[Any]],\n    Sequence[Any],\n    ndarray,\n    DataFrame,\n    \"Atoms\",\n]\n

    A type convertible into an Atoms.

    "},{"location":"api/atoms/#atomlib.atoms.AtomSelection","title":"AtomSelection module-attribute","text":"
    AtomSelection: TypeAlias = Union[\n    IntoExprColumn,\n    NDArray[bool_],\n    ArrayLike,\n    Mapping[str, Any],\n]\n

    Polars expression selecting a subset of atoms. Can be used with many Atoms methods.

    "},{"location":"api/atoms/#atomlib.atoms.AtomValues","title":"AtomValues module-attribute","text":"
    AtomValues: TypeAlias = Union[\n    IntoExprColumn,\n    NDArray[generic],\n    ArrayLike,\n    Mapping[str, Any],\n]\n

    Array, value, or polars expression mapping atom symbols to values. Can be used with with_* methods on Atoms

    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms","title":"HasAtoms","text":"

    Bases: ABC

    Abstract class representing any (possibly compound) collection of atoms.

    Source code in atomlib/atoms.py
    class HasAtoms(abc.ABC):\n    \"\"\"Abstract class representing any (possibly compound) collection of atoms.\"\"\"\n\n    # abstract methods\n\n    @abc.abstractmethod\n    def get_atoms(self, frame: t.Literal['local'] = 'local') -> Atoms:\n        \"\"\"\n        Get atoms contained in `self`. This should be a low cost method.\n\n        Args:\n          frame: Coordinate frame to return atoms in. For a plain [`HasAtoms`][atomlib.atoms.HasAtoms],\n                 only `'local'` is supported.\n\n        Return:\n          The contained atoms\n        \"\"\"\n        ...\n\n    @abc.abstractmethod\n    def with_atoms(self, atoms: HasAtoms, frame: t.Literal['local'] = 'local') -> Self:\n        \"\"\"\n        Return a copy of self with the inner [`Atoms`][atomlib.atoms.Atoms] replaced.\n\n        Args:\n          atoms: [`HasAtoms`][atomlib.atoms.HasAtoms] to replace these with.\n          frame: Coordinate frame inside atoms are in. For a plain [`HasAtoms`][atomlib.atoms.HasAtoms],\n                 only `'local'` is supported.\n\n        Return:\n          A copy of `self` updated with the given atoms\n        \"\"\"\n        ...\n\n    @classmethod\n    @abc.abstractmethod\n    def _combine_metadata(cls: t.Type[HasAtomsT], *atoms: HasAtoms) -> HasAtomsT:\n        \"\"\"\n        When combining multiple `HasAtoms`, check that they are compatible with each other,\n        and return a 'representative' which best represents the combined metadata.\n        Implementors should treat `Atoms` as acceptable, but having no metadata.\n        \"\"\"\n        ...\n\n    def _get_frame(self) -> polars.DataFrame:\n        return self.get_atoms().inner\n\n    # dataframe methods\n\n    @property\n    @_fwd_frame(lambda df: df.columns)\n    def columns(self) -> t.List[str]:\n        \"\"\"\n        Return the column names in `self`.\n\n        Returns:\n          A sequence of column names\n        \"\"\"\n        ...\n\n    @property\n    @_fwd_frame(lambda df: df.dtypes)\n    def dtypes(self) -> t.List[polars.DataType]:\n        \"\"\"\n        Return the datatypes in `self`.\n\n        Returns:\n          A sequence of column [`DataType`][polars.datatypes.DataType]s\n        \"\"\"\n        ...\n\n    @property\n    @_fwd_frame(lambda df: df.schema)  # type: ignore\n    def schema(self) -> Schema:\n        \"\"\"\n        Return the schema of `self`.\n\n        Returns:\n          A dictionary of column names and [`DataType`][polars.datatypes.DataType]s\n        \"\"\"\n        ...\n\n    @_fwd_frame(polars.DataFrame.describe)\n    def describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n                 interpolation: RollingInterpolationMethod = 'nearest') -> polars.DataFrame:\n        \"\"\"\n        Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n        Args:\n          percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                       50% (median), and 75% (third quartile).\n\n        Returns:\n          A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n        \"\"\"\n        ...\n\n    @_fwd_frame_map\n    def with_columns(self,\n                     *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                     **named_exprs: IntoExpr) -> polars.DataFrame:\n        \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n        return self._get_frame().with_columns(*exprs, **named_exprs)\n\n    with_column = with_columns\n\n    @_fwd_frame_map\n    def insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n        return self._get_frame().insert_column(index, column)\n\n    @_fwd_frame(lambda df, name: df.get_column(name))\n    def get_column(self, name: str) -> polars.Series:\n        \"\"\"\n        Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        ...\n\n    @_fwd_frame(polars.DataFrame.get_columns)\n    def get_columns(self) -> t.List[polars.Series]:\n        \"\"\"\n        Return all columns from `self` as a list of [`Series`][polars.Series].\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        ...\n\n    @_fwd_frame(polars.DataFrame.get_column_index)\n    def get_column_index(self, name: str) -> int:\n        \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n        ...\n\n    @_fwd_frame(polars.DataFrame.group_by)\n    def group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]], maintain_order: bool = False,\n                 **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n        \"\"\"\n        Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n        \"\"\"\n        ...\n\n    def pipe(self: HasAtomsT, function: t.Callable[Concatenate[HasAtomsT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n        \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n        return function(self, *args, **kwargs)\n\n    @_fwd_frame_map\n    def clone(self) -> polars.DataFrame:\n        \"\"\"Return a copy of `self`.\"\"\"\n        return self._get_frame().clone()\n\n    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n        \"\"\"Return `self` with the specified columns removed.\"\"\"\n        return self._get_frame().drop(*columns, strict=strict)\n\n    # row-wise operations\n\n    def filter(\n        self,\n        *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n        **constraints: t.Any,\n    ) -> Self:\n        \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n        # TODO clean up\n        preds_not_none = tuple(filter(lambda p: p is not None, predicates))\n        if not len(preds_not_none) and not len(constraints):\n            return self\n        return self.with_atoms(Atoms(self._get_frame().filter(*preds_not_none, **constraints), _unchecked=True))  # type: ignore\n\n    @_fwd_frame_map\n    def sort(\n        self,\n        by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        *more_by: IntoExpr,\n        descending: t.Union[bool, t.Sequence[bool]] = False,\n        nulls_last: bool = False,\n    ) -> polars.DataFrame:\n        \"\"\"\n        Sort the atoms in `self` by the given columns/expressions.\n        \"\"\"\n        return self._get_frame().sort(\n            by, *more_by, descending=descending, nulls_last=nulls_last\n        )\n\n    @_fwd_frame_map\n    def slice(self, offset: int, length: t.Optional[int] = None) -> polars.DataFrame:\n        \"\"\"Return a slice of the rows in `self`.\"\"\"\n        return self._get_frame().slice(offset, length)\n\n    @_fwd_frame_map\n    def head(self, n: int = 5) -> polars.DataFrame:\n        \"\"\"Return the first `n` rows of `self`.\"\"\"\n        return self._get_frame().head(n)\n\n    @_fwd_frame_map\n    def tail(self, n: int = 5) -> polars.DataFrame:\n        \"\"\"Return the last `n` rows of `self`.\"\"\"\n        return self._get_frame().tail(n)\n\n    @_fwd_frame_map\n    def drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n        \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n        return self._get_frame().drop_nulls(subset)\n\n    @_fwd_frame_map\n    def fill_null(\n        self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n        limit: t.Optional[int] = None, matches_supertype: bool = True,\n    ) -> polars.DataFrame:\n        \"\"\"Fill null values in `self`, using the specified value or strategy.\"\"\"\n        return self._get_frame().fill_null(value, strategy, limit, matches_supertype=matches_supertype)\n\n    @_fwd_frame_map\n    def fill_nan(self, value: t.Union[polars.Expr, int, float, None]) -> polars.DataFrame:\n        \"\"\"Fill floating-point NaN values in `self`.\"\"\"\n        return self._get_frame().fill_nan(value)\n\n    @classmethod\n    def concat(cls: t.Type[HasAtomsT],\n               atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n               rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n        \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n        # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n        # same type as ``cls``.\n        if _is_abstract(cls):\n            raise TypeError(\"concat() must be called on a concrete class.\")\n\n        if isinstance(atoms, HasAtoms):\n            atoms = (atoms,)\n        dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n        representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n        if len(dfs) == 0:\n            return representative.with_atoms(Atoms.empty(), 'local')\n\n        if how in ('vertical', 'vertical_relaxed'):\n            # get order from first member\n            cols = dfs[0].columns\n            dfs = [df.select(cols) for df in dfs]\n        elif how == 'inner':\n            cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n            schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n            if len(schema) == 0:\n                raise ValueError(\"Atoms have no columns in common\")\n\n            dfs = [_select_schema(df, schema) for df in dfs]\n            how = 'vertical'\n\n        return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n\n    @t.overload\n    def partition_by(\n        self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n        maintain_order: bool = True, include_key: bool = True, as_dict: t.Literal[False] = False\n    ) -> t.List[Self]:\n        ...\n\n    @t.overload\n    def partition_by(\n        self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n        maintain_order: bool = True, include_key: bool = True, as_dict: t.Literal[True] = ...\n    ) -> t.Dict[t.Any, Self]:\n        ...\n\n    def partition_by(\n        self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n        maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n    ) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n        \"\"\"\n        Group by the given columns and partition into separate dataframes.\n\n        Return the partitions as a dictionary by specifying `as_dict=True`.\n        \"\"\"\n        if as_dict:\n            d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n            return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n        return [\n            self.with_atoms(Atoms(df, _unchecked=True))\n            for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n        ]\n\n    # column-wise operations\n\n    @_fwd_frame(polars.DataFrame.select)\n    def select(\n        self,\n        *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        **named_exprs: IntoExpr,\n    ) -> polars.DataFrame:\n        \"\"\"\n        Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n        Expressions may either be columns or expressions of columns.\n\n        [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n        \"\"\"\n        ...\n\n    # some helpers we add\n\n    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n        \"\"\"\n        Select columns from `self` and cast to the given schema.\n        Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n        \"\"\"\n        return _select_schema(self, schema)\n\n    def select_props(\n        self,\n        *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        **named_exprs: IntoExpr\n    ) -> Self:\n        \"\"\"\n        Select `exprs` from `self`, while keeping required columns.\n\n        Returns:\n          A [`HasAtoms`][atomlib.atoms.HasAtoms] filtered to contain the\n          specified properties (as well as required columns).\n        \"\"\"\n        props = self._get_frame().lazy().select(*exprs, **named_exprs).drop(_REQUIRED_COLUMNS, strict=False).collect(_eager=True)\n        return self.with_atoms(\n            Atoms(self._get_frame().select(_REQUIRED_COLUMNS).hstack(props), _unchecked=False)\n        )\n\n    def try_select(\n        self,\n        *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        **named_exprs: IntoExpr,\n    ) -> t.Optional[polars.DataFrame]:\n        \"\"\"\n        Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n        Expressions may either be columns or expressions of columns. Returns `None` if any\n        columns are missing.\n\n        [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n        \"\"\"\n        try:\n            return self._get_frame().select(*exprs, **named_exprs)\n        except polars.ColumnNotFoundError:\n            return None\n\n    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n        \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n        try:\n            return self.get_column(name)\n        except polars.exceptions.ColumnNotFoundError:\n            return None\n\n    def assert_equal(self, other: t.Any):\n        assert isinstance(other, HasAtoms)\n        assert dict(self.schema) == dict(other.schema)\n        for col in self.schema.keys():\n            polars.testing.assert_series_equal(self[col], other[col], check_names=False, rtol=1e-3, atol=1e-8)\n\n    # dunders\n\n    @_fwd_frame(polars.DataFrame.__len__)\n    def __len__(self) -> int:\n        \"\"\"Return the number of atoms in `self`.\"\"\"\n        ...\n\n    @_fwd_frame(polars.DataFrame.__contains__)\n    def __contains__(self, key: str) -> bool:\n        \"\"\"Return whether `self` contains the given column.\"\"\"\n        ...\n\n    def __add__(self, other: IntoAtoms) -> HasAtoms:\n        return self.__class__.concat((self, other), how='inner')\n\n    def __radd__(self, other: IntoAtoms) -> HasAtoms:\n        return self.__class__.concat((other, self), how='inner')\n\n    def __getitem__(self, column: str) -> polars.Series:\n        try:\n            return self.get_column(column)\n        except polars.exceptions.ColumnNotFoundError:\n            if column in ('x', 'y', 'z'):\n                return self.select(_coord_expr(column)).to_series()\n            raise\n\n    @_fwd_frame(polars.DataFrame.__dataframe__)\n    def __dataframe__(self, nan_as_null: bool = False, allow_copy: bool = True) -> polars.interchange.dataframe.PolarsDataFrame:\n        ...\n\n    # atoms-specific methods\n\n    def bbox_atoms(self) -> BBox3D:\n        \"\"\"Return the bounding box of all the atoms in ``self``.\"\"\"\n        return BBox3D.from_pts(self.coords())\n\n    bbox = bbox_atoms\n\n    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                        transform_velocities: bool = False) -> Self:\n        \"\"\"\n        Transform the atoms in `self` by `transform`.\n        If `selection` is given, only transform the atoms in `selection`.\n        \"\"\"\n        transform = Transform3D.make(transform)\n        selection = _selection_to_numpy(self, selection)\n        transformed = self.with_coords(Transform3D.make(transform) @ self.coords(selection), selection)\n        # try to transform velocities as well\n        if transform_velocities and (velocities := self.velocities(selection)) is not None:\n            return transformed.with_velocity(transform.transform_vec(velocities), selection)\n        return transformed\n\n    transform = transform_atoms\n\n    def round_near_zero(self, tol: float = 1e-14) -> Self:\n        \"\"\"\n        Round atom position values near zero to zero.\n        \"\"\"\n        return self.with_columns(coords=polars.concat_list(\n            polars.when(_coord_expr(col).abs() >= tol).then(_coord_expr(col)).otherwise(polars.lit(0.))\n            for col in range(3)\n        ).list.to_array(3))\n\n    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n             y_min: float = -numpy.inf, y_max: float = numpy.inf,\n             z_min: float = -numpy.inf, z_max: float = numpy.inf) -> Self:\n        \"\"\"\n        Crop, removing all atoms outside of the specified region, inclusive.\n        \"\"\"\n\n        return self.filter(\n            self.x().is_between(x_min, x_max, closed='both'),\n            self.y().is_between(y_min, y_max, closed='both'),\n            self.z().is_between(z_min, z_max, closed='both'),\n        )\n\n    crop_atoms = crop\n\n    def _wrap(self, eps: float = 1e-5) -> Self:\n        coords = (self.coords() + eps) % 1. - eps\n        return self.with_coords(coords)\n\n    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                    keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n        \"\"\"\n        De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n        to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n        `keep` (defaults to the first atom).\n\n        If `subset` is specified, only those columns will be included while assessing duplicates.\n        Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n        \"\"\"\n        import scipy.spatial\n\n        cols = set((subset,) if isinstance(subset, str) else subset)\n\n        indices = numpy.arange(len(self))\n\n        spatial_cols = cols.intersection(('x', 'y', 'z'))\n        cols -= spatial_cols\n        if len(spatial_cols) > 0:\n            coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n            tree = scipy.spatial.KDTree(coords)\n\n            # TODO This is a bad algorithm\n            while True:\n                changed = False\n                for (i, j) in tree.query_pairs(tol, 2.):\n                    # whenever we encounter a pair, ensure their index matches\n                    i_i, i_j = indices[[i, j]]\n                    if i_i != i_j:\n                        indices[i] = indices[j] = min(i_i, i_j)\n                        changed = True\n                if not changed:\n                    break\n\n            self = self.with_column(polars.Series('_unique_pts', indices))\n            cols.add('_unique_pts')\n\n        frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n        if len(spatial_cols) > 0:\n            frame = frame.drop('_unique_pts')\n\n        return self.with_atoms(Atoms(frame, _unchecked=True))\n\n    unique = deduplicate\n\n    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n        \"\"\"\n        Return a periodic cell with the given orthogonal cell dimensions.\n\n        If cell_size is not specified, it will be assumed (and may be incorrect).\n        \"\"\"\n        # TODO: test this\n        from .atomcell import AtomCell\n\n        if cell_size is None:\n            warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n            cell_size = self.bbox().size\n            cell_origin = self.bbox().min\n\n        # TODO test this origin code\n        cell = Cell.from_unit_cell(cell_size)\n        if cell_origin is not None:\n            cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n        return AtomCell(self.get_atoms(), cell, frame='local')\n\n    # property getters and setters\n\n    def coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> NDArray[numpy.float64]:\n        \"\"\"Return a `(N, 3)` ndarray of atom coordinates (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n        df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n        return df.get_column('coords').to_numpy().astype(numpy.float64)\n\n    def x(self) -> polars.Expr:\n        return polars.col('coords').arr.get(0).alias('x')\n\n    def y(self) -> polars.Expr:\n        return polars.col('coords').arr.get(1).alias('y')\n\n    def z(self) -> polars.Expr:\n        return polars.col('coords').arr.get(2).alias('z')\n\n    def velocities(self, selection: t.Optional[AtomSelection] = None) -> t.Optional[NDArray[numpy.float64]]:\n        \"\"\"Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n        if 'velocity' not in self:\n            return None\n\n        df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n        return df.get_column('velocity').to_numpy().astype(numpy.float64)\n\n    def types(self) -> t.Optional[polars.Series]:\n        \"\"\"\n        Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        return self.try_get_column('type')\n\n    def masses(self) -> t.Optional[polars.Series]:\n        \"\"\"\n        Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        return self.try_get_column('mass')\n\n    @t.overload\n    def add_atom(self, elem: t.Union[int, str], x: ArrayLike, /, *,\n                 y: None = None, z: None = None,\n                 **kwargs: t.Any) -> Self:\n        ...\n\n    @t.overload\n    def add_atom(self, elem: t.Union[int, str], /,\n                 x: float, y: float, z: float,\n                 **kwargs: t.Any) -> Self:\n        ...\n\n    def add_atom(self, elem: t.Union[int, str], /,\n                 x: t.Union[ArrayLike, float],\n                 y: t.Optional[float] = None,\n                 z: t.Optional[float] = None,\n                 **kwargs: t.Any) -> Self:\n        \"\"\"\n        Return a copy of `self` with an extra atom.\n\n        By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n        Try to avoid calling this in a loop (Use [`HasAtoms.concat`][atomlib.atoms.HasAtoms.concat] instead).\n        \"\"\"\n        if isinstance(elem, int):\n            kwargs.update(elem=elem)\n        else:\n            kwargs.update(symbol=elem)\n        if hasattr(x, '__len__') and len(x) > 1:  # type: ignore\n            (x, y, z) = to_vec3(x)\n        elif y is None or z is None:\n            raise ValueError(\"Must specify vector of positions or x, y, & z.\")\n\n        sym = get_sym(elem) if isinstance(elem, int) else elem\n        d: t.Dict[str, t.Any] = {'x': x, 'y': y, 'z': z, 'symbol': sym, **kwargs}\n        return self.concat(\n            (self, Atoms(d).select_schema(self.schema)),\n            how='vertical'\n        )\n\n    @t.overload\n    def pos(self, x: t.Sequence[t.Optional[float]], /, *,\n            y: None = None, z: None = None,\n            tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n        ...\n\n    @t.overload\n    def pos(self, x: t.Optional[float] = None, y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n            tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n        ...\n\n    def pos(self,\n            x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n            y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n            tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n        \"\"\"\n        Select all atoms at a given position.\n\n        Formally, returns all atoms within a cube of radius ``tol``\n        centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n        Additional parameters given as ``kwargs`` will be checked\n        as additional parameters (with strict equality).\n        \"\"\"\n\n        if isinstance(x, t.Sequence):\n            (x, y, z) = x\n\n        tol = abs(float(tol))\n        selection = polars.lit(True)\n        if x is not None:\n            selection &= self.x().is_between(x - tol, x + tol, closed='none')\n        if y is not None:\n            selection &= self.y().is_between(y - tol, y + tol, closed='none')\n        if z is not None:\n            selection &= self.z().is_between(z - tol, z + tol, closed='none')\n        for (col, val) in kwargs.items():\n            selection &= (polars.col(col) == val)\n\n        return selection\n\n    def with_index(self, index: t.Optional[AtomValues] = None) -> Self:\n        \"\"\"\n        Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n        If `index` is not specified, defaults to an existing index or a new index.\n        \"\"\"\n        if index is None and 'i' in self.columns:\n            return self\n        if index is None:\n            index = numpy.arange(len(self), dtype=numpy.int64)\n        return self.with_column(_values_to_expr(self, index, polars.Int64).alias('i'))\n\n    def with_wobble(self, wobble: t.Optional[AtomValues] = None) -> Self:\n        \"\"\"\n        Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n        If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n        \"\"\"\n        if wobble is None and 'wobble' in self.columns:\n            return self\n        wobble = 0. if wobble is None else wobble\n        return self.with_column(_values_to_expr(self, wobble, polars.Float64).alias('wobble'))\n\n    def with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None) -> Self:\n        \"\"\"\n        Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n        If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n        \"\"\"\n        if frac_occupancy is None and 'frac_occupancy' in self.columns:\n            return self\n        frac_occupancy = 1. if frac_occupancy is None else frac_occupancy\n        return self.with_column(_values_to_expr(self, frac_occupancy, polars.Float64).alias('frac_occupancy'))\n\n    def apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n        \"\"\"\n        Displace the atoms in `self` by the amount in the `wobble` column.\n        `wobble` is interpretated as a mean-squared displacement, which is distributed\n        equally over each axis.\n        \"\"\"\n        if 'wobble' not in self.columns:\n            return self\n        rng = numpy.random.default_rng(seed=rng)\n\n        stddev = self.select((polars.col('wobble') / 3.).sqrt()).to_series().to_numpy()\n        coords = self.coords()\n        coords += stddev[:, None] * rng.standard_normal(coords.shape)\n        return self.with_coords(coords)\n\n    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n        \"\"\"\n        For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n        \"\"\"\n        if 'frac_occupancy' not in self.columns:\n            return self\n        rng = numpy.random.default_rng(seed=rng)\n\n        frac = self.select('frac_occupancy').to_series().to_numpy()\n        choice = rng.binomial(1, frac).astype(numpy.bool_)\n        return self.filter(polars.lit(choice))\n\n    def with_type(self, types: t.Optional[AtomValues] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atom types in column 'type'.\n        If `types` is not specified, use the already existing types or auto-assign them.\n\n        When auto-assigning, each symbol is given a unique value, case-sensitive.\n        Values are assigned from lowest atomic number to highest.\n        For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n        \"\"\"\n        if types is not None:\n            return self.with_columns(type=_values_to_expr(self, types, polars.Int32))\n        if 'type' in self.columns:\n            return self\n\n        unique = Atoms(self._get_frame().unique(maintain_order=False, subset=['elem', 'symbol']).sort(['elem', 'symbol']), _unchecked=True)\n        new = self.with_column(polars.Series('type', values=numpy.zeros(len(self)), dtype=polars.Int32))\n\n        logging.warning(\"Auto-assigning element types\")\n        for (i, (elem, sym)) in enumerate(unique.select(('elem', 'symbol')).rows()):\n            print(f\"Assigning type {i+1} to element '{sym}'\")\n            new = new.with_column(polars.when((polars.col('elem') == elem) & (polars.col('symbol') == sym))\n                                        .then(polars.lit(i+1))\n                                        .otherwise(polars.col('type'))\n                                        .alias('type'))\n\n        assert (new.get_column('type') == 0).sum() == 0\n        return new\n\n    def with_mass(self, mass: t.Optional[ArrayLike] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atom masses in column `'mass'`.\n        If `mass` is not specified, use the already existing masses or auto-assign them.\n        \"\"\"\n        if mass is not None:\n            return self.with_column(_values_to_expr(self, mass, polars.Float32).alias('mass'))\n        if 'mass' in self.columns:\n            return self\n\n        unique_elems = self.get_column('elem').unique()\n        new = self.with_column(polars.Series('mass', values=numpy.zeros(len(self)), dtype=polars.Float32))\n\n        logging.warning(\"Auto-assigning element masses\")\n        for elem in unique_elems:\n            new = new.with_column(polars.when(polars.col('elem') == elem)\n                                        .then(polars.lit(get_mass(elem)))\n                                        .otherwise(polars.col('mass'))\n                                        .alias('mass'))\n\n        assert (new.get_column('mass').abs() < 1e-10).sum() == 0\n        return new\n\n    def with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atomic symbols.\n        \"\"\"\n        if selection is not None:\n            selection = _selection_to_numpy(self, selection)\n            new_symbols = self.get_column('symbol')\n            new_symbols[selection] = polars.Series(list(numpy.broadcast_to(symbols, len(selection))), dtype=polars.Utf8)\n            symbols = new_symbols\n\n        # TODO better cast here\n        symbols = polars.Series('symbol', list(numpy.broadcast_to(symbols, len(self))), dtype=polars.Utf8)\n        return self.with_columns((symbols, get_elem(symbols)))\n\n    def with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> Self:\n        \"\"\"\n        Return `self` replaced with the given atomic positions.\n        \"\"\"\n        if selection is not None:\n            selection = _selection_to_numpy(self, selection)\n            new_pts = self.coords()\n            pts = numpy.atleast_2d(pts)\n            assert pts.shape[-1] == 3\n            new_pts[selection] = pts\n            pts = new_pts\n\n        # https://github.com/pola-rs/polars/issues/18369\n        pts = numpy.broadcast_to(pts, (len(self), 3)) if len(self) else []\n        return self.with_columns(polars.Series('coords', pts, polars.Array(polars.Float64, 3)))\n\n    def with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                      selection: t.Optional[AtomSelection] = None) -> Self:\n        \"\"\"\n        Return `self` replaced with the given atomic velocities.\n        If `pts` is not specified, use the already existing velocities or zero.\n        \"\"\"\n        if pts is None:\n            if 'velocity' in self:\n                return self\n            all_pts = numpy.zeros((len(self), 3))\n        else:\n            all_pts = self['velocity'].to_numpy()\n\n        if selection is None:\n            all_pts = pts or all_pts\n        elif pts is not None:\n            selection = _selection_to_numpy(self, selection)\n            all_pts = numpy.require(all_pts, requirements=['WRITEABLE'])\n            pts = numpy.atleast_2d(pts)\n            assert pts.shape[-1] == 3\n            all_pts[selection] = pts\n\n        all_pts = numpy.broadcast_to(all_pts, (len(self), 3))\n        return self.with_columns(polars.Series('velocity', all_pts, polars.Array(polars.Float64, 3)))\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.columns","title":"columns property","text":"
    columns: List[str]\n

    Return the column names in self.

    RETURNS DESCRIPTION List[str]

    A sequence of column names

    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.dtypes","title":"dtypes property","text":"
    dtypes: List[DataType]\n

    Return the datatypes in self.

    RETURNS DESCRIPTION List[DataType]

    A sequence of column DataTypes

    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.schema","title":"schema property","text":"
    schema: Schema\n

    Return the schema of self.

    RETURNS DESCRIPTION Schema

    A dictionary of column names and DataTypes

    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_column","title":"with_column class-attribute instance-attribute","text":"
    with_column = with_columns\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.bbox","title":"bbox class-attribute instance-attribute","text":"
    bbox = bbox_atoms\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.transform","title":"transform class-attribute instance-attribute","text":"
    transform = transform_atoms\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.crop_atoms","title":"crop_atoms class-attribute instance-attribute","text":"
    crop_atoms = crop\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.unique","title":"unique class-attribute instance-attribute","text":"
    unique = deduplicate\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.get_atoms","title":"get_atoms abstractmethod","text":"
    get_atoms(frame: Literal['local'] = 'local') -> Atoms\n

    Get atoms contained in self. This should be a low cost method.

    PARAMETER DESCRIPTION frame

    Coordinate frame to return atoms in. For a plain HasAtoms, only 'local' is supported.

    TYPE: Literal['local'] DEFAULT: 'local'

    Return

    The contained atoms

    Source code in atomlib/atoms.py
    @abc.abstractmethod\ndef get_atoms(self, frame: t.Literal['local'] = 'local') -> Atoms:\n    \"\"\"\n    Get atoms contained in `self`. This should be a low cost method.\n\n    Args:\n      frame: Coordinate frame to return atoms in. For a plain [`HasAtoms`][atomlib.atoms.HasAtoms],\n             only `'local'` is supported.\n\n    Return:\n      The contained atoms\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_atoms","title":"with_atoms abstractmethod","text":"
    with_atoms(\n    atoms: HasAtoms, frame: Literal[\"local\"] = \"local\"\n) -> Self\n

    Return a copy of self with the inner Atoms replaced.

    PARAMETER DESCRIPTION atoms

    HasAtoms to replace these with.

    TYPE: HasAtoms

    frame

    Coordinate frame inside atoms are in. For a plain HasAtoms, only 'local' is supported.

    TYPE: Literal['local'] DEFAULT: 'local'

    Return

    A copy of self updated with the given atoms

    Source code in atomlib/atoms.py
    @abc.abstractmethod\ndef with_atoms(self, atoms: HasAtoms, frame: t.Literal['local'] = 'local') -> Self:\n    \"\"\"\n    Return a copy of self with the inner [`Atoms`][atomlib.atoms.Atoms] replaced.\n\n    Args:\n      atoms: [`HasAtoms`][atomlib.atoms.HasAtoms] to replace these with.\n      frame: Coordinate frame inside atoms are in. For a plain [`HasAtoms`][atomlib.atoms.HasAtoms],\n             only `'local'` is supported.\n\n    Return:\n      A copy of `self` updated with the given atoms\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.describe","title":"describe","text":"
    describe(\n    percentiles: Union[Sequence[float], float, None] = (\n        0.25,\n        0.5,\n        0.75,\n    ),\n    *,\n    interpolation: RollingInterpolationMethod = \"nearest\"\n) -> DataFrame\n

    Return summary statistics for self. See DataFrame.describe for more information.

    PARAMETER DESCRIPTION percentiles

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), 50% (median), and 75% (third quartile).

    TYPE: Union[Sequence[float], float, None] DEFAULT: (0.25, 0.5, 0.75)

    RETURNS DESCRIPTION DataFrame

    A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.describe)\ndef describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n             interpolation: RollingInterpolationMethod = 'nearest') -> polars.DataFrame:\n    \"\"\"\n    Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n    Args:\n      percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                   50% (median), and 75% (third quartile).\n\n    Returns:\n      A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_columns","title":"with_columns","text":"
    with_columns(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Return a copy of self with the given columns added.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef with_columns(self,\n                 *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 **named_exprs: IntoExpr) -> polars.DataFrame:\n    \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n    return self._get_frame().with_columns(*exprs, **named_exprs)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.insert_column","title":"insert_column","text":"
    insert_column(index: int, column: Series) -> DataFrame\n
    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n    return self._get_frame().insert_column(index, column)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.get_column","title":"get_column","text":"
    get_column(name: str) -> Series\n

    Get the specified column from self, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(lambda df, name: df.get_column(name))\ndef get_column(self, name: str) -> polars.Series:\n    \"\"\"\n    Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.get_columns","title":"get_columns","text":"
    get_columns() -> List[Series]\n

    Return all columns from self as a list of Series.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_columns)\ndef get_columns(self) -> t.List[polars.Series]:\n    \"\"\"\n    Return all columns from `self` as a list of [`Series`][polars.Series].\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.get_column_index","title":"get_column_index","text":"
    get_column_index(name: str) -> int\n

    Get the index of a column by name, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_column_index)\ndef get_column_index(self, name: str) -> int:\n    \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.group_by","title":"group_by","text":"
    group_by(\n    *by: Union[IntoExpr, Iterable[IntoExpr]],\n    maintain_order: bool = False,\n    **named_by: IntoExpr\n) -> GroupBy\n

    Start a group by operation. See DataFrame.group_by for more information.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.group_by)\ndef group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]], maintain_order: bool = False,\n             **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n    \"\"\"\n    Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.pipe","title":"pipe","text":"
    pipe(\n    function: Callable[Concatenate[HasAtomsT, P], T],\n    *args: args,\n    **kwargs: kwargs\n) -> T\n

    Apply function to self (in method-call syntax).

    Source code in atomlib/atoms.py
    def pipe(self: HasAtomsT, function: t.Callable[Concatenate[HasAtomsT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n    \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n    return function(self, *args, **kwargs)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.clone","title":"clone","text":"
    clone() -> DataFrame\n

    Return a copy of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef clone(self) -> polars.DataFrame:\n    \"\"\"Return a copy of `self`.\"\"\"\n    return self._get_frame().clone()\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.drop","title":"drop","text":"
    drop(\n    *columns: Union[str, Iterable[str]], strict: bool = True\n) -> DataFrame\n

    Return self with the specified columns removed.

    Source code in atomlib/atoms.py
    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n    \"\"\"Return `self` with the specified columns removed.\"\"\"\n    return self._get_frame().drop(*columns, strict=strict)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.filter","title":"filter","text":"
    filter(\n    *predicates: Union[\n        None,\n        IntoExprColumn,\n        Iterable[IntoExprColumn],\n        bool,\n        List[bool],\n        ndarray,\n    ],\n    **constraints: Any\n) -> Self\n

    Filter self, removing rows which evaluate to False.

    Source code in atomlib/atoms.py
    def filter(\n    self,\n    *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n    **constraints: t.Any,\n) -> Self:\n    \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n    # TODO clean up\n    preds_not_none = tuple(filter(lambda p: p is not None, predicates))\n    if not len(preds_not_none) and not len(constraints):\n        return self\n    return self.with_atoms(Atoms(self._get_frame().filter(*preds_not_none, **constraints), _unchecked=True))  # type: ignore\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.sort","title":"sort","text":"
    sort(\n    by: Union[IntoExpr, Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: Union[bool, Sequence[bool]] = False,\n    nulls_last: bool = False\n) -> DataFrame\n

    Sort the atoms in self by the given columns/expressions.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef sort(\n    self,\n    by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: t.Union[bool, t.Sequence[bool]] = False,\n    nulls_last: bool = False,\n) -> polars.DataFrame:\n    \"\"\"\n    Sort the atoms in `self` by the given columns/expressions.\n    \"\"\"\n    return self._get_frame().sort(\n        by, *more_by, descending=descending, nulls_last=nulls_last\n    )\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.slice","title":"slice","text":"
    slice(\n    offset: int, length: Optional[int] = None\n) -> DataFrame\n

    Return a slice of the rows in self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef slice(self, offset: int, length: t.Optional[int] = None) -> polars.DataFrame:\n    \"\"\"Return a slice of the rows in `self`.\"\"\"\n    return self._get_frame().slice(offset, length)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.head","title":"head","text":"
    head(n: int = 5) -> DataFrame\n

    Return the first n rows of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef head(self, n: int = 5) -> polars.DataFrame:\n    \"\"\"Return the first `n` rows of `self`.\"\"\"\n    return self._get_frame().head(n)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.tail","title":"tail","text":"
    tail(n: int = 5) -> DataFrame\n

    Return the last n rows of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef tail(self, n: int = 5) -> polars.DataFrame:\n    \"\"\"Return the last `n` rows of `self`.\"\"\"\n    return self._get_frame().tail(n)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.drop_nulls","title":"drop_nulls","text":"
    drop_nulls(\n    subset: Union[str, Collection[str], None] = None\n) -> DataFrame\n

    Drop rows that contain nulls in any of columns subset.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n    \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n    return self._get_frame().drop_nulls(subset)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.fill_null","title":"fill_null","text":"
    fill_null(\n    value: Any = None,\n    strategy: Optional[FillNullStrategy] = None,\n    limit: Optional[int] = None,\n    matches_supertype: bool = True,\n) -> DataFrame\n

    Fill null values in self, using the specified value or strategy.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef fill_null(\n    self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n    limit: t.Optional[int] = None, matches_supertype: bool = True,\n) -> polars.DataFrame:\n    \"\"\"Fill null values in `self`, using the specified value or strategy.\"\"\"\n    return self._get_frame().fill_null(value, strategy, limit, matches_supertype=matches_supertype)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.fill_nan","title":"fill_nan","text":"
    fill_nan(value: Union[Expr, int, float, None]) -> DataFrame\n

    Fill floating-point NaN values in self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef fill_nan(self, value: t.Union[polars.Expr, int, float, None]) -> polars.DataFrame:\n    \"\"\"Fill floating-point NaN values in `self`.\"\"\"\n    return self._get_frame().fill_nan(value)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.concat","title":"concat classmethod","text":"
    concat(\n    atoms: Union[\n        HasAtomsT,\n        IntoAtoms,\n        Iterable[Union[HasAtomsT, IntoAtoms]],\n    ],\n    *,\n    rechunk: bool = True,\n    how: ConcatMethod = \"vertical\"\n) -> HasAtomsT\n

    Concatenate multiple Atoms together, handling metadata appropriately.

    Source code in atomlib/atoms.py
    @classmethod\ndef concat(cls: t.Type[HasAtomsT],\n           atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n           rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n    \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n    # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n    # same type as ``cls``.\n    if _is_abstract(cls):\n        raise TypeError(\"concat() must be called on a concrete class.\")\n\n    if isinstance(atoms, HasAtoms):\n        atoms = (atoms,)\n    dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n    representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n    if len(dfs) == 0:\n        return representative.with_atoms(Atoms.empty(), 'local')\n\n    if how in ('vertical', 'vertical_relaxed'):\n        # get order from first member\n        cols = dfs[0].columns\n        dfs = [df.select(cols) for df in dfs]\n    elif how == 'inner':\n        cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n        schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n        if len(schema) == 0:\n            raise ValueError(\"Atoms have no columns in common\")\n\n        dfs = [_select_schema(df, schema) for df in dfs]\n        how = 'vertical'\n\n    return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.partition_by","title":"partition_by","text":"
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: bool = False\n) -> Union[List[Self], Dict[Any, Self]]\n

    Group by the given columns and partition into separate dataframes.

    Return the partitions as a dictionary by specifying as_dict=True.

    Source code in atomlib/atoms.py
    def partition_by(\n    self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n    maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n    \"\"\"\n    Group by the given columns and partition into separate dataframes.\n\n    Return the partitions as a dictionary by specifying `as_dict=True`.\n    \"\"\"\n    if as_dict:\n        d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n        return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n    return [\n        self.with_atoms(Atoms(df, _unchecked=True))\n        for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n    ]\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.select","title":"select","text":"
    select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.select)\ndef select(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr,\n) -> polars.DataFrame:\n    \"\"\"\n    Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.select_schema","title":"select_schema","text":"
    select_schema(schema: SchemaDict) -> DataFrame\n

    Select columns from self and cast to the given schema. Raises TypeError if a column is not found or if it can't be cast.

    Source code in atomlib/atoms.py
    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n    \"\"\"\n    Select columns from `self` and cast to the given schema.\n    Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n    \"\"\"\n    return _select_schema(self, schema)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.select_props","title":"select_props","text":"
    select_props(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Self\n

    Select exprs from self, while keeping required columns.

    RETURNS DESCRIPTION Self

    A HasAtoms filtered to contain the

    Self

    specified properties (as well as required columns).

    Source code in atomlib/atoms.py
    def select_props(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Self:\n    \"\"\"\n    Select `exprs` from `self`, while keeping required columns.\n\n    Returns:\n      A [`HasAtoms`][atomlib.atoms.HasAtoms] filtered to contain the\n      specified properties (as well as required columns).\n    \"\"\"\n    props = self._get_frame().lazy().select(*exprs, **named_exprs).drop(_REQUIRED_COLUMNS, strict=False).collect(_eager=True)\n    return self.with_atoms(\n        Atoms(self._get_frame().select(_REQUIRED_COLUMNS).hstack(props), _unchecked=False)\n    )\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.try_select","title":"try_select","text":"
    try_select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Optional[DataFrame]\n

    Try to select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns. Returns None if any columns are missing.

    Source code in atomlib/atoms.py
    def try_select(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr,\n) -> t.Optional[polars.DataFrame]:\n    \"\"\"\n    Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns. Returns `None` if any\n    columns are missing.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    try:\n        return self._get_frame().select(*exprs, **named_exprs)\n    except polars.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.try_get_column","title":"try_get_column","text":"
    try_get_column(name: str) -> Optional[Series]\n

    Try to get a column from self, returning None if it doesn't exist.

    Source code in atomlib/atoms.py
    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n    \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n    try:\n        return self.get_column(name)\n    except polars.exceptions.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n
    Source code in atomlib/atoms.py
    def assert_equal(self, other: t.Any):\n    assert isinstance(other, HasAtoms)\n    assert dict(self.schema) == dict(other.schema)\n    for col in self.schema.keys():\n        polars.testing.assert_series_equal(self[col], other[col], check_names=False, rtol=1e-3, atol=1e-8)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.bbox_atoms","title":"bbox_atoms","text":"
    bbox_atoms() -> BBox3D\n

    Return the bounding box of all the atoms in self.

    Source code in atomlib/atoms.py
    def bbox_atoms(self) -> BBox3D:\n    \"\"\"Return the bounding box of all the atoms in ``self``.\"\"\"\n    return BBox3D.from_pts(self.coords())\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.transform_atoms","title":"transform_atoms","text":"
    transform_atoms(\n    transform: IntoTransform3D,\n    selection: Optional[AtomSelection] = None,\n    *,\n    transform_velocities: bool = False\n) -> Self\n

    Transform the atoms in self by transform. If selection is given, only transform the atoms in selection.

    Source code in atomlib/atoms.py
    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                    transform_velocities: bool = False) -> Self:\n    \"\"\"\n    Transform the atoms in `self` by `transform`.\n    If `selection` is given, only transform the atoms in `selection`.\n    \"\"\"\n    transform = Transform3D.make(transform)\n    selection = _selection_to_numpy(self, selection)\n    transformed = self.with_coords(Transform3D.make(transform) @ self.coords(selection), selection)\n    # try to transform velocities as well\n    if transform_velocities and (velocities := self.velocities(selection)) is not None:\n        return transformed.with_velocity(transform.transform_vec(velocities), selection)\n    return transformed\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.round_near_zero","title":"round_near_zero","text":"
    round_near_zero(tol: float = 1e-14) -> Self\n

    Round atom position values near zero to zero.

    Source code in atomlib/atoms.py
    def round_near_zero(self, tol: float = 1e-14) -> Self:\n    \"\"\"\n    Round atom position values near zero to zero.\n    \"\"\"\n    return self.with_columns(coords=polars.concat_list(\n        polars.when(_coord_expr(col).abs() >= tol).then(_coord_expr(col)).otherwise(polars.lit(0.))\n        for col in range(3)\n    ).list.to_array(3))\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.crop","title":"crop","text":"
    crop(\n    x_min: float = -numpy.inf,\n    x_max: float = numpy.inf,\n    y_min: float = -numpy.inf,\n    y_max: float = numpy.inf,\n    z_min: float = -numpy.inf,\n    z_max: float = numpy.inf,\n) -> Self\n

    Crop, removing all atoms outside of the specified region, inclusive.

    Source code in atomlib/atoms.py
    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf) -> Self:\n    \"\"\"\n    Crop, removing all atoms outside of the specified region, inclusive.\n    \"\"\"\n\n    return self.filter(\n        self.x().is_between(x_min, x_max, closed='both'),\n        self.y().is_between(y_min, y_max, closed='both'),\n        self.z().is_between(z_min, z_max, closed='both'),\n    )\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.deduplicate","title":"deduplicate","text":"
    deduplicate(\n    tol: float = 0.001,\n    subset: Iterable[str] = (\"x\", \"y\", \"z\", \"symbol\"),\n    keep: UniqueKeepStrategy = \"first\",\n    maintain_order: bool = True,\n) -> Self\n

    De-duplicate atoms in self. Atoms of the same symbol that are closer than tolerance to each other (by Euclidian distance) will be removed, leaving only the atom specified by keep (defaults to the first atom).

    If subset is specified, only those columns will be included while assessing duplicates. Floating point columns other than 'x', 'y', and 'z' will not by toleranced.

    Source code in atomlib/atoms.py
    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n    \"\"\"\n    De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n    to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n    `keep` (defaults to the first atom).\n\n    If `subset` is specified, only those columns will be included while assessing duplicates.\n    Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n    \"\"\"\n    import scipy.spatial\n\n    cols = set((subset,) if isinstance(subset, str) else subset)\n\n    indices = numpy.arange(len(self))\n\n    spatial_cols = cols.intersection(('x', 'y', 'z'))\n    cols -= spatial_cols\n    if len(spatial_cols) > 0:\n        coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n        tree = scipy.spatial.KDTree(coords)\n\n        # TODO This is a bad algorithm\n        while True:\n            changed = False\n            for (i, j) in tree.query_pairs(tol, 2.):\n                # whenever we encounter a pair, ensure their index matches\n                i_i, i_j = indices[[i, j]]\n                if i_i != i_j:\n                    indices[i] = indices[j] = min(i_i, i_j)\n                    changed = True\n            if not changed:\n                break\n\n        self = self.with_column(polars.Series('_unique_pts', indices))\n        cols.add('_unique_pts')\n\n    frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n    if len(spatial_cols) > 0:\n        frame = frame.drop('_unique_pts')\n\n    return self.with_atoms(Atoms(frame, _unchecked=True))\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_bounds","title":"with_bounds","text":"
    with_bounds(\n    cell_size: Optional[VecLike] = None,\n    cell_origin: Optional[VecLike] = None,\n) -> \"AtomCell\"\n

    Return a periodic cell with the given orthogonal cell dimensions.

    If cell_size is not specified, it will be assumed (and may be incorrect).

    Source code in atomlib/atoms.py
    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n    \"\"\"\n    Return a periodic cell with the given orthogonal cell dimensions.\n\n    If cell_size is not specified, it will be assumed (and may be incorrect).\n    \"\"\"\n    # TODO: test this\n    from .atomcell import AtomCell\n\n    if cell_size is None:\n        warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n        cell_size = self.bbox().size\n        cell_origin = self.bbox().min\n\n    # TODO test this origin code\n    cell = Cell.from_unit_cell(cell_size)\n    if cell_origin is not None:\n        cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n    return AtomCell(self.get_atoms(), cell, frame='local')\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.coords","title":"coords","text":"
    coords(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Literal[\"local\"] = \"local\"\n) -> NDArray[float64]\n

    Return a (N, 3) ndarray of atom coordinates (dtype numpy.float64).

    Source code in atomlib/atoms.py
    def coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> NDArray[numpy.float64]:\n    \"\"\"Return a `(N, 3)` ndarray of atom coordinates (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n    df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n    return df.get_column('coords').to_numpy().astype(numpy.float64)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.x","title":"x","text":"
    x() -> Expr\n
    Source code in atomlib/atoms.py
    def x(self) -> polars.Expr:\n    return polars.col('coords').arr.get(0).alias('x')\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.y","title":"y","text":"
    y() -> Expr\n
    Source code in atomlib/atoms.py
    def y(self) -> polars.Expr:\n    return polars.col('coords').arr.get(1).alias('y')\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.z","title":"z","text":"
    z() -> Expr\n
    Source code in atomlib/atoms.py
    def z(self) -> polars.Expr:\n    return polars.col('coords').arr.get(2).alias('z')\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.velocities","title":"velocities","text":"
    velocities(\n    selection: Optional[AtomSelection] = None,\n) -> Optional[NDArray[float64]]\n

    Return a (N, 3) ndarray of atom velocities (dtype numpy.float64).

    Source code in atomlib/atoms.py
    def velocities(self, selection: t.Optional[AtomSelection] = None) -> t.Optional[NDArray[numpy.float64]]:\n    \"\"\"Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n    if 'velocity' not in self:\n        return None\n\n    df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n    return df.get_column('velocity').to_numpy().astype(numpy.float64)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.types","title":"types","text":"
    types() -> Optional[Series]\n

    Returns a Series of atom types (dtype polars.Int32).

    Source code in atomlib/atoms.py
    def types(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('type')\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.masses","title":"masses","text":"
    masses() -> Optional[Series]\n

    Returns a Series of atom masses (dtype polars.Float32).

    Source code in atomlib/atoms.py
    def masses(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('mass')\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.add_atom","title":"add_atom","text":"
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: Union[ArrayLike, float],\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    **kwargs: Any,\n) -> Self\n

    Return a copy of self with an extra atom.

    By default, all extra columns present in self must be specified as **kwargs.

    Try to avoid calling this in a loop (Use HasAtoms.concat instead).

    Source code in atomlib/atoms.py
    def add_atom(self, elem: t.Union[int, str], /,\n             x: t.Union[ArrayLike, float],\n             y: t.Optional[float] = None,\n             z: t.Optional[float] = None,\n             **kwargs: t.Any) -> Self:\n    \"\"\"\n    Return a copy of `self` with an extra atom.\n\n    By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n    Try to avoid calling this in a loop (Use [`HasAtoms.concat`][atomlib.atoms.HasAtoms.concat] instead).\n    \"\"\"\n    if isinstance(elem, int):\n        kwargs.update(elem=elem)\n    else:\n        kwargs.update(symbol=elem)\n    if hasattr(x, '__len__') and len(x) > 1:  # type: ignore\n        (x, y, z) = to_vec3(x)\n    elif y is None or z is None:\n        raise ValueError(\"Must specify vector of positions or x, y, & z.\")\n\n    sym = get_sym(elem) if isinstance(elem, int) else elem\n    d: t.Dict[str, t.Any] = {'x': x, 'y': y, 'z': z, 'symbol': sym, **kwargs}\n    return self.concat(\n        (self, Atoms(d).select_schema(self.schema)),\n        how='vertical'\n    )\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.pos","title":"pos","text":"
    pos(\n    x: Union[Sequence[Optional[float]], float, None] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n

    Select all atoms at a given position.

    Formally, returns all atoms within a cube of radius tol centered at (x,y,z), exclusive of the cube's surface.

    Additional parameters given as kwargs will be checked as additional parameters (with strict equality).

    Source code in atomlib/atoms.py
    def pos(self,\n        x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n        y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n        tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n    \"\"\"\n    Select all atoms at a given position.\n\n    Formally, returns all atoms within a cube of radius ``tol``\n    centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n    Additional parameters given as ``kwargs`` will be checked\n    as additional parameters (with strict equality).\n    \"\"\"\n\n    if isinstance(x, t.Sequence):\n        (x, y, z) = x\n\n    tol = abs(float(tol))\n    selection = polars.lit(True)\n    if x is not None:\n        selection &= self.x().is_between(x - tol, x + tol, closed='none')\n    if y is not None:\n        selection &= self.y().is_between(y - tol, y + tol, closed='none')\n    if z is not None:\n        selection &= self.z().is_between(z - tol, z + tol, closed='none')\n    for (col, val) in kwargs.items():\n        selection &= (polars.col(col) == val)\n\n    return selection\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_index","title":"with_index","text":"
    with_index(index: Optional[AtomValues] = None) -> Self\n

    Returns self with a row index added in column 'i' (dtype polars.Int64). If index is not specified, defaults to an existing index or a new index.

    Source code in atomlib/atoms.py
    def with_index(self, index: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n    If `index` is not specified, defaults to an existing index or a new index.\n    \"\"\"\n    if index is None and 'i' in self.columns:\n        return self\n    if index is None:\n        index = numpy.arange(len(self), dtype=numpy.int64)\n    return self.with_column(_values_to_expr(self, index, polars.Int64).alias('i'))\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_wobble","title":"with_wobble","text":"
    with_wobble(wobble: Optional[AtomValues] = None) -> Self\n

    Return self with the given displacements in column 'wobble' (dtype polars.Float64). If wobble is not specified, defaults to the already-existing wobbles or 0.

    Source code in atomlib/atoms.py
    def with_wobble(self, wobble: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n    \"\"\"\n    if wobble is None and 'wobble' in self.columns:\n        return self\n    wobble = 0. if wobble is None else wobble\n    return self.with_column(_values_to_expr(self, wobble, polars.Float64).alias('wobble'))\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_occupancy","title":"with_occupancy","text":"
    with_occupancy(\n    frac_occupancy: Optional[AtomValues] = None,\n) -> Self\n

    Return self with the given fractional occupancies (dtype polars.Float64). If frac_occupancy is not specified, defaults to the already-existing occupancies or 1.

    Source code in atomlib/atoms.py
    def with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n    \"\"\"\n    if frac_occupancy is None and 'frac_occupancy' in self.columns:\n        return self\n    frac_occupancy = 1. if frac_occupancy is None else frac_occupancy\n    return self.with_column(_values_to_expr(self, frac_occupancy, polars.Float64).alias('frac_occupancy'))\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.apply_wobble","title":"apply_wobble","text":"
    apply_wobble(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    Displace the atoms in self by the amount in the wobble column. wobble is interpretated as a mean-squared displacement, which is distributed equally over each axis.

    Source code in atomlib/atoms.py
    def apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    Displace the atoms in `self` by the amount in the `wobble` column.\n    `wobble` is interpretated as a mean-squared displacement, which is distributed\n    equally over each axis.\n    \"\"\"\n    if 'wobble' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    stddev = self.select((polars.col('wobble') / 3.).sqrt()).to_series().to_numpy()\n    coords = self.coords()\n    coords += stddev[:, None] * rng.standard_normal(coords.shape)\n    return self.with_coords(coords)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.apply_occupancy","title":"apply_occupancy","text":"
    apply_occupancy(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    For each atom in self, use its frac_occupancy to randomly decide whether to remove it.

    Source code in atomlib/atoms.py
    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n    \"\"\"\n    if 'frac_occupancy' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    frac = self.select('frac_occupancy').to_series().to_numpy()\n    choice = rng.binomial(1, frac).astype(numpy.bool_)\n    return self.filter(polars.lit(choice))\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_type","title":"with_type","text":"
    with_type(types: Optional[AtomValues] = None) -> Self\n

    Return self with the given atom types in column 'type'. If types is not specified, use the already existing types or auto-assign them.

    When auto-assigning, each symbol is given a unique value, case-sensitive. Values are assigned from lowest atomic number to highest. For instance: [\"Ag+\", \"Na\", \"H\", \"Ag\"] => [3, 11, 1, 2]

    Source code in atomlib/atoms.py
    def with_type(self, types: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom types in column 'type'.\n    If `types` is not specified, use the already existing types or auto-assign them.\n\n    When auto-assigning, each symbol is given a unique value, case-sensitive.\n    Values are assigned from lowest atomic number to highest.\n    For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n    \"\"\"\n    if types is not None:\n        return self.with_columns(type=_values_to_expr(self, types, polars.Int32))\n    if 'type' in self.columns:\n        return self\n\n    unique = Atoms(self._get_frame().unique(maintain_order=False, subset=['elem', 'symbol']).sort(['elem', 'symbol']), _unchecked=True)\n    new = self.with_column(polars.Series('type', values=numpy.zeros(len(self)), dtype=polars.Int32))\n\n    logging.warning(\"Auto-assigning element types\")\n    for (i, (elem, sym)) in enumerate(unique.select(('elem', 'symbol')).rows()):\n        print(f\"Assigning type {i+1} to element '{sym}'\")\n        new = new.with_column(polars.when((polars.col('elem') == elem) & (polars.col('symbol') == sym))\n                                    .then(polars.lit(i+1))\n                                    .otherwise(polars.col('type'))\n                                    .alias('type'))\n\n    assert (new.get_column('type') == 0).sum() == 0\n    return new\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_mass","title":"with_mass","text":"
    with_mass(mass: Optional[ArrayLike] = None) -> Self\n

    Return self with the given atom masses in column 'mass'. If mass is not specified, use the already existing masses or auto-assign them.

    Source code in atomlib/atoms.py
    def with_mass(self, mass: t.Optional[ArrayLike] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom masses in column `'mass'`.\n    If `mass` is not specified, use the already existing masses or auto-assign them.\n    \"\"\"\n    if mass is not None:\n        return self.with_column(_values_to_expr(self, mass, polars.Float32).alias('mass'))\n    if 'mass' in self.columns:\n        return self\n\n    unique_elems = self.get_column('elem').unique()\n    new = self.with_column(polars.Series('mass', values=numpy.zeros(len(self)), dtype=polars.Float32))\n\n    logging.warning(\"Auto-assigning element masses\")\n    for elem in unique_elems:\n        new = new.with_column(polars.when(polars.col('elem') == elem)\n                                    .then(polars.lit(get_mass(elem)))\n                                    .otherwise(polars.col('mass'))\n                                    .alias('mass'))\n\n    assert (new.get_column('mass').abs() < 1e-10).sum() == 0\n    return new\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_symbol","title":"with_symbol","text":"
    with_symbol(\n    symbols: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n) -> Self\n

    Return self with the given atomic symbols.

    Source code in atomlib/atoms.py
    def with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atomic symbols.\n    \"\"\"\n    if selection is not None:\n        selection = _selection_to_numpy(self, selection)\n        new_symbols = self.get_column('symbol')\n        new_symbols[selection] = polars.Series(list(numpy.broadcast_to(symbols, len(selection))), dtype=polars.Utf8)\n        symbols = new_symbols\n\n    # TODO better cast here\n    symbols = polars.Series('symbol', list(numpy.broadcast_to(symbols, len(self))), dtype=polars.Utf8)\n    return self.with_columns((symbols, get_elem(symbols)))\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_coords","title":"with_coords","text":"
    with_coords(\n    pts: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Literal[\"local\"] = \"local\"\n) -> Self\n

    Return self replaced with the given atomic positions.

    Source code in atomlib/atoms.py
    def with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic positions.\n    \"\"\"\n    if selection is not None:\n        selection = _selection_to_numpy(self, selection)\n        new_pts = self.coords()\n        pts = numpy.atleast_2d(pts)\n        assert pts.shape[-1] == 3\n        new_pts[selection] = pts\n        pts = new_pts\n\n    # https://github.com/pola-rs/polars/issues/18369\n    pts = numpy.broadcast_to(pts, (len(self), 3)) if len(self) else []\n    return self.with_columns(polars.Series('coords', pts, polars.Array(polars.Float64, 3)))\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_velocity","title":"with_velocity","text":"
    with_velocity(\n    pts: Optional[ArrayLike] = None,\n    selection: Optional[AtomSelection] = None,\n) -> Self\n

    Return self replaced with the given atomic velocities. If pts is not specified, use the already existing velocities or zero.

    Source code in atomlib/atoms.py
    def with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                  selection: t.Optional[AtomSelection] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic velocities.\n    If `pts` is not specified, use the already existing velocities or zero.\n    \"\"\"\n    if pts is None:\n        if 'velocity' in self:\n            return self\n        all_pts = numpy.zeros((len(self), 3))\n    else:\n        all_pts = self['velocity'].to_numpy()\n\n    if selection is None:\n        all_pts = pts or all_pts\n    elif pts is not None:\n        selection = _selection_to_numpy(self, selection)\n        all_pts = numpy.require(all_pts, requirements=['WRITEABLE'])\n        pts = numpy.atleast_2d(pts)\n        assert pts.shape[-1] == 3\n        all_pts[selection] = pts\n\n    all_pts = numpy.broadcast_to(all_pts, (len(self), 3))\n    return self.with_columns(polars.Series('velocity', all_pts, polars.Array(polars.Float64, 3)))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms","title":"Atoms","text":"

    Bases: AtomsIOMixin, HasAtoms

    A collection of atoms, absent any implied coordinate system. Implemented as a wrapper around a polars.DataFrame.

    Must contain the following columns:

    • coords: array of [x, y, z] positions, float
    • elem: atomic number, int
    • symbol: atomic symbol (may contain charges)

    In addition, it commonly contains the following columns:

    • i: Initial atom number
    • wobble: Isotropic Debye-Waller mean-squared deviation (\\(\\left<u^2\\right> = B \\cdot \\frac{3}{8 \\pi^2}\\), dimensions of [Length^2])
    • frac_occupancy: Fractional occupancy, in the range [0., 1.]
    • mass: Atomic mass, in g/mol (approx. Da)
    • velocity: array of [x, y, z] velocities, float, dimensions of length/time
    • type: Numeric atom type, as used by programs like LAMMPS
    Source code in atomlib/atoms.py
    class Atoms(AtomsIOMixin, HasAtoms):\n    r\"\"\"\n    A collection of atoms, absent any implied coordinate system.\n    Implemented as a wrapper around a [`polars.DataFrame`][polars.DataFrame].\n\n    Must contain the following columns:\n\n    - coords: array of `[x, y, z]` positions, float\n    - elem: atomic number, int\n    - symbol: atomic symbol (may contain charges)\n\n    In addition, it commonly contains the following columns:\n\n    - i: Initial atom number\n    - wobble: Isotropic Debye-Waller mean-squared deviation ($\\left<u^2\\right> = B \\cdot \\frac{3}{8 \\pi^2}$, dimensions of [Length^2])\n    - frac_occupancy: Fractional occupancy, in the range [0., 1.]\n    - mass: Atomic mass, in g/mol (approx. Da)\n    - velocity: array of `[x, y, z]` velocities, float, dimensions of length/time\n    - type: Numeric atom type, as used by programs like LAMMPS\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n\n    def __init__(self, data: t.Optional[IntoAtoms] = None, columns: t.Optional[t.Sequence[str]] = None,\n                 orient: t.Union[t.Literal['row'], t.Literal['col'], None] = None,\n                 _unchecked: bool = False):\n        self._bbox: t.Optional[BBox3D] = None\n        self.inner: polars.DataFrame\n\n        if data is None:\n            assert columns is None\n            self.inner = polars.DataFrame([\n                polars.Series('coords', (), dtype=polars.Array(polars.Float64, 3)),\n                polars.Series('elem', (), dtype=polars.Int8),\n                polars.Series('symbol', (), dtype=polars.Utf8),\n            ])\n        elif isinstance(data, polars.DataFrame):\n            self.inner = data\n        elif isinstance(data, Atoms):\n            self.inner = data.inner\n            _unchecked = True\n        else:\n            self.inner = polars.DataFrame(data, schema=columns, orient=orient)\n\n        if not _unchecked:\n            # stack ('x', 'y', 'z') -> 'coords'\n            self.inner = _with_columns_stacked(self.inner, ('x', 'y', 'z'), 'coords')\n            self.inner = _with_columns_stacked(self.inner, ('v_x', 'v_y', 'v_z'), 'velocity')\n\n            missing: t.Tuple[str, ...] = tuple(set(['symbol', 'elem']) - set(self.columns))\n            if len(missing) > 1:\n                raise ValueError(\"'Atoms' missing columns 'elem' and/or 'symbol'.\")\n            # fill 'symbol' from 'elem' or vice-versa\n            if missing == ('symbol',):\n                self.inner = self.inner.with_columns(get_sym(self.inner['elem']))\n            elif missing == ('elem',):\n                # by convention, add before 'symbol' column\n                self.inner = self.inner.insert_column(\n                    self.inner.get_column_index('symbol'),\n                    get_elem(self.inner['symbol']),\n                )\n\n            # cast to standard dtypes\n            self.inner = self.inner.with_columns([\n                self.inner[col].cast(dtype)\n                for (col, dtype) in _COLUMN_DTYPES.items() if col in self.inner\n            ])\n\n            self._validate_atoms()\n\n    @staticmethod\n    def empty() -> Atoms:\n        \"\"\"\n        Return an empty Atoms with only the mandatory columns.\n        \"\"\"\n        return Atoms()\n\n    def _validate_atoms(self):\n        missing = [col for col in _REQUIRED_COLUMNS if col not in self.columns]\n        if len(missing):\n            raise ValueError(f\"'Atoms' missing column(s) {', '.join(map(repr, missing))}\")\n\n    def get_atoms(self, frame: t.Literal['local'] = 'local') -> Atoms:\n        if frame != 'local':\n            raise ValueError(f\"Atoms without a cell only support the 'local' coordinate frame, not '{frame}'.\")\n        return self\n\n    def with_atoms(self, atoms: HasAtoms, frame: t.Literal['local'] = 'local') -> Atoms:\n        if frame != 'local':\n            raise ValueError(f\"Atoms without a cell only support the 'local' coordinate frame, not '{frame}'.\")\n        return atoms.get_atoms()\n\n    @classmethod\n    def _combine_metadata(cls: t.Type[Atoms], *atoms: HasAtoms) -> Atoms:\n        return cls.empty()\n\n    def bbox(self) -> BBox3D:\n        \"\"\"Return the bounding box of all the points in `self`.\"\"\"\n        if self._bbox is None:\n            self._bbox = BBox3D.from_pts(self.coords())\n\n        return self._bbox\n\n    def __str__(self) -> str:\n        return f\"Atoms, {self.inner!s}\"\n\n    def __repr__(self) -> str:\n        buf = StringIO()\n        buf.write(\"Atoms([\\n\")\n\n        for series in self.inner.to_dict().values():\n            buf.write(f\"    Series({series.name!r}, {series.to_list()!r}, dtype={series.dtype!r}),\\n\")\n\n        buf.write(\"])\\n\")\n        return buf.getvalue()\n\n    def _repr_pretty_(self, p: t.Any, cycle: bool) -> None:\n        p.text('Atoms(...)') if cycle else p.text(str(self))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.columns","title":"columns property","text":"
    columns: List[str]\n

    Return the column names in self.

    RETURNS DESCRIPTION List[str]

    A sequence of column names

    "},{"location":"api/atoms/#atomlib.atoms.Atoms.dtypes","title":"dtypes property","text":"
    dtypes: List[DataType]\n

    Return the datatypes in self.

    RETURNS DESCRIPTION List[DataType]

    A sequence of column DataTypes

    "},{"location":"api/atoms/#atomlib.atoms.Atoms.schema","title":"schema property","text":"
    schema: Schema\n

    Return the schema of self.

    RETURNS DESCRIPTION Schema

    A dictionary of column names and DataTypes

    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_column","title":"with_column class-attribute instance-attribute","text":"
    with_column = with_columns\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.transform","title":"transform class-attribute instance-attribute","text":"
    transform = transform_atoms\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.crop_atoms","title":"crop_atoms class-attribute instance-attribute","text":"
    crop_atoms = crop\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.unique","title":"unique class-attribute instance-attribute","text":"
    unique = deduplicate\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.inner","title":"inner instance-attribute","text":"
    inner: DataFrame\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.describe","title":"describe","text":"
    describe(\n    percentiles: Union[Sequence[float], float, None] = (\n        0.25,\n        0.5,\n        0.75,\n    ),\n    *,\n    interpolation: RollingInterpolationMethod = \"nearest\"\n) -> DataFrame\n

    Return summary statistics for self. See DataFrame.describe for more information.

    PARAMETER DESCRIPTION percentiles

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), 50% (median), and 75% (third quartile).

    TYPE: Union[Sequence[float], float, None] DEFAULT: (0.25, 0.5, 0.75)

    RETURNS DESCRIPTION DataFrame

    A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.describe)\ndef describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n             interpolation: RollingInterpolationMethod = 'nearest') -> polars.DataFrame:\n    \"\"\"\n    Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n    Args:\n      percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                   50% (median), and 75% (third quartile).\n\n    Returns:\n      A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_columns","title":"with_columns","text":"
    with_columns(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Return a copy of self with the given columns added.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef with_columns(self,\n                 *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 **named_exprs: IntoExpr) -> polars.DataFrame:\n    \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n    return self._get_frame().with_columns(*exprs, **named_exprs)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.insert_column","title":"insert_column","text":"
    insert_column(index: int, column: Series) -> DataFrame\n
    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n    return self._get_frame().insert_column(index, column)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.get_column","title":"get_column","text":"
    get_column(name: str) -> Series\n

    Get the specified column from self, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(lambda df, name: df.get_column(name))\ndef get_column(self, name: str) -> polars.Series:\n    \"\"\"\n    Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.get_columns","title":"get_columns","text":"
    get_columns() -> List[Series]\n

    Return all columns from self as a list of Series.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_columns)\ndef get_columns(self) -> t.List[polars.Series]:\n    \"\"\"\n    Return all columns from `self` as a list of [`Series`][polars.Series].\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.get_column_index","title":"get_column_index","text":"
    get_column_index(name: str) -> int\n

    Get the index of a column by name, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_column_index)\ndef get_column_index(self, name: str) -> int:\n    \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.group_by","title":"group_by","text":"
    group_by(\n    *by: Union[IntoExpr, Iterable[IntoExpr]],\n    maintain_order: bool = False,\n    **named_by: IntoExpr\n) -> GroupBy\n

    Start a group by operation. See DataFrame.group_by for more information.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.group_by)\ndef group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]], maintain_order: bool = False,\n             **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n    \"\"\"\n    Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.pipe","title":"pipe","text":"
    pipe(\n    function: Callable[Concatenate[HasAtomsT, P], T],\n    *args: args,\n    **kwargs: kwargs\n) -> T\n

    Apply function to self (in method-call syntax).

    Source code in atomlib/atoms.py
    def pipe(self: HasAtomsT, function: t.Callable[Concatenate[HasAtomsT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n    \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n    return function(self, *args, **kwargs)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.clone","title":"clone","text":"
    clone() -> DataFrame\n

    Return a copy of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef clone(self) -> polars.DataFrame:\n    \"\"\"Return a copy of `self`.\"\"\"\n    return self._get_frame().clone()\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.drop","title":"drop","text":"
    drop(\n    *columns: Union[str, Iterable[str]], strict: bool = True\n) -> DataFrame\n

    Return self with the specified columns removed.

    Source code in atomlib/atoms.py
    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n    \"\"\"Return `self` with the specified columns removed.\"\"\"\n    return self._get_frame().drop(*columns, strict=strict)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.filter","title":"filter","text":"
    filter(\n    *predicates: Union[\n        None,\n        IntoExprColumn,\n        Iterable[IntoExprColumn],\n        bool,\n        List[bool],\n        ndarray,\n    ],\n    **constraints: Any\n) -> Self\n

    Filter self, removing rows which evaluate to False.

    Source code in atomlib/atoms.py
    def filter(\n    self,\n    *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n    **constraints: t.Any,\n) -> Self:\n    \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n    # TODO clean up\n    preds_not_none = tuple(filter(lambda p: p is not None, predicates))\n    if not len(preds_not_none) and not len(constraints):\n        return self\n    return self.with_atoms(Atoms(self._get_frame().filter(*preds_not_none, **constraints), _unchecked=True))  # type: ignore\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.sort","title":"sort","text":"
    sort(\n    by: Union[IntoExpr, Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: Union[bool, Sequence[bool]] = False,\n    nulls_last: bool = False\n) -> DataFrame\n

    Sort the atoms in self by the given columns/expressions.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef sort(\n    self,\n    by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: t.Union[bool, t.Sequence[bool]] = False,\n    nulls_last: bool = False,\n) -> polars.DataFrame:\n    \"\"\"\n    Sort the atoms in `self` by the given columns/expressions.\n    \"\"\"\n    return self._get_frame().sort(\n        by, *more_by, descending=descending, nulls_last=nulls_last\n    )\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.slice","title":"slice","text":"
    slice(\n    offset: int, length: Optional[int] = None\n) -> DataFrame\n

    Return a slice of the rows in self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef slice(self, offset: int, length: t.Optional[int] = None) -> polars.DataFrame:\n    \"\"\"Return a slice of the rows in `self`.\"\"\"\n    return self._get_frame().slice(offset, length)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.head","title":"head","text":"
    head(n: int = 5) -> DataFrame\n

    Return the first n rows of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef head(self, n: int = 5) -> polars.DataFrame:\n    \"\"\"Return the first `n` rows of `self`.\"\"\"\n    return self._get_frame().head(n)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.tail","title":"tail","text":"
    tail(n: int = 5) -> DataFrame\n

    Return the last n rows of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef tail(self, n: int = 5) -> polars.DataFrame:\n    \"\"\"Return the last `n` rows of `self`.\"\"\"\n    return self._get_frame().tail(n)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.drop_nulls","title":"drop_nulls","text":"
    drop_nulls(\n    subset: Union[str, Collection[str], None] = None\n) -> DataFrame\n

    Drop rows that contain nulls in any of columns subset.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n    \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n    return self._get_frame().drop_nulls(subset)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.fill_null","title":"fill_null","text":"
    fill_null(\n    value: Any = None,\n    strategy: Optional[FillNullStrategy] = None,\n    limit: Optional[int] = None,\n    matches_supertype: bool = True,\n) -> DataFrame\n

    Fill null values in self, using the specified value or strategy.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef fill_null(\n    self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n    limit: t.Optional[int] = None, matches_supertype: bool = True,\n) -> polars.DataFrame:\n    \"\"\"Fill null values in `self`, using the specified value or strategy.\"\"\"\n    return self._get_frame().fill_null(value, strategy, limit, matches_supertype=matches_supertype)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.fill_nan","title":"fill_nan","text":"
    fill_nan(value: Union[Expr, int, float, None]) -> DataFrame\n

    Fill floating-point NaN values in self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef fill_nan(self, value: t.Union[polars.Expr, int, float, None]) -> polars.DataFrame:\n    \"\"\"Fill floating-point NaN values in `self`.\"\"\"\n    return self._get_frame().fill_nan(value)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.concat","title":"concat classmethod","text":"
    concat(\n    atoms: Union[\n        HasAtomsT,\n        IntoAtoms,\n        Iterable[Union[HasAtomsT, IntoAtoms]],\n    ],\n    *,\n    rechunk: bool = True,\n    how: ConcatMethod = \"vertical\"\n) -> HasAtomsT\n

    Concatenate multiple Atoms together, handling metadata appropriately.

    Source code in atomlib/atoms.py
    @classmethod\ndef concat(cls: t.Type[HasAtomsT],\n           atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n           rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n    \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n    # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n    # same type as ``cls``.\n    if _is_abstract(cls):\n        raise TypeError(\"concat() must be called on a concrete class.\")\n\n    if isinstance(atoms, HasAtoms):\n        atoms = (atoms,)\n    dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n    representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n    if len(dfs) == 0:\n        return representative.with_atoms(Atoms.empty(), 'local')\n\n    if how in ('vertical', 'vertical_relaxed'):\n        # get order from first member\n        cols = dfs[0].columns\n        dfs = [df.select(cols) for df in dfs]\n    elif how == 'inner':\n        cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n        schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n        if len(schema) == 0:\n            raise ValueError(\"Atoms have no columns in common\")\n\n        dfs = [_select_schema(df, schema) for df in dfs]\n        how = 'vertical'\n\n    return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.partition_by","title":"partition_by","text":"
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: bool = False\n) -> Union[List[Self], Dict[Any, Self]]\n

    Group by the given columns and partition into separate dataframes.

    Return the partitions as a dictionary by specifying as_dict=True.

    Source code in atomlib/atoms.py
    def partition_by(\n    self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n    maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n    \"\"\"\n    Group by the given columns and partition into separate dataframes.\n\n    Return the partitions as a dictionary by specifying `as_dict=True`.\n    \"\"\"\n    if as_dict:\n        d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n        return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n    return [\n        self.with_atoms(Atoms(df, _unchecked=True))\n        for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n    ]\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.select","title":"select","text":"
    select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.select)\ndef select(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr,\n) -> polars.DataFrame:\n    \"\"\"\n    Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.select_schema","title":"select_schema","text":"
    select_schema(schema: SchemaDict) -> DataFrame\n

    Select columns from self and cast to the given schema. Raises TypeError if a column is not found or if it can't be cast.

    Source code in atomlib/atoms.py
    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n    \"\"\"\n    Select columns from `self` and cast to the given schema.\n    Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n    \"\"\"\n    return _select_schema(self, schema)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.select_props","title":"select_props","text":"
    select_props(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Self\n

    Select exprs from self, while keeping required columns.

    RETURNS DESCRIPTION Self

    A HasAtoms filtered to contain the

    Self

    specified properties (as well as required columns).

    Source code in atomlib/atoms.py
    def select_props(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Self:\n    \"\"\"\n    Select `exprs` from `self`, while keeping required columns.\n\n    Returns:\n      A [`HasAtoms`][atomlib.atoms.HasAtoms] filtered to contain the\n      specified properties (as well as required columns).\n    \"\"\"\n    props = self._get_frame().lazy().select(*exprs, **named_exprs).drop(_REQUIRED_COLUMNS, strict=False).collect(_eager=True)\n    return self.with_atoms(\n        Atoms(self._get_frame().select(_REQUIRED_COLUMNS).hstack(props), _unchecked=False)\n    )\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.try_select","title":"try_select","text":"
    try_select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Optional[DataFrame]\n

    Try to select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns. Returns None if any columns are missing.

    Source code in atomlib/atoms.py
    def try_select(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr,\n) -> t.Optional[polars.DataFrame]:\n    \"\"\"\n    Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns. Returns `None` if any\n    columns are missing.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    try:\n        return self._get_frame().select(*exprs, **named_exprs)\n    except polars.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.try_get_column","title":"try_get_column","text":"
    try_get_column(name: str) -> Optional[Series]\n

    Try to get a column from self, returning None if it doesn't exist.

    Source code in atomlib/atoms.py
    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n    \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n    try:\n        return self.get_column(name)\n    except polars.exceptions.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n
    Source code in atomlib/atoms.py
    def assert_equal(self, other: t.Any):\n    assert isinstance(other, HasAtoms)\n    assert dict(self.schema) == dict(other.schema)\n    for col in self.schema.keys():\n        polars.testing.assert_series_equal(self[col], other[col], check_names=False, rtol=1e-3, atol=1e-8)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.bbox_atoms","title":"bbox_atoms","text":"
    bbox_atoms() -> BBox3D\n

    Return the bounding box of all the atoms in self.

    Source code in atomlib/atoms.py
    def bbox_atoms(self) -> BBox3D:\n    \"\"\"Return the bounding box of all the atoms in ``self``.\"\"\"\n    return BBox3D.from_pts(self.coords())\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.transform_atoms","title":"transform_atoms","text":"
    transform_atoms(\n    transform: IntoTransform3D,\n    selection: Optional[AtomSelection] = None,\n    *,\n    transform_velocities: bool = False\n) -> Self\n

    Transform the atoms in self by transform. If selection is given, only transform the atoms in selection.

    Source code in atomlib/atoms.py
    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                    transform_velocities: bool = False) -> Self:\n    \"\"\"\n    Transform the atoms in `self` by `transform`.\n    If `selection` is given, only transform the atoms in `selection`.\n    \"\"\"\n    transform = Transform3D.make(transform)\n    selection = _selection_to_numpy(self, selection)\n    transformed = self.with_coords(Transform3D.make(transform) @ self.coords(selection), selection)\n    # try to transform velocities as well\n    if transform_velocities and (velocities := self.velocities(selection)) is not None:\n        return transformed.with_velocity(transform.transform_vec(velocities), selection)\n    return transformed\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.round_near_zero","title":"round_near_zero","text":"
    round_near_zero(tol: float = 1e-14) -> Self\n

    Round atom position values near zero to zero.

    Source code in atomlib/atoms.py
    def round_near_zero(self, tol: float = 1e-14) -> Self:\n    \"\"\"\n    Round atom position values near zero to zero.\n    \"\"\"\n    return self.with_columns(coords=polars.concat_list(\n        polars.when(_coord_expr(col).abs() >= tol).then(_coord_expr(col)).otherwise(polars.lit(0.))\n        for col in range(3)\n    ).list.to_array(3))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.crop","title":"crop","text":"
    crop(\n    x_min: float = -numpy.inf,\n    x_max: float = numpy.inf,\n    y_min: float = -numpy.inf,\n    y_max: float = numpy.inf,\n    z_min: float = -numpy.inf,\n    z_max: float = numpy.inf,\n) -> Self\n

    Crop, removing all atoms outside of the specified region, inclusive.

    Source code in atomlib/atoms.py
    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf) -> Self:\n    \"\"\"\n    Crop, removing all atoms outside of the specified region, inclusive.\n    \"\"\"\n\n    return self.filter(\n        self.x().is_between(x_min, x_max, closed='both'),\n        self.y().is_between(y_min, y_max, closed='both'),\n        self.z().is_between(z_min, z_max, closed='both'),\n    )\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.deduplicate","title":"deduplicate","text":"
    deduplicate(\n    tol: float = 0.001,\n    subset: Iterable[str] = (\"x\", \"y\", \"z\", \"symbol\"),\n    keep: UniqueKeepStrategy = \"first\",\n    maintain_order: bool = True,\n) -> Self\n

    De-duplicate atoms in self. Atoms of the same symbol that are closer than tolerance to each other (by Euclidian distance) will be removed, leaving only the atom specified by keep (defaults to the first atom).

    If subset is specified, only those columns will be included while assessing duplicates. Floating point columns other than 'x', 'y', and 'z' will not by toleranced.

    Source code in atomlib/atoms.py
    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n    \"\"\"\n    De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n    to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n    `keep` (defaults to the first atom).\n\n    If `subset` is specified, only those columns will be included while assessing duplicates.\n    Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n    \"\"\"\n    import scipy.spatial\n\n    cols = set((subset,) if isinstance(subset, str) else subset)\n\n    indices = numpy.arange(len(self))\n\n    spatial_cols = cols.intersection(('x', 'y', 'z'))\n    cols -= spatial_cols\n    if len(spatial_cols) > 0:\n        coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n        tree = scipy.spatial.KDTree(coords)\n\n        # TODO This is a bad algorithm\n        while True:\n            changed = False\n            for (i, j) in tree.query_pairs(tol, 2.):\n                # whenever we encounter a pair, ensure their index matches\n                i_i, i_j = indices[[i, j]]\n                if i_i != i_j:\n                    indices[i] = indices[j] = min(i_i, i_j)\n                    changed = True\n            if not changed:\n                break\n\n        self = self.with_column(polars.Series('_unique_pts', indices))\n        cols.add('_unique_pts')\n\n    frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n    if len(spatial_cols) > 0:\n        frame = frame.drop('_unique_pts')\n\n    return self.with_atoms(Atoms(frame, _unchecked=True))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_bounds","title":"with_bounds","text":"
    with_bounds(\n    cell_size: Optional[VecLike] = None,\n    cell_origin: Optional[VecLike] = None,\n) -> \"AtomCell\"\n

    Return a periodic cell with the given orthogonal cell dimensions.

    If cell_size is not specified, it will be assumed (and may be incorrect).

    Source code in atomlib/atoms.py
    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n    \"\"\"\n    Return a periodic cell with the given orthogonal cell dimensions.\n\n    If cell_size is not specified, it will be assumed (and may be incorrect).\n    \"\"\"\n    # TODO: test this\n    from .atomcell import AtomCell\n\n    if cell_size is None:\n        warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n        cell_size = self.bbox().size\n        cell_origin = self.bbox().min\n\n    # TODO test this origin code\n    cell = Cell.from_unit_cell(cell_size)\n    if cell_origin is not None:\n        cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n    return AtomCell(self.get_atoms(), cell, frame='local')\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.coords","title":"coords","text":"
    coords(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Literal[\"local\"] = \"local\"\n) -> NDArray[float64]\n

    Return a (N, 3) ndarray of atom coordinates (dtype numpy.float64).

    Source code in atomlib/atoms.py
    def coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> NDArray[numpy.float64]:\n    \"\"\"Return a `(N, 3)` ndarray of atom coordinates (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n    df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n    return df.get_column('coords').to_numpy().astype(numpy.float64)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.x","title":"x","text":"
    x() -> Expr\n
    Source code in atomlib/atoms.py
    def x(self) -> polars.Expr:\n    return polars.col('coords').arr.get(0).alias('x')\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.y","title":"y","text":"
    y() -> Expr\n
    Source code in atomlib/atoms.py
    def y(self) -> polars.Expr:\n    return polars.col('coords').arr.get(1).alias('y')\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.z","title":"z","text":"
    z() -> Expr\n
    Source code in atomlib/atoms.py
    def z(self) -> polars.Expr:\n    return polars.col('coords').arr.get(2).alias('z')\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.velocities","title":"velocities","text":"
    velocities(\n    selection: Optional[AtomSelection] = None,\n) -> Optional[NDArray[float64]]\n

    Return a (N, 3) ndarray of atom velocities (dtype numpy.float64).

    Source code in atomlib/atoms.py
    def velocities(self, selection: t.Optional[AtomSelection] = None) -> t.Optional[NDArray[numpy.float64]]:\n    \"\"\"Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n    if 'velocity' not in self:\n        return None\n\n    df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n    return df.get_column('velocity').to_numpy().astype(numpy.float64)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.types","title":"types","text":"
    types() -> Optional[Series]\n

    Returns a Series of atom types (dtype polars.Int32).

    Source code in atomlib/atoms.py
    def types(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('type')\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.masses","title":"masses","text":"
    masses() -> Optional[Series]\n

    Returns a Series of atom masses (dtype polars.Float32).

    Source code in atomlib/atoms.py
    def masses(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('mass')\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.add_atom","title":"add_atom","text":"
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: Union[ArrayLike, float],\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    **kwargs: Any,\n) -> Self\n

    Return a copy of self with an extra atom.

    By default, all extra columns present in self must be specified as **kwargs.

    Try to avoid calling this in a loop (Use HasAtoms.concat instead).

    Source code in atomlib/atoms.py
    def add_atom(self, elem: t.Union[int, str], /,\n             x: t.Union[ArrayLike, float],\n             y: t.Optional[float] = None,\n             z: t.Optional[float] = None,\n             **kwargs: t.Any) -> Self:\n    \"\"\"\n    Return a copy of `self` with an extra atom.\n\n    By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n    Try to avoid calling this in a loop (Use [`HasAtoms.concat`][atomlib.atoms.HasAtoms.concat] instead).\n    \"\"\"\n    if isinstance(elem, int):\n        kwargs.update(elem=elem)\n    else:\n        kwargs.update(symbol=elem)\n    if hasattr(x, '__len__') and len(x) > 1:  # type: ignore\n        (x, y, z) = to_vec3(x)\n    elif y is None or z is None:\n        raise ValueError(\"Must specify vector of positions or x, y, & z.\")\n\n    sym = get_sym(elem) if isinstance(elem, int) else elem\n    d: t.Dict[str, t.Any] = {'x': x, 'y': y, 'z': z, 'symbol': sym, **kwargs}\n    return self.concat(\n        (self, Atoms(d).select_schema(self.schema)),\n        how='vertical'\n    )\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.pos","title":"pos","text":"
    pos(\n    x: Union[Sequence[Optional[float]], float, None] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n

    Select all atoms at a given position.

    Formally, returns all atoms within a cube of radius tol centered at (x,y,z), exclusive of the cube's surface.

    Additional parameters given as kwargs will be checked as additional parameters (with strict equality).

    Source code in atomlib/atoms.py
    def pos(self,\n        x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n        y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n        tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n    \"\"\"\n    Select all atoms at a given position.\n\n    Formally, returns all atoms within a cube of radius ``tol``\n    centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n    Additional parameters given as ``kwargs`` will be checked\n    as additional parameters (with strict equality).\n    \"\"\"\n\n    if isinstance(x, t.Sequence):\n        (x, y, z) = x\n\n    tol = abs(float(tol))\n    selection = polars.lit(True)\n    if x is not None:\n        selection &= self.x().is_between(x - tol, x + tol, closed='none')\n    if y is not None:\n        selection &= self.y().is_between(y - tol, y + tol, closed='none')\n    if z is not None:\n        selection &= self.z().is_between(z - tol, z + tol, closed='none')\n    for (col, val) in kwargs.items():\n        selection &= (polars.col(col) == val)\n\n    return selection\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_index","title":"with_index","text":"
    with_index(index: Optional[AtomValues] = None) -> Self\n

    Returns self with a row index added in column 'i' (dtype polars.Int64). If index is not specified, defaults to an existing index or a new index.

    Source code in atomlib/atoms.py
    def with_index(self, index: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n    If `index` is not specified, defaults to an existing index or a new index.\n    \"\"\"\n    if index is None and 'i' in self.columns:\n        return self\n    if index is None:\n        index = numpy.arange(len(self), dtype=numpy.int64)\n    return self.with_column(_values_to_expr(self, index, polars.Int64).alias('i'))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_wobble","title":"with_wobble","text":"
    with_wobble(wobble: Optional[AtomValues] = None) -> Self\n

    Return self with the given displacements in column 'wobble' (dtype polars.Float64). If wobble is not specified, defaults to the already-existing wobbles or 0.

    Source code in atomlib/atoms.py
    def with_wobble(self, wobble: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n    \"\"\"\n    if wobble is None and 'wobble' in self.columns:\n        return self\n    wobble = 0. if wobble is None else wobble\n    return self.with_column(_values_to_expr(self, wobble, polars.Float64).alias('wobble'))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_occupancy","title":"with_occupancy","text":"
    with_occupancy(\n    frac_occupancy: Optional[AtomValues] = None,\n) -> Self\n

    Return self with the given fractional occupancies (dtype polars.Float64). If frac_occupancy is not specified, defaults to the already-existing occupancies or 1.

    Source code in atomlib/atoms.py
    def with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n    \"\"\"\n    if frac_occupancy is None and 'frac_occupancy' in self.columns:\n        return self\n    frac_occupancy = 1. if frac_occupancy is None else frac_occupancy\n    return self.with_column(_values_to_expr(self, frac_occupancy, polars.Float64).alias('frac_occupancy'))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.apply_wobble","title":"apply_wobble","text":"
    apply_wobble(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    Displace the atoms in self by the amount in the wobble column. wobble is interpretated as a mean-squared displacement, which is distributed equally over each axis.

    Source code in atomlib/atoms.py
    def apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    Displace the atoms in `self` by the amount in the `wobble` column.\n    `wobble` is interpretated as a mean-squared displacement, which is distributed\n    equally over each axis.\n    \"\"\"\n    if 'wobble' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    stddev = self.select((polars.col('wobble') / 3.).sqrt()).to_series().to_numpy()\n    coords = self.coords()\n    coords += stddev[:, None] * rng.standard_normal(coords.shape)\n    return self.with_coords(coords)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.apply_occupancy","title":"apply_occupancy","text":"
    apply_occupancy(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    For each atom in self, use its frac_occupancy to randomly decide whether to remove it.

    Source code in atomlib/atoms.py
    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n    \"\"\"\n    if 'frac_occupancy' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    frac = self.select('frac_occupancy').to_series().to_numpy()\n    choice = rng.binomial(1, frac).astype(numpy.bool_)\n    return self.filter(polars.lit(choice))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_type","title":"with_type","text":"
    with_type(types: Optional[AtomValues] = None) -> Self\n

    Return self with the given atom types in column 'type'. If types is not specified, use the already existing types or auto-assign them.

    When auto-assigning, each symbol is given a unique value, case-sensitive. Values are assigned from lowest atomic number to highest. For instance: [\"Ag+\", \"Na\", \"H\", \"Ag\"] => [3, 11, 1, 2]

    Source code in atomlib/atoms.py
    def with_type(self, types: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom types in column 'type'.\n    If `types` is not specified, use the already existing types or auto-assign them.\n\n    When auto-assigning, each symbol is given a unique value, case-sensitive.\n    Values are assigned from lowest atomic number to highest.\n    For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n    \"\"\"\n    if types is not None:\n        return self.with_columns(type=_values_to_expr(self, types, polars.Int32))\n    if 'type' in self.columns:\n        return self\n\n    unique = Atoms(self._get_frame().unique(maintain_order=False, subset=['elem', 'symbol']).sort(['elem', 'symbol']), _unchecked=True)\n    new = self.with_column(polars.Series('type', values=numpy.zeros(len(self)), dtype=polars.Int32))\n\n    logging.warning(\"Auto-assigning element types\")\n    for (i, (elem, sym)) in enumerate(unique.select(('elem', 'symbol')).rows()):\n        print(f\"Assigning type {i+1} to element '{sym}'\")\n        new = new.with_column(polars.when((polars.col('elem') == elem) & (polars.col('symbol') == sym))\n                                    .then(polars.lit(i+1))\n                                    .otherwise(polars.col('type'))\n                                    .alias('type'))\n\n    assert (new.get_column('type') == 0).sum() == 0\n    return new\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_mass","title":"with_mass","text":"
    with_mass(mass: Optional[ArrayLike] = None) -> Self\n

    Return self with the given atom masses in column 'mass'. If mass is not specified, use the already existing masses or auto-assign them.

    Source code in atomlib/atoms.py
    def with_mass(self, mass: t.Optional[ArrayLike] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom masses in column `'mass'`.\n    If `mass` is not specified, use the already existing masses or auto-assign them.\n    \"\"\"\n    if mass is not None:\n        return self.with_column(_values_to_expr(self, mass, polars.Float32).alias('mass'))\n    if 'mass' in self.columns:\n        return self\n\n    unique_elems = self.get_column('elem').unique()\n    new = self.with_column(polars.Series('mass', values=numpy.zeros(len(self)), dtype=polars.Float32))\n\n    logging.warning(\"Auto-assigning element masses\")\n    for elem in unique_elems:\n        new = new.with_column(polars.when(polars.col('elem') == elem)\n                                    .then(polars.lit(get_mass(elem)))\n                                    .otherwise(polars.col('mass'))\n                                    .alias('mass'))\n\n    assert (new.get_column('mass').abs() < 1e-10).sum() == 0\n    return new\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_symbol","title":"with_symbol","text":"
    with_symbol(\n    symbols: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n) -> Self\n

    Return self with the given atomic symbols.

    Source code in atomlib/atoms.py
    def with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atomic symbols.\n    \"\"\"\n    if selection is not None:\n        selection = _selection_to_numpy(self, selection)\n        new_symbols = self.get_column('symbol')\n        new_symbols[selection] = polars.Series(list(numpy.broadcast_to(symbols, len(selection))), dtype=polars.Utf8)\n        symbols = new_symbols\n\n    # TODO better cast here\n    symbols = polars.Series('symbol', list(numpy.broadcast_to(symbols, len(self))), dtype=polars.Utf8)\n    return self.with_columns((symbols, get_elem(symbols)))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_coords","title":"with_coords","text":"
    with_coords(\n    pts: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Literal[\"local\"] = \"local\"\n) -> Self\n

    Return self replaced with the given atomic positions.

    Source code in atomlib/atoms.py
    def with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic positions.\n    \"\"\"\n    if selection is not None:\n        selection = _selection_to_numpy(self, selection)\n        new_pts = self.coords()\n        pts = numpy.atleast_2d(pts)\n        assert pts.shape[-1] == 3\n        new_pts[selection] = pts\n        pts = new_pts\n\n    # https://github.com/pola-rs/polars/issues/18369\n    pts = numpy.broadcast_to(pts, (len(self), 3)) if len(self) else []\n    return self.with_columns(polars.Series('coords', pts, polars.Array(polars.Float64, 3)))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_velocity","title":"with_velocity","text":"
    with_velocity(\n    pts: Optional[ArrayLike] = None,\n    selection: Optional[AtomSelection] = None,\n) -> Self\n

    Return self replaced with the given atomic velocities. If pts is not specified, use the already existing velocities or zero.

    Source code in atomlib/atoms.py
    def with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                  selection: t.Optional[AtomSelection] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic velocities.\n    If `pts` is not specified, use the already existing velocities or zero.\n    \"\"\"\n    if pts is None:\n        if 'velocity' in self:\n            return self\n        all_pts = numpy.zeros((len(self), 3))\n    else:\n        all_pts = self['velocity'].to_numpy()\n\n    if selection is None:\n        all_pts = pts or all_pts\n    elif pts is not None:\n        selection = _selection_to_numpy(self, selection)\n        all_pts = numpy.require(all_pts, requirements=['WRITEABLE'])\n        pts = numpy.atleast_2d(pts)\n        assert pts.shape[-1] == 3\n        all_pts[selection] = pts\n\n    all_pts = numpy.broadcast_to(all_pts, (len(self), 3))\n    return self.with_columns(polars.Series('velocity', all_pts, polars.Array(polars.Float64, 3)))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.read","title":"read classmethod","text":"
    read(\n    path: FileOrPath, ty: Optional[FileType] = None\n) -> HasAtomsT\n

    Read a structure from a file.

    Supported types can be found in the io module. If no ty is specified, it is inferred from the file's extension.

    Source code in atomlib/mixins.py
    @classmethod\ndef read(cls: t.Type[HasAtomsT], path: FileOrPath, ty: t.Optional[FileType] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a file.\n\n    Supported types can be found in the [io][atomlib.io] module.\n    If no `ty` is specified, it is inferred from the file's extension.\n    \"\"\"\n    from .io import read\n    return _cast_atoms(read(path, ty), cls)  # type: ignore\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.read_cif","title":"read_cif classmethod","text":"
    read_cif(\n    f: Union[FileOrPath, CIF, CIFDataBlock],\n    block: Union[int, str, None] = None,\n) -> HasAtomsT\n

    Read a structure from a CIF file.

    If block is specified, read data from the given block of the CIF file (index or name).

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cif(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CIF, CIFDataBlock], block: t.Union[int, str, None] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a CIF file.\n\n    If `block` is specified, read data from the given block of the CIF file (index or name).\n    \"\"\"\n    from .io import read_cif\n    return _cast_atoms(read_cif(f, block), cls)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.read_xyz","title":"read_xyz classmethod","text":"
    read_xyz(f: Union[FileOrPath, XYZ]) -> HasAtomsT\n

    Read a structure from an XYZ file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xyz(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XYZ]) -> HasAtomsT:\n    \"\"\"Read a structure from an XYZ file.\"\"\"\n    from .io import read_xyz\n    return _cast_atoms(read_xyz(f), cls)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.read_xsf","title":"read_xsf classmethod","text":"
    read_xsf(f: Union[FileOrPath, XSF]) -> HasAtomsT\n

    Read a structure from an XSF file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xsf(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XSF]) -> HasAtomsT:\n    \"\"\"Read a structure from an XSF file.\"\"\"\n    from .io import read_xsf\n    return _cast_atoms(read_xsf(f), cls)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.read_cfg","title":"read_cfg classmethod","text":"
    read_cfg(f: Union[FileOrPath, CFG]) -> HasAtomsT\n

    Read a structure from a CFG file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cfg(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CFG]) -> HasAtomsT:\n    \"\"\"Read a structure from a CFG file.\"\"\"\n    from .io import read_cfg\n    return _cast_atoms(read_cfg(f), cls)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.read_lmp","title":"read_lmp classmethod","text":"
    read_lmp(\n    f: Union[FileOrPath, LMP],\n    type_map: Optional[Dict[int, Union[str, int]]] = None,\n) -> HasAtomsT\n

    Read a structure from a LAAMPS data file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_lmp(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, LMP], type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> HasAtomsT:\n    \"\"\"Read a structure from a LAAMPS data file.\"\"\"\n    from .io import read_lmp\n    return _cast_atoms(read_lmp(f, type_map=type_map), cls)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.write_cif","title":"write_cif","text":"
    write_cif(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cif(self, f: FileOrPath):\n    from .io import write_cif\n    write_cif(self, f)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.write_xyz","title":"write_xyz","text":"
    write_xyz(f: FileOrPath, fmt: XYZFormat = 'exyz')\n
    Source code in atomlib/mixins.py
    def write_xyz(self, f: FileOrPath, fmt: XYZFormat = 'exyz'):\n    from .io import write_xyz\n    write_xyz(self, f, fmt)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.write_xsf","title":"write_xsf","text":"
    write_xsf(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_xsf(self, f: FileOrPath):\n    from .io import write_xsf\n    write_xsf(self, f)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.write_cfg","title":"write_cfg","text":"
    write_cfg(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cfg(self, f: FileOrPath):\n    from .io import write_cfg\n    write_cfg(self, f)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.write_lmp","title":"write_lmp","text":"
    write_lmp(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_lmp(self, f: FileOrPath):\n    from .io import write_lmp\n    write_lmp(self, f)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.write","title":"write","text":"
    write(path: FileOrPath, ty: Optional[FileType] = None)\n

    Write this structure to a file.

    A file type may be specified using ty. If no ty is specified, it is inferred from the path's extension.

    Source code in atomlib/mixins.py
    def write(self, path: FileOrPath, ty: t.Optional[FileType] = None):\n    \"\"\"\n    Write this structure to a file.\n\n    A file type may be specified using `ty`.\n    If no `ty` is specified, it is inferred from the path's extension.\n    \"\"\"\n    from .io import write\n    write(self, path, ty)  # type: ignore\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.empty","title":"empty staticmethod","text":"
    empty() -> Atoms\n

    Return an empty Atoms with only the mandatory columns.

    Source code in atomlib/atoms.py
    @staticmethod\ndef empty() -> Atoms:\n    \"\"\"\n    Return an empty Atoms with only the mandatory columns.\n    \"\"\"\n    return Atoms()\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.get_atoms","title":"get_atoms","text":"
    get_atoms(frame: Literal['local'] = 'local') -> Atoms\n
    Source code in atomlib/atoms.py
    def get_atoms(self, frame: t.Literal['local'] = 'local') -> Atoms:\n    if frame != 'local':\n        raise ValueError(f\"Atoms without a cell only support the 'local' coordinate frame, not '{frame}'.\")\n    return self\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_atoms","title":"with_atoms","text":"
    with_atoms(\n    atoms: HasAtoms, frame: Literal[\"local\"] = \"local\"\n) -> Atoms\n
    Source code in atomlib/atoms.py
    def with_atoms(self, atoms: HasAtoms, frame: t.Literal['local'] = 'local') -> Atoms:\n    if frame != 'local':\n        raise ValueError(f\"Atoms without a cell only support the 'local' coordinate frame, not '{frame}'.\")\n    return atoms.get_atoms()\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.bbox","title":"bbox","text":"
    bbox() -> BBox3D\n

    Return the bounding box of all the points in self.

    Source code in atomlib/atoms.py
    def bbox(self) -> BBox3D:\n    \"\"\"Return the bounding box of all the points in `self`.\"\"\"\n    if self._bbox is None:\n        self._bbox = BBox3D.from_pts(self.coords())\n\n    return self._bbox\n
    "},{"location":"api/bbox/","title":"atomlib.bbox","text":"

    Bounding boxes

    "},{"location":"api/bbox/#atomlib.bbox.BBox3D","title":"BBox3D","text":"

    3D axis-aligned bounding box, with corners min and max.

    Source code in atomlib/bbox.py
    class BBox3D:\n    \"\"\"\n    3D axis-aligned bounding box, with corners `min` and `max`.\n    \"\"\"\n\n    def __init__(self, min: VecLike, max: VecLike):\n        # shape: (3, 2)\n        # self._inner[1, 0]: min of y\n        min, max = to_vec3(min), to_vec3(max)\n        self.inner: numpy.ndarray = numpy.stack((min, max), axis=-1)\n\n    @classmethod\n    def from_pts(cls, pts: t.Union[numpy.ndarray, t.Sequence[Vec3]]) -> BBox3D:\n        \"\"\"Construct the minimum bounding box containing the points `pts`.\"\"\"\n        pts = numpy.atleast_2d(pts).reshape(-1, 3)\n        return cls(numpy.nanmin(pts, axis=0), numpy.nanmax(pts, axis=0))\n\n    @classmethod\n    def unit(cls) -> BBox3D:\n        \"\"\"Return a unit bbox (cube from [0,0,0] to [1,1,1]).\"\"\"\n        return cls([0., 0., 0.], [1., 1., 1.])\n\n    def transform_from_unit(self) -> AffineTransform3D:\n        \"\"\"Return the transform which transforms a unit bbox to `self`.\"\"\"\n        from .transform import AffineTransform3D\n        return AffineTransform3D.translate(self.min).scale(self.max - self.min)\n\n    def transform_to_unit(self) -> AffineTransform3D:\n        \"\"\"Return the transform which transforms `self` to a unit bbox.\"\"\"\n        return self.transform_from_unit().inverse()\n\n    @property\n    def min(self) -> Vec3:\n        \"\"\"Return the minimum corner `[xmin, ymin, zmin]`.\"\"\"\n        return self.inner[:, 0]\n\n    @property\n    def max(self) -> Vec3:\n        \"\"\"Return the minimum corner `[xmax, ymax, zmax]`.\"\"\"\n        return self.inner[:, 1]\n\n    @property\n    def x(self) -> numpy.ndarray:\n        \"\"\"Return the interval `[xmin, xmax]`.\"\"\"\n        return self.inner[0]\n\n    @property\n    def y(self) -> numpy.ndarray:\n        \"\"\"Return the interval `[ymin, ymax]`.\"\"\"\n        return self.inner[1]\n\n    @property\n    def z(self) -> numpy.ndarray:\n        \"\"\"Return the interval `[zmin, zmax]`.\"\"\"\n        return self.inner[2]\n\n    @property\n    def size(self) -> Vec3:\n        \"\"\"Return the size `[xsize, ysize, zsize]`.\"\"\"\n        return self.max - self.min\n\n    def volume(self) -> float:\n        \"\"\"Return the volume of the bbox.\"\"\"\n        return float(numpy.prod(self.size))\n\n    def corners(self) -> numpy.ndarray:\n        \"\"\"Return a (8, 3) ndarray containing the corners of the bbox.\"\"\"\n        return numpy.stack(list(map(numpy.ravel, numpy.meshgrid(*self.inner))), axis=-1)\n\n    def pad(self, amount: t.Union[float, VecLike]) -> BBox3D:\n        \"\"\"\n        Pad the given bbox by `amount`. If a vector `[x, y, z]` is given, pad each axis by the given amount.\n        \"\"\"\n        amount_v = numpy.broadcast_to(amount, 3)\n\n        return type(self)(\n            self.min - amount_v,\n            self.max + amount_v\n        )\n\n    def __or__(self, other: t.Union[Vec3, BBox3D]) -> BBox3D:\n        \"\"\"\n        Union this bbox with another point or bbox.\n        \"\"\"\n        if isinstance(other, numpy.ndarray):\n            return self.from_pts((self.min, self.max, other))\n\n        return type(self)(\n            numpy.nanmin(((self.min, other.min)), axis=0),\n            numpy.nanmax(((self.max, other.max)), axis=0),\n        )\n\n    __ror__ = __or__\n\n    def __and__(self, other: BBox3D) -> BBox3D:\n        \"\"\"\n        Intersect this bbox with another point or bbox.\n\n        Undefined if there is no overlap between the two.\n        \"\"\"\n        return type(self)(\n            numpy.nanmax(((self.min, other.min)), axis=0),\n            numpy.nanmin(((self.max, other.max)), axis=0),\n        )\n\n    def __repr__(self) -> str:\n        return f\"BBox({self.min}, {self.max})\"\n
    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.inner","title":"inner instance-attribute","text":"
    inner: ndarray = stack((min, max), axis=-1)\n
    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.min","title":"min property","text":"
    min: Vec3\n

    Return the minimum corner [xmin, ymin, zmin].

    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.max","title":"max property","text":"
    max: Vec3\n

    Return the minimum corner [xmax, ymax, zmax].

    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.x","title":"x property","text":"
    x: ndarray\n

    Return the interval [xmin, xmax].

    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.y","title":"y property","text":"
    y: ndarray\n

    Return the interval [ymin, ymax].

    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.z","title":"z property","text":"
    z: ndarray\n

    Return the interval [zmin, zmax].

    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.size","title":"size property","text":"
    size: Vec3\n

    Return the size [xsize, ysize, zsize].

    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.from_pts","title":"from_pts classmethod","text":"
    from_pts(pts: Union[ndarray, Sequence[Vec3]]) -> BBox3D\n

    Construct the minimum bounding box containing the points pts.

    Source code in atomlib/bbox.py
    @classmethod\ndef from_pts(cls, pts: t.Union[numpy.ndarray, t.Sequence[Vec3]]) -> BBox3D:\n    \"\"\"Construct the minimum bounding box containing the points `pts`.\"\"\"\n    pts = numpy.atleast_2d(pts).reshape(-1, 3)\n    return cls(numpy.nanmin(pts, axis=0), numpy.nanmax(pts, axis=0))\n
    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.unit","title":"unit classmethod","text":"
    unit() -> BBox3D\n

    Return a unit bbox (cube from [0,0,0] to [1,1,1]).

    Source code in atomlib/bbox.py
    @classmethod\ndef unit(cls) -> BBox3D:\n    \"\"\"Return a unit bbox (cube from [0,0,0] to [1,1,1]).\"\"\"\n    return cls([0., 0., 0.], [1., 1., 1.])\n
    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.transform_from_unit","title":"transform_from_unit","text":"
    transform_from_unit() -> AffineTransform3D\n

    Return the transform which transforms a unit bbox to self.

    Source code in atomlib/bbox.py
    def transform_from_unit(self) -> AffineTransform3D:\n    \"\"\"Return the transform which transforms a unit bbox to `self`.\"\"\"\n    from .transform import AffineTransform3D\n    return AffineTransform3D.translate(self.min).scale(self.max - self.min)\n
    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.transform_to_unit","title":"transform_to_unit","text":"
    transform_to_unit() -> AffineTransform3D\n

    Return the transform which transforms self to a unit bbox.

    Source code in atomlib/bbox.py
    def transform_to_unit(self) -> AffineTransform3D:\n    \"\"\"Return the transform which transforms `self` to a unit bbox.\"\"\"\n    return self.transform_from_unit().inverse()\n
    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.volume","title":"volume","text":"
    volume() -> float\n

    Return the volume of the bbox.

    Source code in atomlib/bbox.py
    def volume(self) -> float:\n    \"\"\"Return the volume of the bbox.\"\"\"\n    return float(numpy.prod(self.size))\n
    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.corners","title":"corners","text":"
    corners() -> ndarray\n

    Return a (8, 3) ndarray containing the corners of the bbox.

    Source code in atomlib/bbox.py
    def corners(self) -> numpy.ndarray:\n    \"\"\"Return a (8, 3) ndarray containing the corners of the bbox.\"\"\"\n    return numpy.stack(list(map(numpy.ravel, numpy.meshgrid(*self.inner))), axis=-1)\n
    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.pad","title":"pad","text":"
    pad(amount: Union[float, VecLike]) -> BBox3D\n

    Pad the given bbox by amount. If a vector [x, y, z] is given, pad each axis by the given amount.

    Source code in atomlib/bbox.py
    def pad(self, amount: t.Union[float, VecLike]) -> BBox3D:\n    \"\"\"\n    Pad the given bbox by `amount`. If a vector `[x, y, z]` is given, pad each axis by the given amount.\n    \"\"\"\n    amount_v = numpy.broadcast_to(amount, 3)\n\n    return type(self)(\n        self.min - amount_v,\n        self.max + amount_v\n    )\n
    "},{"location":"api/cell/","title":"atomlib.cell","text":"

    Crystallographic unit cell.

    This module defines HasCell and the concrete Cell, the core types for dealing with crystallographic unit cells and their associated coordinate frames.

    "},{"location":"api/cell/#atomlib.cell.CoordinateFrame","title":"CoordinateFrame module-attribute","text":"
    CoordinateFrame: TypeAlias = Literal[\n    \"cell\",\n    \"cell_frac\",\n    \"cell_box\",\n    \"ortho\",\n    \"ortho_frac\",\n    \"ortho_box\",\n    \"linear\",\n    \"local\",\n    \"global\",\n]\n

    A coordinate frame to use.

    • cell: Real-space units along crystal axes
    • cell_frac: Fraction of unit cells
    • cell_box: Fraction of cell box
    • ortho: Real-space units along orthogonal cell
    • ortho_frac: Fraction of orthogonal cell
    • ortho_box: Fraction of orthogonal box
    • linear: Angstroms in local coordinate system (without affine transformation)
    • local: Angstroms in local coordinate system (with affine transformation)
    • global: Angstroms in global coordinate system (with all transformations)

    For more information, see the documentation at Coordinate systems, or the example notebook at examples/coords.ipynb.

    "},{"location":"api/cell/#atomlib.cell.HasCellT","title":"HasCellT module-attribute","text":"
    HasCellT = TypeVar('HasCellT', bound='HasCell')\n
    "},{"location":"api/cell/#atomlib.cell.HasCell","title":"HasCell","text":"Source code in atomlib/cell.py
    class HasCell:\n    # abstract methods\n\n    @abc.abstractmethod\n    def get_cell(self) -> Cell:\n        \"\"\"Get the cell contained in ``self``. This should be a low cost method.\"\"\"\n        ...\n\n    @abc.abstractmethod\n    def with_cell(self: HasCellT, cell: Cell) -> HasCellT:\n        \"\"\"Replace the cell in ``self`` with ``cell``.\"\"\"\n        ...\n\n    # getters\n\n    @property\n    def affine(self) -> AffineTransform3D:\n        \"\"\"\n        Affine transformation. Holds transformation from 'ortho' to 'local' coordinates,\n        including rotation away from the standard crystal orientation.\n        \"\"\"\n        return self.get_cell()._affine\n\n    @property\n    def ortho(self) -> LinearTransform3D:\n        \"\"\"\n        Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.\n        \"\"\"\n        return self.get_cell()._ortho\n\n    @property\n    def metric(self) -> LinearTransform3D:\n        r\"\"\"\n        Cell metric tensor\n\n        Returns the dot product between every combination of basis vectors.\n        :math:`\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j`\n        \"\"\"\n        ortho = self.get_cell()._ortho.scale(self.cell_size)\n        return ortho.T @ ortho\n\n    @property\n    def cell_size(self) -> NDArray[numpy.float64]:\n        \"\"\"Unit cell size.\"\"\"\n        return self.get_cell()._cell_size\n\n    @property\n    def cell_angle(self) -> NDArray[numpy.float64]:\n        \"\"\"Unit cell angles, in radians.\"\"\"\n        return self.get_cell()._cell_angle\n\n    @property\n    def n_cells(self) -> NDArray[numpy.int_]:\n        \"\"\"Number of unit cells.\"\"\"\n        return self.get_cell()._n_cells\n\n    @property\n    def pbc(self) -> NDArray[numpy.bool_]:\n        \"\"\"Flags indicating the presence of periodic boundary conditions along each axis.\"\"\"\n        return self.get_cell()._pbc\n\n    @property\n    def ortho_size(self) -> NDArray[numpy.float64]:\n        \"\"\"\n        Return size of orthogonal unit cell.\n\n        Equivalent to the diagonal of the orthogonalization matrix.\n        \"\"\"\n        return self.cell_size * numpy.diag(self.ortho.inner)\n\n    @property\n    def box_size(self) -> NDArray[numpy.float64]:\n        \"\"\"\n        Return size of the cell box.\n\n        Equivalent to ``self.n_cells * self.cell_size``.\n        \"\"\"\n        return self.n_cells * self.cell_size\n\n    # get transforms\n\n    def _get_transform_to_local(self, frame: CoordinateFrame) -> AffineTransform3D:\n        \"\"\"Get the transform from `frame` to local coordinates.\"\"\"\n        frame = t.cast(CoordinateFrame, frame.lower())\n\n        if frame == 'local' or frame == 'global':\n            return LinearTransform3D()\n\n        if frame == 'linear':\n            return self.affine.to_translation()\n\n        if frame.startswith('cell'):\n            transform = self.affine @ self.ortho\n            cell_size = self.cell_size\n        elif frame.startswith('ortho'):\n            transform = self.affine\n            cell_size = self.ortho_size\n        else:\n            raise ValueError(f\"Unknown coordinate frame '{frame}'\")\n\n        if '_' not in frame:\n            return transform\n        end = frame.split('_', 2)[1]\n        if end == 'frac':\n            return transform @ LinearTransform3D.scale(cell_size)\n        if end == 'box':\n            return transform @ LinearTransform3D.scale(cell_size * self.n_cells)\n        raise ValueError(f\"Unknown coordinate frame '{frame}'\")\n\n    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n        \"\"\"\n        In the two-argument form, get the transform to `frame_to` from `frame_from`.\n        In the one-argument form, get the transform from local coordinates to 'frame'.\n        \"\"\"\n        transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n        transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n        if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n            return AffineTransform3D()\n        return transform_to.inverse() @ transform_from\n\n    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n        corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n        return self.get_transform(frame, 'cell_box') @ corners\n\n    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n        \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n        return BBox3D.from_pts(self.corners(frame))\n\n    bbox = bbox_cell\n\n    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n        \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n        return self.ortho.is_diagonal(tol=tol)\n\n    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n        \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n        transform = (self.affine @ self.ortho).to_linear()\n        if not transform.is_scaled_orthogonal(tol):\n            return False\n        normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n        # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n        return all(\n            any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n            for row in normed\n        )\n\n    def _cell_size_in_local(self) -> Vec3:\n        \"\"\"Calculate cell_size in the local coordinate system. Assumes `self.is_orthogonal_in_local()`.\"\"\"\n        return numpy.abs(self.get_transform('local', 'ortho').transform_vec(self.cell_size))\n\n    def _box_size_in_local(self) -> Vec3:\n        \"\"\"Calculate box_size in the local coordinate system. Assumes `self.is_orthogonal_in_local()`.\"\"\"\n        return numpy.abs(self.get_transform('local', 'ortho').transform_vec(self.box_size))\n\n    def _n_cells_in_local(self) -> NDArray[numpy.int_]:\n        \"\"\"Calculate n_cells after any local rotation. Assumes `self.is_orthogonal_in_local()`.\"\"\"\n        return numpy.abs(numpy.round(self.get_transform('local', 'ortho').transform_vec(self.n_cells)).astype(int))\n\n    def to_ortho(self) -> AffineTransform3D:\n        return self.get_transform('local', 'cell_box')\n\n    def transform_cell(self: HasCellT, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> HasCellT:\n        \"\"\"\n        Apply the given transform to the unit cell, and return a new `Cell`.\n        The transform is applied in coordinate frame 'frame'.\n        Orthogonal and affine transformations are applied to the affine matrix component,\n        while skew and scaling is applied to the orthogonalization matrix/cell_size.\n        \"\"\"\n        transform = t.cast(AffineTransform3D, self.change_transform(transform, 'local', frame))\n        if not transform.to_linear().is_orthogonal():\n            raise NotImplementedError()\n        return self.with_cell(Cell(\n            affine=transform @ self.affine,\n            ortho=self.ortho,\n            cell_size=self.cell_size,\n            cell_angle=self.cell_angle,\n            n_cells=self.n_cells,\n            pbc=self.pbc,\n        ))\n\n    def strain_orthogonal(self: HasCellT) -> HasCellT:\n        \"\"\"\n        Orthogonalize using strain.\n\n        Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n        For small displacements, no hydrostatic strain is applied (volume is conserved).\n        \"\"\"\n        return self.with_cell(Cell(\n            affine=self.affine,\n            ortho=LinearTransform3D(),\n            cell_size=self.cell_size,\n            n_cells=self.n_cells,\n            pbc=self.pbc,\n        ))\n\n    def repeat(self: HasCellT, n: t.Union[int, VecLike]) -> HasCellT:\n        \"\"\"Tile the cell by `n` in each dimension.\"\"\"\n        ns = numpy.broadcast_to(n, 3)\n        if not numpy.issubdtype(ns.dtype, numpy.integer):\n            raise ValueError(\"repeat() argument must be an integer or integer array.\")\n        return self.with_cell(Cell(\n            affine=self.affine,\n            ortho=self.ortho,\n            cell_size=self.cell_size,\n            cell_angle=self.cell_angle,\n            n_cells=self.n_cells * numpy.broadcast_to(n, 3),\n            pbc = self.pbc | (ns > 1)  # assume periodic along repeated directions\n        ))\n\n    def explode(self: HasCellT) -> HasCellT:\n        \"\"\"Materialize repeated cells as one supercell.\"\"\"\n        return self.with_cell(Cell(\n            affine=self.affine,\n            ortho=self.ortho,\n            cell_size=self.cell_size*self.n_cells,\n            cell_angle=self.cell_angle,\n            pbc=self.pbc,\n        ))\n\n    def explode_z(self: HasCellT) -> HasCellT:\n        \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n        return self.with_cell(Cell(\n            affine=self.affine,\n            ortho=self.ortho,\n            cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n            n_cells=[*self.n_cells[:2], 1],\n            cell_angle=self.cell_angle,\n            pbc=self.pbc,\n        ))\n\n    def crop(self: HasCellT, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n             y_min: float = -numpy.inf, y_max: float = numpy.inf,\n             z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n             frame: CoordinateFrame = 'local') -> HasCellT:\n        \"\"\"\n        Crop `self` to the given extents. For a non-orthogonal\n        cell, this must be specified in cell coordinates. This\n        function implicity `explode`s the cell as well.\n        \"\"\"\n\n        if not frame.lower().startswith('cell'):\n            if not self.is_orthogonal():\n                raise ValueError(\"Cannot crop a non-orthogonal cell in orthogonal coordinates. Use crop_atoms instead.\")\n\n        min = to_vec3([x_min, y_min, z_min])\n        max = to_vec3([x_max, y_max, z_max])\n        (min, max) = self.get_transform('cell_box', frame).transform([min, max])\n        new_box = BBox3D(min, max) & BBox3D.unit()\n        cropped = (new_box.min > 0.) | (new_box.max < 1.)\n\n        return self.with_cell(Cell(\n            affine=self.affine @ AffineTransform3D.translate(-new_box.min),\n            ortho=self.ortho,\n            cell_size=new_box.size * self.cell_size * numpy.where(cropped, self.n_cells, 1),\n            n_cells=numpy.where(cropped, 1, self.n_cells),\n            cell_angle=self.cell_angle,\n            pbc=self.pbc & ~cropped  # remove periodicity along cropped directions\n        ))\n\n    @t.overload\n    def change_transform(self, transform: AffineTransform3D,\n                         frame_to: t.Optional[CoordinateFrame] = None,\n                         frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n        ...\n\n    @t.overload\n    def change_transform(self, transform: Transform3D,\n                         frame_to: t.Optional[CoordinateFrame] = None,\n                         frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n        ...\n\n    def change_transform(self, transform: Transform3D,\n                         frame_to: t.Optional[CoordinateFrame] = None,\n                         frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n        \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n        if frame_to == frame_from and frame_to is not None:\n            return transform\n        coord_change = self.get_transform(frame_to, frame_from)\n        return coord_change @ transform @ coord_change.inverse()\n\n    def assert_equal(self, other: t.Any):\n        assert isinstance(other, HasCell) and type(self) is type(other)\n        numpy.testing.assert_array_almost_equal(self.affine.inner, other.affine.inner, 6)\n        numpy.testing.assert_array_almost_equal(self.ortho.inner, other.ortho.inner, 6)\n        numpy.testing.assert_array_almost_equal(self.cell_size, other.cell_size, 6)\n        numpy.testing.assert_array_equal(self.n_cells, other.n_cells)\n        numpy.testing.assert_array_equal(self.pbc, other.pbc)\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.affine","title":"affine property","text":"
    affine: AffineTransform3D\n

    Affine transformation. Holds transformation from 'ortho' to 'local' coordinates, including rotation away from the standard crystal orientation.

    "},{"location":"api/cell/#atomlib.cell.HasCell.ortho","title":"ortho property","text":"
    ortho: LinearTransform3D\n

    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.

    "},{"location":"api/cell/#atomlib.cell.HasCell.metric","title":"metric property","text":"
    metric: LinearTransform3D\n

    Cell metric tensor

    Returns the dot product between every combination of basis vectors. :math:\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j

    "},{"location":"api/cell/#atomlib.cell.HasCell.cell_size","title":"cell_size property","text":"
    cell_size: NDArray[float64]\n

    Unit cell size.

    "},{"location":"api/cell/#atomlib.cell.HasCell.cell_angle","title":"cell_angle property","text":"
    cell_angle: NDArray[float64]\n

    Unit cell angles, in radians.

    "},{"location":"api/cell/#atomlib.cell.HasCell.n_cells","title":"n_cells property","text":"
    n_cells: NDArray[int_]\n

    Number of unit cells.

    "},{"location":"api/cell/#atomlib.cell.HasCell.pbc","title":"pbc property","text":"
    pbc: NDArray[bool_]\n

    Flags indicating the presence of periodic boundary conditions along each axis.

    "},{"location":"api/cell/#atomlib.cell.HasCell.ortho_size","title":"ortho_size property","text":"
    ortho_size: NDArray[float64]\n

    Return size of orthogonal unit cell.

    Equivalent to the diagonal of the orthogonalization matrix.

    "},{"location":"api/cell/#atomlib.cell.HasCell.box_size","title":"box_size property","text":"
    box_size: NDArray[float64]\n

    Return size of the cell box.

    Equivalent to self.n_cells * self.cell_size.

    "},{"location":"api/cell/#atomlib.cell.HasCell.bbox","title":"bbox class-attribute instance-attribute","text":"
    bbox = bbox_cell\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.get_cell","title":"get_cell abstractmethod","text":"
    get_cell() -> Cell\n

    Get the cell contained in self. This should be a low cost method.

    Source code in atomlib/cell.py
    @abc.abstractmethod\ndef get_cell(self) -> Cell:\n    \"\"\"Get the cell contained in ``self``. This should be a low cost method.\"\"\"\n    ...\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.with_cell","title":"with_cell abstractmethod","text":"
    with_cell(cell: Cell) -> HasCellT\n

    Replace the cell in self with cell.

    Source code in atomlib/cell.py
    @abc.abstractmethod\ndef with_cell(self: HasCellT, cell: Cell) -> HasCellT:\n    \"\"\"Replace the cell in ``self`` with ``cell``.\"\"\"\n    ...\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.get_transform","title":"get_transform","text":"
    get_transform(\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n

    In the two-argument form, get the transform to frame_to from frame_from. In the one-argument form, get the transform from local coordinates to 'frame'.

    Source code in atomlib/cell.py
    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n    \"\"\"\n    In the two-argument form, get the transform to `frame_to` from `frame_from`.\n    In the one-argument form, get the transform from local coordinates to 'frame'.\n    \"\"\"\n    transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n    transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n    if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n        return AffineTransform3D()\n    return transform_to.inverse() @ transform_from\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.corners","title":"corners","text":"
    corners(frame: CoordinateFrame = 'local') -> ndarray\n
    Source code in atomlib/cell.py
    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n    corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n    return self.get_transform(frame, 'cell_box') @ corners\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.bbox_cell","title":"bbox_cell","text":"
    bbox_cell(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the bounding box of the cell box in the given coordinate system.

    Source code in atomlib/cell.py
    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n    return BBox3D.from_pts(self.corners(frame))\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.is_orthogonal","title":"is_orthogonal","text":"
    is_orthogonal(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal (axes are at right angles.)

    Source code in atomlib/cell.py
    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n    return self.ortho.is_diagonal(tol=tol)\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.is_orthogonal_in_local","title":"is_orthogonal_in_local","text":"
    is_orthogonal_in_local(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal and aligned with the local coordinate system.

    Source code in atomlib/cell.py
    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n    transform = (self.affine @ self.ortho).to_linear()\n    if not transform.is_scaled_orthogonal(tol):\n        return False\n    normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n    # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n    return all(\n        any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n        for row in normed\n    )\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.to_ortho","title":"to_ortho","text":"
    to_ortho() -> AffineTransform3D\n
    Source code in atomlib/cell.py
    def to_ortho(self) -> AffineTransform3D:\n    return self.get_transform('local', 'cell_box')\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.transform_cell","title":"transform_cell","text":"
    transform_cell(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> HasCellT\n

    Apply the given transform to the unit cell, and return a new Cell. The transform is applied in coordinate frame 'frame'. Orthogonal and affine transformations are applied to the affine matrix component, while skew and scaling is applied to the orthogonalization matrix/cell_size.

    Source code in atomlib/cell.py
    def transform_cell(self: HasCellT, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> HasCellT:\n    \"\"\"\n    Apply the given transform to the unit cell, and return a new `Cell`.\n    The transform is applied in coordinate frame 'frame'.\n    Orthogonal and affine transformations are applied to the affine matrix component,\n    while skew and scaling is applied to the orthogonalization matrix/cell_size.\n    \"\"\"\n    transform = t.cast(AffineTransform3D, self.change_transform(transform, 'local', frame))\n    if not transform.to_linear().is_orthogonal():\n        raise NotImplementedError()\n    return self.with_cell(Cell(\n        affine=transform @ self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size,\n        cell_angle=self.cell_angle,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.strain_orthogonal","title":"strain_orthogonal","text":"
    strain_orthogonal() -> HasCellT\n

    Orthogonalize using strain.

    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane. For small displacements, no hydrostatic strain is applied (volume is conserved).

    Source code in atomlib/cell.py
    def strain_orthogonal(self: HasCellT) -> HasCellT:\n    \"\"\"\n    Orthogonalize using strain.\n\n    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n    For small displacements, no hydrostatic strain is applied (volume is conserved).\n    \"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=LinearTransform3D(),\n        cell_size=self.cell_size,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.repeat","title":"repeat","text":"
    repeat(n: Union[int, VecLike]) -> HasCellT\n

    Tile the cell by n in each dimension.

    Source code in atomlib/cell.py
    def repeat(self: HasCellT, n: t.Union[int, VecLike]) -> HasCellT:\n    \"\"\"Tile the cell by `n` in each dimension.\"\"\"\n    ns = numpy.broadcast_to(n, 3)\n    if not numpy.issubdtype(ns.dtype, numpy.integer):\n        raise ValueError(\"repeat() argument must be an integer or integer array.\")\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size,\n        cell_angle=self.cell_angle,\n        n_cells=self.n_cells * numpy.broadcast_to(n, 3),\n        pbc = self.pbc | (ns > 1)  # assume periodic along repeated directions\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.explode","title":"explode","text":"
    explode() -> HasCellT\n

    Materialize repeated cells as one supercell.

    Source code in atomlib/cell.py
    def explode(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*self.n_cells,\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.explode_z","title":"explode_z","text":"
    explode_z() -> HasCellT\n

    Materialize repeated cells as one supercell in z.

    Source code in atomlib/cell.py
    def explode_z(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n        n_cells=[*self.n_cells[:2], 1],\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.crop","title":"crop","text":"
    crop(\n    x_min: float = -numpy.inf,\n    x_max: float = numpy.inf,\n    y_min: float = -numpy.inf,\n    y_max: float = numpy.inf,\n    z_min: float = -numpy.inf,\n    z_max: float = numpy.inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> HasCellT\n

    Crop self to the given extents. For a non-orthogonal cell, this must be specified in cell coordinates. This function implicity explodes the cell as well.

    Source code in atomlib/cell.py
    def crop(self: HasCellT, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n         frame: CoordinateFrame = 'local') -> HasCellT:\n    \"\"\"\n    Crop `self` to the given extents. For a non-orthogonal\n    cell, this must be specified in cell coordinates. This\n    function implicity `explode`s the cell as well.\n    \"\"\"\n\n    if not frame.lower().startswith('cell'):\n        if not self.is_orthogonal():\n            raise ValueError(\"Cannot crop a non-orthogonal cell in orthogonal coordinates. Use crop_atoms instead.\")\n\n    min = to_vec3([x_min, y_min, z_min])\n    max = to_vec3([x_max, y_max, z_max])\n    (min, max) = self.get_transform('cell_box', frame).transform([min, max])\n    new_box = BBox3D(min, max) & BBox3D.unit()\n    cropped = (new_box.min > 0.) | (new_box.max < 1.)\n\n    return self.with_cell(Cell(\n        affine=self.affine @ AffineTransform3D.translate(-new_box.min),\n        ortho=self.ortho,\n        cell_size=new_box.size * self.cell_size * numpy.where(cropped, self.n_cells, 1),\n        n_cells=numpy.where(cropped, 1, self.n_cells),\n        cell_angle=self.cell_angle,\n        pbc=self.pbc & ~cropped  # remove periodicity along cropped directions\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.change_transform","title":"change_transform","text":"
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n

    Coordinate-change a transformation from frame_from into frame_to.

    Source code in atomlib/cell.py
    def change_transform(self, transform: Transform3D,\n                     frame_to: t.Optional[CoordinateFrame] = None,\n                     frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n    \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n    if frame_to == frame_from and frame_to is not None:\n        return transform\n    coord_change = self.get_transform(frame_to, frame_from)\n    return coord_change @ transform @ coord_change.inverse()\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n
    Source code in atomlib/cell.py
    def assert_equal(self, other: t.Any):\n    assert isinstance(other, HasCell) and type(self) is type(other)\n    numpy.testing.assert_array_almost_equal(self.affine.inner, other.affine.inner, 6)\n    numpy.testing.assert_array_almost_equal(self.ortho.inner, other.ortho.inner, 6)\n    numpy.testing.assert_array_almost_equal(self.cell_size, other.cell_size, 6)\n    numpy.testing.assert_array_equal(self.n_cells, other.n_cells)\n    numpy.testing.assert_array_equal(self.pbc, other.pbc)\n
    "},{"location":"api/cell/#atomlib.cell.Cell","title":"Cell dataclass","text":"

    Bases: HasCell

    Internal class for representing the coordinate systems of a crystal.

    The overall transformation from crystal coordinates to real-space coordinates is is split into four transformations, applied from bottom to top. First is n_cells, which scales from fractions of a unit cell to fractions of a supercell. Next is cell_size, which scales to real-space units. ortho is an orthogonalization matrix, a det = 1 upper-triangular matrix which transforms crystal axes to an orthogonal coordinate system. Finally, affine contains any remaining transformations to the local coordinate system, which atoms are stored in.

    Source code in atomlib/cell.py
    @dataclass(frozen=True, init=False)\nclass Cell(HasCell):\n    \"\"\"\n    Internal class for representing the coordinate systems of a crystal.\n\n    The overall transformation from crystal coordinates to real-space coordinates is\n    is split into four transformations, applied from bottom to top. First is `n_cells`,\n    which scales from fractions of a unit cell to fractions of a supercell. Next is\n    `cell_size`, which scales to real-space units. `ortho` is an orthogonalization\n    matrix, a det = 1 upper-triangular matrix which transforms crystal axes to\n    an orthogonal coordinate system. Finally, `affine` contains any remaining\n    transformations to the local coordinate system, which atoms are stored in.\n    \"\"\"\n\n    def get_cell(self) -> Cell:\n        return self\n\n    def with_cell(self: Cell, cell: Cell) -> Cell:\n        return cell\n\n    _affine: AffineTransform3D = AffineTransform3D()\n    \"\"\"\n    Affine transformation. Holds transformation from `'ortho'` to `'local'` coordinates,\n    including rotation away from the standard crystal orientation.\n    \"\"\"\n\n    _ortho: LinearTransform3D = LinearTransform3D()\n    \"\"\"\n    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.\n    \"\"\"\n\n    _cell_size: NDArray[numpy.float64]\n    \"\"\"Unit cell size.\"\"\"\n    _cell_angle: NDArray[numpy.float64] = field(default_factory=lambda: numpy.full(3, numpy.pi/2.))\n    \"\"\"Unit cell angles, in radians.\"\"\"\n    _n_cells: NDArray[numpy.int64] = field(default_factory=lambda: numpy.ones(3, numpy.int64))\n    \"\"\"Number of unit cells.\"\"\"\n    _pbc: NDArray[numpy.bool_] = field(default_factory=lambda: numpy.ones(3, numpy.bool_))\n    \"\"\"Flags indicating the presence of periodic boundary conditions along each axis.\"\"\"\n\n    def __init__(self, *,\n        affine: t.Optional[AffineTransform3D] = None, ortho: t.Optional[LinearTransform3D] = None,\n        cell_size: VecLike, cell_angle: t.Optional[VecLike] = None,\n        n_cells: t.Optional[VecLike] = None, pbc: t.Optional[VecLike] = None):\n\n        object.__setattr__(self, '_affine', AffineTransform3D() if affine is None else affine)\n        object.__setattr__(self, '_ortho', LinearTransform3D() if ortho is None else ortho)\n        object.__setattr__(self, '_cell_size', to_vec3(cell_size))\n        object.__setattr__(self, '_cell_angle', numpy.full(3, numpy.pi/2.) if cell_angle is None else to_vec3(cell_angle))\n        object.__setattr__(self, '_n_cells', numpy.ones(3, numpy.int_) if n_cells is None else to_vec3(n_cells, numpy.int64))\n        object.__setattr__(self, '_pbc', numpy.ones(3, numpy.bool_) if pbc is None else to_vec3(pbc, numpy.bool_))\n\n    @staticmethod\n    def from_unit_cell(cell_size: VecLike, cell_angle: t.Optional[VecLike] = None, n_cells: t.Optional[VecLike] = None,\n                       pbc: t.Optional[VecLike] = None):\n        return Cell(\n            ortho=cell_to_ortho([1.]*3, cell_angle),\n            n_cells=to_vec3([1]*3 if n_cells is None else n_cells, numpy.int_),\n            cell_size=to_vec3(cell_size),\n            cell_angle=to_vec3([numpy.pi/2.]*3 if cell_angle is None else cell_angle),\n            pbc=pbc\n        )\n\n    @staticmethod\n    def from_ortho(ortho: AffineTransform3D, n_cells: t.Optional[VecLike] = None, pbc: t.Optional[VecLike] = None):\n        lin = ortho.to_linear()\n        # decompose into orthogonal and upper triangular\n        q, r = numpy.linalg.qr(lin.inner)\n\n        # flip QR decomposition so R has positive diagonals\n        signs = numpy.sign(numpy.diagonal(r))\n        # multiply flips to columns of Q, rows of R\n        q = q * signs\n        r = r * signs[:, None]\n        #numpy.testing.assert_allclose(q @ r, lin.inner)\n        if numpy.linalg.det(q) < 0:\n            warn(\"Crystal is left-handed. This is currently unsupported, and may cause errors.\")\n            # currently, behavior is to leave `ortho` proper, and move the inversion into the affine transform\n\n        cell_size, cell_angle = ortho_to_cell(lin)\n        return Cell(\n            affine=LinearTransform3D(q).translate(ortho.translation()),\n            ortho=LinearTransform3D(r / cell_size).round_near_zero(),\n            cell_size=cell_size, cell_angle=cell_angle,\n            n_cells=to_vec3([1]*3 if n_cells is None else n_cells, numpy.int_),\n            pbc=pbc,\n        )\n\n    def __str__(self) -> str:\n        return \"\\n\".join((\n            self.__class__.__name__,\n            f\"Cell size: {self.cell_size!r}\",\n            f\"Cell angle: {self.cell_angle!r}\",\n            f\"# cells: {self.n_cells!r}\",\n            f\"pbc: {self.pbc!r}\",\n        ))\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"ortho={self.ortho}, affine={self.affine}, cell_size={self.cell_size}, \"\n            f\"cell_angle={self.cell_angle}, n_cells={self.n_cells}, pbc={self.pbc})\"\n        )\n\n    def _repr_pretty_(self, p: t.Any, cycle: bool) -> None:\n        p.text(f\"{self.__class__.__name__}(...)\") if cycle else p.text(str(self))\n
    "},{"location":"api/cell/#atomlib.cell.Cell.affine","title":"affine property","text":"
    affine: AffineTransform3D\n

    Affine transformation. Holds transformation from 'ortho' to 'local' coordinates, including rotation away from the standard crystal orientation.

    "},{"location":"api/cell/#atomlib.cell.Cell.ortho","title":"ortho property","text":"
    ortho: LinearTransform3D\n

    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.

    "},{"location":"api/cell/#atomlib.cell.Cell.metric","title":"metric property","text":"
    metric: LinearTransform3D\n

    Cell metric tensor

    Returns the dot product between every combination of basis vectors. :math:\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j

    "},{"location":"api/cell/#atomlib.cell.Cell.cell_size","title":"cell_size property","text":"
    cell_size: NDArray[float64]\n

    Unit cell size.

    "},{"location":"api/cell/#atomlib.cell.Cell.cell_angle","title":"cell_angle property","text":"
    cell_angle: NDArray[float64]\n

    Unit cell angles, in radians.

    "},{"location":"api/cell/#atomlib.cell.Cell.n_cells","title":"n_cells property","text":"
    n_cells: NDArray[int_]\n

    Number of unit cells.

    "},{"location":"api/cell/#atomlib.cell.Cell.pbc","title":"pbc property","text":"
    pbc: NDArray[bool_]\n

    Flags indicating the presence of periodic boundary conditions along each axis.

    "},{"location":"api/cell/#atomlib.cell.Cell.ortho_size","title":"ortho_size property","text":"
    ortho_size: NDArray[float64]\n

    Return size of orthogonal unit cell.

    Equivalent to the diagonal of the orthogonalization matrix.

    "},{"location":"api/cell/#atomlib.cell.Cell.box_size","title":"box_size property","text":"
    box_size: NDArray[float64]\n

    Return size of the cell box.

    Equivalent to self.n_cells * self.cell_size.

    "},{"location":"api/cell/#atomlib.cell.Cell.bbox","title":"bbox class-attribute instance-attribute","text":"
    bbox = bbox_cell\n
    "},{"location":"api/cell/#atomlib.cell.Cell.get_transform","title":"get_transform","text":"
    get_transform(\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n

    In the two-argument form, get the transform to frame_to from frame_from. In the one-argument form, get the transform from local coordinates to 'frame'.

    Source code in atomlib/cell.py
    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n    \"\"\"\n    In the two-argument form, get the transform to `frame_to` from `frame_from`.\n    In the one-argument form, get the transform from local coordinates to 'frame'.\n    \"\"\"\n    transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n    transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n    if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n        return AffineTransform3D()\n    return transform_to.inverse() @ transform_from\n
    "},{"location":"api/cell/#atomlib.cell.Cell.corners","title":"corners","text":"
    corners(frame: CoordinateFrame = 'local') -> ndarray\n
    Source code in atomlib/cell.py
    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n    corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n    return self.get_transform(frame, 'cell_box') @ corners\n
    "},{"location":"api/cell/#atomlib.cell.Cell.bbox_cell","title":"bbox_cell","text":"
    bbox_cell(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the bounding box of the cell box in the given coordinate system.

    Source code in atomlib/cell.py
    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n    return BBox3D.from_pts(self.corners(frame))\n
    "},{"location":"api/cell/#atomlib.cell.Cell.is_orthogonal","title":"is_orthogonal","text":"
    is_orthogonal(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal (axes are at right angles.)

    Source code in atomlib/cell.py
    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n    return self.ortho.is_diagonal(tol=tol)\n
    "},{"location":"api/cell/#atomlib.cell.Cell.is_orthogonal_in_local","title":"is_orthogonal_in_local","text":"
    is_orthogonal_in_local(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal and aligned with the local coordinate system.

    Source code in atomlib/cell.py
    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n    transform = (self.affine @ self.ortho).to_linear()\n    if not transform.is_scaled_orthogonal(tol):\n        return False\n    normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n    # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n    return all(\n        any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n        for row in normed\n    )\n
    "},{"location":"api/cell/#atomlib.cell.Cell.to_ortho","title":"to_ortho","text":"
    to_ortho() -> AffineTransform3D\n
    Source code in atomlib/cell.py
    def to_ortho(self) -> AffineTransform3D:\n    return self.get_transform('local', 'cell_box')\n
    "},{"location":"api/cell/#atomlib.cell.Cell.transform_cell","title":"transform_cell","text":"
    transform_cell(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> HasCellT\n

    Apply the given transform to the unit cell, and return a new Cell. The transform is applied in coordinate frame 'frame'. Orthogonal and affine transformations are applied to the affine matrix component, while skew and scaling is applied to the orthogonalization matrix/cell_size.

    Source code in atomlib/cell.py
    def transform_cell(self: HasCellT, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> HasCellT:\n    \"\"\"\n    Apply the given transform to the unit cell, and return a new `Cell`.\n    The transform is applied in coordinate frame 'frame'.\n    Orthogonal and affine transformations are applied to the affine matrix component,\n    while skew and scaling is applied to the orthogonalization matrix/cell_size.\n    \"\"\"\n    transform = t.cast(AffineTransform3D, self.change_transform(transform, 'local', frame))\n    if not transform.to_linear().is_orthogonal():\n        raise NotImplementedError()\n    return self.with_cell(Cell(\n        affine=transform @ self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size,\n        cell_angle=self.cell_angle,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.Cell.strain_orthogonal","title":"strain_orthogonal","text":"
    strain_orthogonal() -> HasCellT\n

    Orthogonalize using strain.

    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane. For small displacements, no hydrostatic strain is applied (volume is conserved).

    Source code in atomlib/cell.py
    def strain_orthogonal(self: HasCellT) -> HasCellT:\n    \"\"\"\n    Orthogonalize using strain.\n\n    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n    For small displacements, no hydrostatic strain is applied (volume is conserved).\n    \"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=LinearTransform3D(),\n        cell_size=self.cell_size,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.Cell.repeat","title":"repeat","text":"
    repeat(n: Union[int, VecLike]) -> HasCellT\n

    Tile the cell by n in each dimension.

    Source code in atomlib/cell.py
    def repeat(self: HasCellT, n: t.Union[int, VecLike]) -> HasCellT:\n    \"\"\"Tile the cell by `n` in each dimension.\"\"\"\n    ns = numpy.broadcast_to(n, 3)\n    if not numpy.issubdtype(ns.dtype, numpy.integer):\n        raise ValueError(\"repeat() argument must be an integer or integer array.\")\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size,\n        cell_angle=self.cell_angle,\n        n_cells=self.n_cells * numpy.broadcast_to(n, 3),\n        pbc = self.pbc | (ns > 1)  # assume periodic along repeated directions\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.Cell.explode","title":"explode","text":"
    explode() -> HasCellT\n

    Materialize repeated cells as one supercell.

    Source code in atomlib/cell.py
    def explode(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*self.n_cells,\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.Cell.explode_z","title":"explode_z","text":"
    explode_z() -> HasCellT\n

    Materialize repeated cells as one supercell in z.

    Source code in atomlib/cell.py
    def explode_z(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n        n_cells=[*self.n_cells[:2], 1],\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.Cell.crop","title":"crop","text":"
    crop(\n    x_min: float = -numpy.inf,\n    x_max: float = numpy.inf,\n    y_min: float = -numpy.inf,\n    y_max: float = numpy.inf,\n    z_min: float = -numpy.inf,\n    z_max: float = numpy.inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> HasCellT\n

    Crop self to the given extents. For a non-orthogonal cell, this must be specified in cell coordinates. This function implicity explodes the cell as well.

    Source code in atomlib/cell.py
    def crop(self: HasCellT, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n         frame: CoordinateFrame = 'local') -> HasCellT:\n    \"\"\"\n    Crop `self` to the given extents. For a non-orthogonal\n    cell, this must be specified in cell coordinates. This\n    function implicity `explode`s the cell as well.\n    \"\"\"\n\n    if not frame.lower().startswith('cell'):\n        if not self.is_orthogonal():\n            raise ValueError(\"Cannot crop a non-orthogonal cell in orthogonal coordinates. Use crop_atoms instead.\")\n\n    min = to_vec3([x_min, y_min, z_min])\n    max = to_vec3([x_max, y_max, z_max])\n    (min, max) = self.get_transform('cell_box', frame).transform([min, max])\n    new_box = BBox3D(min, max) & BBox3D.unit()\n    cropped = (new_box.min > 0.) | (new_box.max < 1.)\n\n    return self.with_cell(Cell(\n        affine=self.affine @ AffineTransform3D.translate(-new_box.min),\n        ortho=self.ortho,\n        cell_size=new_box.size * self.cell_size * numpy.where(cropped, self.n_cells, 1),\n        n_cells=numpy.where(cropped, 1, self.n_cells),\n        cell_angle=self.cell_angle,\n        pbc=self.pbc & ~cropped  # remove periodicity along cropped directions\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.Cell.change_transform","title":"change_transform","text":"
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n

    Coordinate-change a transformation from frame_from into frame_to.

    Source code in atomlib/cell.py
    def change_transform(self, transform: Transform3D,\n                     frame_to: t.Optional[CoordinateFrame] = None,\n                     frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n    \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n    if frame_to == frame_from and frame_to is not None:\n        return transform\n    coord_change = self.get_transform(frame_to, frame_from)\n    return coord_change @ transform @ coord_change.inverse()\n
    "},{"location":"api/cell/#atomlib.cell.Cell.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n
    Source code in atomlib/cell.py
    def assert_equal(self, other: t.Any):\n    assert isinstance(other, HasCell) and type(self) is type(other)\n    numpy.testing.assert_array_almost_equal(self.affine.inner, other.affine.inner, 6)\n    numpy.testing.assert_array_almost_equal(self.ortho.inner, other.ortho.inner, 6)\n    numpy.testing.assert_array_almost_equal(self.cell_size, other.cell_size, 6)\n    numpy.testing.assert_array_equal(self.n_cells, other.n_cells)\n    numpy.testing.assert_array_equal(self.pbc, other.pbc)\n
    "},{"location":"api/cell/#atomlib.cell.Cell.get_cell","title":"get_cell","text":"
    get_cell() -> Cell\n
    Source code in atomlib/cell.py
    def get_cell(self) -> Cell:\n    return self\n
    "},{"location":"api/cell/#atomlib.cell.Cell.with_cell","title":"with_cell","text":"
    with_cell(cell: Cell) -> Cell\n
    Source code in atomlib/cell.py
    def with_cell(self: Cell, cell: Cell) -> Cell:\n    return cell\n
    "},{"location":"api/cell/#atomlib.cell.Cell.from_unit_cell","title":"from_unit_cell staticmethod","text":"
    from_unit_cell(\n    cell_size: VecLike,\n    cell_angle: Optional[VecLike] = None,\n    n_cells: Optional[VecLike] = None,\n    pbc: Optional[VecLike] = None,\n)\n
    Source code in atomlib/cell.py
    @staticmethod\ndef from_unit_cell(cell_size: VecLike, cell_angle: t.Optional[VecLike] = None, n_cells: t.Optional[VecLike] = None,\n                   pbc: t.Optional[VecLike] = None):\n    return Cell(\n        ortho=cell_to_ortho([1.]*3, cell_angle),\n        n_cells=to_vec3([1]*3 if n_cells is None else n_cells, numpy.int_),\n        cell_size=to_vec3(cell_size),\n        cell_angle=to_vec3([numpy.pi/2.]*3 if cell_angle is None else cell_angle),\n        pbc=pbc\n    )\n
    "},{"location":"api/cell/#atomlib.cell.Cell.from_ortho","title":"from_ortho staticmethod","text":"
    from_ortho(\n    ortho: AffineTransform3D,\n    n_cells: Optional[VecLike] = None,\n    pbc: Optional[VecLike] = None,\n)\n
    Source code in atomlib/cell.py
    @staticmethod\ndef from_ortho(ortho: AffineTransform3D, n_cells: t.Optional[VecLike] = None, pbc: t.Optional[VecLike] = None):\n    lin = ortho.to_linear()\n    # decompose into orthogonal and upper triangular\n    q, r = numpy.linalg.qr(lin.inner)\n\n    # flip QR decomposition so R has positive diagonals\n    signs = numpy.sign(numpy.diagonal(r))\n    # multiply flips to columns of Q, rows of R\n    q = q * signs\n    r = r * signs[:, None]\n    #numpy.testing.assert_allclose(q @ r, lin.inner)\n    if numpy.linalg.det(q) < 0:\n        warn(\"Crystal is left-handed. This is currently unsupported, and may cause errors.\")\n        # currently, behavior is to leave `ortho` proper, and move the inversion into the affine transform\n\n    cell_size, cell_angle = ortho_to_cell(lin)\n    return Cell(\n        affine=LinearTransform3D(q).translate(ortho.translation()),\n        ortho=LinearTransform3D(r / cell_size).round_near_zero(),\n        cell_size=cell_size, cell_angle=cell_angle,\n        n_cells=to_vec3([1]*3 if n_cells is None else n_cells, numpy.int_),\n        pbc=pbc,\n    )\n
    "},{"location":"api/cell/#atomlib.cell.cell_to_ortho","title":"cell_to_ortho","text":"
    cell_to_ortho(\n    cell_size: VecLike, cell_angle: Optional[VecLike] = None\n) -> LinearTransform3D\n

    Get orthogonalization transform from unit cell parameters (which turns fractional cell coordinates into real-space coordinates). .

    Source code in atomlib/cell.py
    def cell_to_ortho(cell_size: VecLike, cell_angle: t.Optional[VecLike] = None) -> LinearTransform3D:\n    \"\"\"\n    Get orthogonalization transform from unit cell parameters (which turns fractional cell coordinates into real-space coordinates).\n    .\"\"\"\n    (a, b, c) = _validate_cell_size(cell_size)\n    cell_angle = _validate_cell_angle(cell_angle)\n\n    if numpy.allclose(cell_angle.view(numpy.ndarray), numpy.pi/2.):\n        return LinearTransform3D.scale(a, b, c)\n\n    (alpha, beta, gamma) = cell_angle\n    alphastar = numpy.cos(beta) * numpy.cos(gamma) - numpy.cos(alpha)\n    alphastar /= numpy.sin(beta) * numpy.sin(gamma)\n    alphastar = numpy.arccos(alphastar)\n    assert not numpy.isnan(alphastar)\n\n    # aligns a axis along x\n    # aligns b axis in the x-y plane\n    return LinearTransform3D(numpy.array([\n        [a,  b * numpy.cos(gamma),  c * numpy.cos(beta)],\n        [0.,  b * numpy.sin(gamma), -c * numpy.sin(beta) * numpy.cos(alphastar)],\n        [0.,  0.,                     c * numpy.sin(beta) * numpy.sin(alphastar)],\n    ], dtype=float)).round_near_zero()\n
    "},{"location":"api/cell/#atomlib.cell.ortho_to_cell","title":"ortho_to_cell","text":"
    ortho_to_cell(\n    ortho: LinearTransform3D,\n) -> Tuple[Vec3, Vec3]\n

    Get unit cell parameters (cell_size, cell_angle) from orthogonalization transform.

    Source code in atomlib/cell.py
    def ortho_to_cell(ortho: LinearTransform3D) -> t.Tuple[Vec3, Vec3]:\n    \"\"\"Get unit cell parameters `(cell_size, cell_angle)` from orthogonalization transform.\"\"\"\n    # TODO suspect\n    cell_size = numpy.linalg.norm(ortho.inner, axis=-2)\n    cell_size = _validate_cell_size(cell_size)\n    normed = ortho.inner / cell_size\n    cosines = numpy.array([\n        numpy.dot(normed[..., 1], normed[..., 2]), # alpha\n        numpy.dot(normed[..., 2], normed[..., 0]), # beta\n        numpy.dot(normed[..., 0], normed[..., 1]), # gamma\n    ])\n    cell_angle = numpy.arccos(cosines)\n    cell_angle = _validate_cell_angle(cell_angle)\n\n    return (cell_size, cell_angle)\n
    "},{"location":"api/cell/#atomlib.cell.plane_to_zone","title":"plane_to_zone","text":"
    plane_to_zone(\n    metric: LinearTransform3D,\n    plane: VecLike,\n    reduce: bool = True,\n) -> Vec3\n

    Return the zone axis associated with a given crystallographic plane. If reduce is True, call reduce_vec before returning. Otherwise, return a unit vector.

    Source code in atomlib/cell.py
    def plane_to_zone(metric: LinearTransform3D, plane: VecLike, reduce: bool = True) -> Vec3:\n    \"\"\"\n    Return the zone axis associated with a given crystallographic plane.\n    If `reduce` is `True`, call `reduce_vec` before returning. Otherwise,\n    return a unit vector.\n    \"\"\"\n\n    plane = to_vec3(plane)\n    if metric.is_orthogonal():\n        return plane\n\n    # reciprocal lattice is transpose of inverse of real lattice\n    # [b1 b2 b3]^T = [a1 a2 a3]^-1\n    # so real indices [uvw] = O^-1 O^-1^T (hkl)\n    # O^-1 O^-1^T = (O^T O)^-1 = M^-1\n    # in other words, we are raising the index of the tensor,\n    # converting a covector into a vector\n    zone = metric.inverse() @ plane\n\n    if reduce:\n        return to_vec3(reduce_vec(zone))\n    # otherwise reduce to unit vector\n    return zone / float(numpy.linalg.norm(zone))\n
    "},{"location":"api/cell/#atomlib.cell.zone_to_plane","title":"zone_to_plane","text":"
    zone_to_plane(\n    metric: LinearTransform3D,\n    zone: VecLike,\n    reduce: bool = True,\n) -> Vec3\n

    Return the crystallographic plane associated with a given zone axis. If reduce is True, call reduce_vec before returning. Otherwise, return a unit vector.

    Source code in atomlib/cell.py
    def zone_to_plane(metric: LinearTransform3D, zone: VecLike, reduce: bool = True) -> Vec3:\n    \"\"\"\n    Return the crystallographic plane associated with a given zone axis.\n    If `reduce` is True, call `reduce_vec` before returning. Otherwise,\n    return a unit vector.\n    \"\"\"\n\n    zone = to_vec3(zone)\n    if metric.is_orthogonal():\n        return zone\n\n    plane = metric @ zone\n\n    if reduce:\n        return to_vec3(reduce_vec(plane))\n    # otherwise reduce to unit vector\n    return plane / float(numpy.linalg.norm(plane))\n
    "},{"location":"api/defect/","title":"atomlib.defect","text":"

    A collection of functions for inserting dislocations into structures.

    "},{"location":"api/defect/#atomlib.defect.CutType","title":"CutType module-attribute","text":"
    CutType: TypeAlias = Literal['shift', 'add', 'rm']\n

    Cut plane to use when creating a (non-screw) dislocation.

    "},{"location":"api/defect/#atomlib.defect.ellip_pi","title":"ellip_pi","text":"
    ellip_pi(\n    n: NDArray[float64], m: NDArray[float64]\n) -> NDArray[float64]\n

    Complete elliptic integral of the third kind, \\(\\Pi(n | m)\\).

    Follows the definition of Wolfram Mathworld.

    Source code in atomlib/defect.py
    def ellip_pi(n: NDArray[numpy.float64], m: NDArray[numpy.float64]) -> NDArray[numpy.float64]:\n    \"\"\"\n    Complete elliptic integral of the third kind, $\\\\Pi(n | m)$.\n\n    Follows the definition of [Wolfram Mathworld][wolfram_ellip_pi].\n\n    [wolfram_ellip_pi]: https://mathworld.wolfram.com/EllipticIntegraloftheThirdKind.html\n    \"\"\"\n    from scipy.special import elliprf, elliprj  # type: ignore\n\n    y = 1 - m\n    assert numpy.all(y > 0)\n\n    rf = elliprf(0, y, 1)\n    rj = elliprj(0, y, 1, 1 - n)\n    return rf + rj * n / 3\n
    "},{"location":"api/defect/#atomlib.defect.stacking_fault","title":"stacking_fault","text":"
    stacking_fault(\n    atoms: HasAtomsT,\n    pos: VecLike,\n    shift: VecLike,\n    plane: VecLike,\n) -> HasAtomsT\n

    Add a stacking fault to the structure.

    The fault plane will pass through the position pos, with normal plane. Atoms above plane will be shifted by the vector shift.

    If there is a component of shift parallel to plane, applying the shift will create (or remove) volume. In this case, atoms are added (or removed) accordingly. If shift \\(\\cdot\\) plane is positive, atoms are added.

    In general, adding a stacking fault will not preserve a cell's periodicity. Please verify this for your usecase.

    PARAMETER DESCRIPTION atoms

    Structure to add fault to

    TYPE: HasAtomsT

    pos

    Position on fault plane

    TYPE: VecLike

    shift

    Vector to shift by

    TYPE: VecLike

    plane

    Normal to fault plane

    TYPE: VecLike

    RETURNS DESCRIPTION HasAtomsT

    Structure with a stacking fault added

    Source code in atomlib/defect.py
    def stacking_fault(atoms: HasAtomsT, pos: VecLike, shift: VecLike, plane: VecLike) -> HasAtomsT:\n    \"\"\"\n    Add a stacking fault to the structure.\n\n    The fault plane will pass through the position `pos`, with normal `plane`.\n    Atoms above `plane` will be shifted by the vector `shift`.\n\n    If there is a component of `shift` parallel to `plane`, applying the shift\n    will create (or remove) volume. In this case, atoms are added (or removed)\n    accordingly. If `shift` $\\\\cdot$ `plane` is positive, atoms are added.\n\n    In general, adding a stacking fault will not preserve a cell's periodicity.\n    Please verify this for your usecase.\n\n    Args:\n      atoms: Structure to add fault to\n      pos: Position on fault plane\n      shift: Vector to shift by\n      plane: Normal to fault plane\n\n    Returns:\n      Structure with a stacking fault added\n    \"\"\"\n    pos = to_vec3(pos)\n    shift = to_vec3(shift)\n    plane = to_vec3(plane)\n    plane /= numpy.linalg.norm(plane)\n\n    coords = atoms.coords(frame='local')  # type: ignore\n\n    perp_dist = numpy.dot(coords - pos, plane)\n    atoms = atoms.with_columns(perp_dist=polars.Series(perp_dist)) \\\n        .with_columns(branch=polars.col('perp_dist') > 0.)\n\n    d = numpy.dot(shift, plane)\n    if -d > 1e-8:\n        logging.info(\"Removing atoms.\")\n        old_len = len(atoms)\n        atoms = atoms.filter(\n            (polars.col('perp_dist') < 0) | (polars.col('perp_dist') >= -d)\n        )\n        logging.info(f\"Removed {old_len - len(atoms)} atoms\")\n    if d > 1e-8:\n        logging.info(\"Duplicating atoms.\")\n        duplicate = atoms.filter(\n            (polars.col('perp_dist') >= -d) & (polars.col('perp_dist') < 0)\n        ).with_columns(~polars.col('branch'))\n        logging.info(f\"Duplicated {len(duplicate)} atoms\")\n\n        atoms = atoms.with_atoms(Atoms.concat((atoms, duplicate)))\n\n    coords = atoms.coords(frame='local')\n    branch = atoms['branch'].to_numpy()\n    atoms = atoms.with_coords(coords + shift * branch[:, None], frame='local')\n    return atoms.with_atoms(Atoms(atoms.drop('perp_dist', 'branch')))\n
    "},{"location":"api/defect/#atomlib.defect.disloc_edge","title":"disloc_edge","text":"
    disloc_edge(\n    atoms: HasAtomsT,\n    center: VecLike,\n    b: VecLike,\n    t: VecLike,\n    cut: Union[CutType, VecLike] = \"shift\",\n    *,\n    poisson: float = 0.25\n) -> HasAtomsT\n

    Add a Volterra edge dislocation to the structure.

    The dislocation will pass through center, with line vector t and Burgers vector b. t will be modified such that it is perpendicular to b.

    The cut parameter defines the cut (discontinuity) plane used to displace the atoms. By default, cut is 'shift', which defines the cut plane to contain b, ensuring no atoms need to be added or removed. Instead, 'add' or 'rm' may be specified, which defines the cut plane as containing \\(\\mathbf{b} \\times \\mathbf{t}\\). In this mode, atoms will be added or removed in the plane of b to create the dislocation. Alternatively, a vector cut may be supplied. The cut plane will contain this vector.

    In the coordinate system where t is along \\(z\\), and b is along \\(+x\\), the displacement due to the edge dislocation can be calculated as follows:

    \\[\\begin{align} u_x &= \\frac{b}{2\\pi} \\left( \\arctan(x, y) + \\frac{x y}{2(x^2 + y^2)(1-\\nu)} \\right) \\\\ u_y &= -\\frac{b}{4(1-\\nu)} \\left( (1-2\\nu) \\ln(x^2 + y^2) + \\frac{x^2 - y^2}{x^2 + y^2} \\right) \\end{align}\\]

    Where \\(x\\) and \\(y\\) are distances from the dislocation center. This creates a discontinuity along the \\(-x\\) axis. This coordinate system is rotated to support branches along an arbitrary axis.

    The dislocation is defined by the FS/RH convention: Performing one loop of positive sense relative to the tangent vector displaces the real crystal one b relative to a reference crystal.

    PARAMETER DESCRIPTION atoms

    Structure to add edge dislocation to

    TYPE: HasAtomsT

    center

    Position on dislocation line

    TYPE: VecLike

    b

    Burgers vector of dislocation (FS/RH convention)

    TYPE: VecLike

    t

    Tangent vector of dislocation

    TYPE: VecLike

    cut

    Cut plane to create dislocation on

    TYPE: Union[CutType, VecLike] DEFAULT: 'shift'

    poisson

    Poisson ratio of material

    TYPE: float DEFAULT: 0.25

    RETURNS DESCRIPTION HasAtomsT

    Structure with an edge dislocation added

    Source code in atomlib/defect.py
    def disloc_edge(atoms: HasAtomsT, center: VecLike, b: VecLike, t: VecLike, cut: t.Union[CutType, VecLike] = 'shift',\n                *, poisson: float = 0.25) -> HasAtomsT:\n    r\"\"\"\n    Add a Volterra edge dislocation to the structure.\n\n    The dislocation will pass through `center`, with line vector `t` and Burgers vector `b`.\n    `t` will be modified such that it is perpendicular to `b`.\n\n    The `cut` parameter defines the cut (discontinuity) plane used to displace the atoms.\n    By default, `cut` is `'shift'`, which defines the cut plane to contain `b`, ensuring no atoms\n    need to be added or removed.\n    Instead, `'add'` or `'rm'` may be specified, which defines the cut plane as containing\n    $\\mathbf{b} \\times \\mathbf{t}$. In this mode, atoms will be added or removed\n    in the plane of `b` to create the dislocation. Alternatively, a vector `cut`\n    may be supplied. The cut plane will contain this vector.\n\n    In the coordinate system where `t` is along $z$, and `b` is along $+x$, the displacement\n    due to the edge dislocation can be calculated as follows:\n\n    $$\\begin{align}\n       u_x &= \\frac{b}{2\\pi} \\left( \\arctan(x, y) + \\frac{x y}{2(x^2 + y^2)(1-\\nu)} \\right) \\\\\n       u_y &= -\\frac{b}{4(1-\\nu)} \\left( (1-2\\nu) \\ln(x^2 + y^2) + \\frac{x^2 - y^2}{x^2 + y^2} \\right)\n    \\end{align}$$\n\n    Where $x$ and $y$ are distances from the dislocation center. This creates a discontinuity along\n    the $-x$ axis. This coordinate system is rotated to support branches along an arbitrary axis.\n\n    The dislocation is defined by the FS/RH convention: Performing one loop of positive sense\n    relative to the tangent vector displaces the real crystal one `b` relative to a reference\n    crystal.\n\n    Args:\n      atoms: Structure to add edge dislocation to\n      center: Position on dislocation line\n      b: Burgers vector of dislocation (FS/RH convention)\n      t: Tangent vector of dislocation\n      cut: Cut plane to create dislocation on\n      poisson: Poisson ratio of material\n\n    Returns:\n      Structure with an edge dislocation added\n    \"\"\"\n\n    center = to_vec3(center)\n    b_vec = to_vec3(b)\n    #b_mag = norm(b_vec)\n\n    # get component of t perpendicular to b, normalize\n    t = to_vec3(t)\n    t = perp(t, b)\n    if norm(t) < 1e-10:\n        raise ValueError(\"`b` and `t` must be different.\")\n    t /= norm(t)\n\n    if isinstance(cut, str):\n        cut = cast(CutType, cut.lower())\n        if cut == 'shift':\n            plane_v = b_vec.copy()\n        elif cut == 'add':\n            # FS/RH convention: t x b points to extra half plane\n            plane_v = numpy.cross(t, b_vec)\n        elif cut == 'rm':\n            plane_v = -numpy.cross(t, b_vec)\n        else:\n            raise ValueError(f\"Unknown cut plane type `{cut}`. Expected 'shift', 'add', 'rm', or a vector.\")\n        plane_v /= norm(plane_v)\n    else:\n        plane_v = to_vec3(cut)\n        plane_v = plane_v / norm(plane_v)\n        if numpy.linalg.norm(numpy.cross(plane_v, t)) < 1e-10:\n            raise ValueError('`cut` and `t` must be different.')\n\n    # translate center to 0., and align t to [0, 0, 1], plane to +y\n    transform = AffineTransform3D.translate(center).inverse().compose(\n        LinearTransform3D.align_to(t, [0., 0., 1.], plane_v, [-1., 0., 0.])\n    )\n    frame = atoms.get_atoms('local').transform(transform)\n    b_vec = transform.transform_vec(b_vec)\n\n    d = numpy.dot(b_vec, [0., 1., 0.])\n    if -d > 1e-8:\n        logging.info(\"Removing atoms.\")\n        old_len = len(frame)\n        frame = frame.filter(~(\n            (polars.col('coords').arr.get(0) < 0)\n            & (polars.col('coords').arr.get(1) >= d/2.)\n            & (polars.col('coords').arr.get(1) <= -d/2.)\n        ))\n        logging.info(f\"Removed {old_len - len(frame)} atoms\")\n    if d > 1e-8:\n        logging.info(\"Duplicating atoms.\")\n        duplicate = frame.filter(\n            (polars.col('coords').arr.get(0) < 0)\n            & (polars.col('coords').arr.get(1) >= -d/2.)\n            & (polars.col('coords').arr.get(1) <= d/2.)\n        )\n        logging.info(f\"Duplicated {len(duplicate)} atoms\")\n\n        frame = Atoms.concat((frame, duplicate))\n        #atoms = atoms._replace_atoms(frame)\n        branch = numpy.ones(len(frame), dtype=float)\n        if len(duplicate) > 0:\n            branch[-len(duplicate):] *= -1  # flip branch of duplicated atoms\n    else:\n        branch = numpy.ones(len(frame), dtype=float)\n\n    pts = frame.coords()\n\n    x, y, _ = split_arr(pts, axis=-1)\n    r2 = x**2 + y**2\n\n    # displacement parallel and perpendicular to b\n    d_para = branch * numpy.arctan2(y, x) + x*y/(2*(1-poisson)*r2)\n    d_perp = -(1-2*poisson)/(4*(1-poisson)) * numpy.log(r2) + (y**2 - x**2)/(4*(1-poisson)*r2)\n\n    disps = numpy.stack([\n        d_para * b_vec[0] + d_perp * b_vec[1],\n        d_perp * b_vec[0] + d_para * b_vec[1],\n        numpy.zeros_like(x)\n    ], axis=-1) / (2*numpy.pi)\n\n    return atoms.with_atoms(frame.with_coords(pts + disps).transform(transform.inverse()), 'local')\n
    "},{"location":"api/defect/#atomlib.defect.disloc_screw","title":"disloc_screw","text":"
    disloc_screw(\n    atoms: HasAtomsT,\n    center: VecLike,\n    b: VecLike,\n    cut: Optional[VecLike] = None,\n    sign: bool = True,\n) -> HasAtomsT\n

    Add a Volterra screw dislocation to the structure.

    The dislocation will pass through center, with Burgers vector b.

    The cut parameter defines the cut (discontinuity) plane used to displace the atoms. By default, cut is chosen automtically, but it may also be specified as a vector which points from the dislocation core towards the cut plane (not normal to the cut plane!)

    The screw dislocation in an isotropic medium has a particularily simple form, given by:

    \\[ \\mathbf{u} = \\frac{\\mathbf{b}}{2\\pi} \\arctan(x, y) \\]

    for a dislocation along \\(+z\\) with cut plane along \\(-x\\). To support arbitrary cut planes, \\(x\\) and \\(y\\) are replaced by the components of \\(r\\) parallel and perpendicular to the cut plane, evaluated in the plane of b.

    The dislocation is defined by the FS/RH convention: Performing one loop of positive sense relative to the tangent vector displaces the real crystal one b relative to a reference crystal.

    PARAMETER DESCRIPTION atoms

    Structure to add screw dislocation to

    TYPE: HasAtomsT

    center

    Position on dislocation line

    TYPE: VecLike

    b

    Burgers vector of dislocation (FS/RH convention)

    TYPE: VecLike

    cut

    Cut plane to create dislocation on

    TYPE: Optional[VecLike] DEFAULT: None

    sign

    Sign of screw dislocation (True means positive)

    TYPE: bool DEFAULT: True

    RETURNS DESCRIPTION HasAtomsT

    Structure with a screw dislocation added

    Source code in atomlib/defect.py
    def disloc_screw(atoms: HasAtomsT, center: VecLike, b: VecLike, cut: t.Optional[VecLike] = None,\n                 sign: bool = True) -> HasAtomsT:\n    r\"\"\"\n    Add a Volterra screw dislocation to the structure.\n\n    The dislocation will pass through `center`, with Burgers vector `b`.\n\n    The `cut` parameter defines the cut (discontinuity) plane used to displace the atoms.\n    By default, `cut` is chosen automtically, but it may also be specified as a vector\n    which points from the dislocation core towards the cut plane (not normal to the cut plane!)\n\n    The screw dislocation in an isotropic medium has a particularily simple form, given by:\n\n    $$\n       \\mathbf{u} = \\frac{\\mathbf{b}}{2\\pi} \\arctan(x, y)\n    $$\n\n    for a dislocation along $+z$ with cut plane along $-x$. To support arbitrary cut planes,\n    $x$ and $y$ are replaced by the components of $r$ parallel and perpendicular to the cut plane,\n    evaluated in the plane of `b`.\n\n    The dislocation is defined by the FS/RH convention: Performing one loop of positive sense\n    relative to the tangent vector displaces the real crystal one `b` relative to a reference\n    crystal.\n\n    Args:\n      atoms: Structure to add screw dislocation to\n      center: Position on dislocation line\n      b: Burgers vector of dislocation (FS/RH convention)\n      cut: Cut plane to create dislocation on\n      sign: Sign of screw dislocation (True means positive)\n\n    Returns:\n      Structure with a screw dislocation added\n    \"\"\"\n\n    center = to_vec3(center)\n    b_vec = to_vec3(b)\n    t = b_vec / float(numpy.linalg.norm(b_vec))\n    t = -t if not sign else t\n    if cut is None:\n        if numpy.linalg.norm(numpy.cross(t, [1., 1., 1.])) < numpy.pi/4:\n            # near 111, choose x as cut plane direction\n            cut = to_vec3([1., 0., 0.])\n        else:\n            # otherwise find plane by rotating around 111\n            cut = cast(NDArray[numpy.float64], LinearTransform3D.rotate([1., 1., 1.], 2*numpy.pi/3).transform(t))\n    else:\n        cut = to_vec3(cut)\n        cut /= norm(cut)\n        if numpy.allclose(cut, t, atol=1e-2):\n            raise ValueError(\"`t` and `cut` must be different.\")\n\n    print(f\"Cut plane direction: {cut}\")\n\n    frame = atoms.get_atoms('local')\n    pts = frame.coords() - center\n\n    # components perpendicular to t\n    cut_perp = -perp(cut, t)\n    pts_perp = perp(pts, t)\n\n    # signed angle around dislocation\n    theta = numpy.arctan2(dot(t, numpy.cross(cut_perp, pts_perp)), dot(cut_perp, pts_perp))\n    # FS/RH convention\n    disp = b_vec * (theta / (2*numpy.pi))\n\n    return atoms.with_atoms(frame.with_coords(pts + center + disp), 'local')\n
    "},{"location":"api/defect/#atomlib.defect.disloc_loop_z","title":"disloc_loop_z","text":"
    disloc_loop_z(\n    atoms: HasAtomsT,\n    center: VecLike,\n    b: VecLike,\n    loop_r: float,\n    *,\n    poisson: float = 0.25\n) -> HasAtomsT\n

    Add a circular dislocation loop to the structure, assuming an elastically isotropic material.

    The loop will have radius loop_r, be centered at center, and oriented along the z-axis.

    The dislocation's sign is defined such that travelling upwards through the loop results in a displacement of b. poisson is the material's poisson ratio, which affects the shape of dislocations with an edge component.

    Adding the loop creates (or removes) a volume of \\(\\mathbf{b} \\cdot \\mathbf{n}A\\), where \\(\\mathbf{n}A\\) is the loop's normal times its area. Consequently, this function adds or remove atoms to effect this volume change.

    PARAMETER DESCRIPTION atoms

    Structure to add dislocation loop to

    TYPE: HasAtomsT

    center

    Center of dislocation loop

    TYPE: VecLike

    b

    Burgers vector of dislocation (defined so travelling upwards leads to a plastic displacment of b).

    TYPE: VecLike

    loop_r

    Radius of dislocation loop

    TYPE: float

    poisson

    Poisson ratio of material

    TYPE: float DEFAULT: 0.25

    RETURNS DESCRIPTION HasAtomsT

    Structure with a dislocation loop added

    Source code in atomlib/defect.py
    def disloc_loop_z(atoms: HasAtomsT, center: VecLike, b: VecLike,\n                  loop_r: float, *, poisson: float = 0.25) -> HasAtomsT:\n    r\"\"\"\n    Add a circular dislocation loop to the structure, assuming an elastically isotropic material.\n\n    The loop will have radius `loop_r`, be centered at `center`, and oriented along the z-axis.\n\n    The dislocation's sign is defined such that travelling upwards through the loop results in a displacement of `b`.\n    `poisson` is the material's poisson ratio, which affects the shape of dislocations with an edge component.\n\n    Adding the loop creates (or removes) a volume of $\\mathbf{b} \\cdot \\mathbf{n}A$, where $\\mathbf{n}A$ is the loop's\n    normal times its area. Consequently, this function adds or remove atoms to effect this volume change.\n\n    Args:\n      atoms: Structure to add dislocation loop to\n      center: Center of dislocation loop\n      b: Burgers vector of dislocation (defined so travelling upwards leads to a plastic displacment of `b`).\n      loop_r: Radius of dislocation loop\n      poisson: Poisson ratio of material\n\n    Returns:\n      Structure with a dislocation loop added\n    \"\"\"\n\n    center = to_vec3(center)\n    b_vec = to_vec3(b)\n\n    atoms = atoms.transform_atoms(AffineTransform3D.translate(center).inverse())\n    frame = atoms.get_atoms('local')\n    branch = None\n\n    d = numpy.dot(b_vec, [0, 0, 1])\n    if -d > 1e-8:\n        logging.info(\"Non-conservative dislocation. Removing atoms.\")\n        frame = frame.filter(~(\n            (frame.x()**2 + frame.y()**2 < loop_r**2)\n            & frame.z().is_between(d/2., -d/2., closed='both')\n        ))\n\n    if d > 1e-8:\n        logging.info(\"Non-conservative dislocation. Duplicating atoms.\")\n        duplicate = frame.filter(\n            (frame.x()**2 + frame.y()**2 < loop_r**2)\n            & frame.z().is_between(-d/2., d/2., closed='both')\n        )\n        logging.info(f\"Adding {len(duplicate)} atoms\")\n\n        frame = Atoms.concat((frame, duplicate))\n        #atoms = atoms._replace_atoms(frame)\n        branch = numpy.sign(frame['z'].to_numpy())\n        if len(duplicate) > 0:\n            branch[-len(duplicate):] *= -1  # flip branch of duplicated atoms\n\n    pts = frame.coords()\n    disps = _loop_disp_z(pts, b_vec, loop_r, poisson=poisson, branch=branch)\n\n    return atoms.with_atoms(frame.with_coords(pts + disps + center), 'local')\n
    "},{"location":"api/defect/#atomlib.defect.disloc_square_z","title":"disloc_square_z","text":"
    disloc_square_z(\n    atoms: HasAtomsT,\n    center: VecLike,\n    b: VecLike,\n    loop_r: float,\n    *,\n    poisson: float = 0.25\n) -> HasAtomsT\n

    Add a square dislocation loop to the structure, assuming an elastically isotropic material.

    The dislocation's sign is defined such that traveling upwards through the loop results in a displacement of b. poisson is the material's poisson ratio, which affects the shape of dislocations with an edge component.

    Adding the loop creates (or removes) a volume of \\(\\mathbf{b} \\cdot \\mathbf{n}A\\), where \\(\\mathbf{n}A\\) is the loop's normal times its area. Consequently, this function adds or remove atoms to effect this volume change.

    PARAMETER DESCRIPTION atoms

    Structure to add dislocation loop to

    TYPE: HasAtomsT

    center

    Center of dislocation loop

    TYPE: VecLike

    b

    Burgers vector of dislocation (defined so travelling upwards leads to a plastic displacment of b).

    TYPE: VecLike

    loop_r

    Radius (side length/2) of dislocation loop

    TYPE: float

    poisson

    Poisson ratio of material

    TYPE: float DEFAULT: 0.25

    RETURNS DESCRIPTION HasAtomsT

    Structure with a dislocation loop added

    Source code in atomlib/defect.py
    def disloc_square_z(atoms: HasAtomsT, center: VecLike, b: VecLike,\n                    loop_r: float, *, poisson: float = 0.25) -> HasAtomsT:\n    r\"\"\"\n    Add a square dislocation loop to the structure, assuming an elastically isotropic material.\n\n    The dislocation's sign is defined such that traveling upwards through the loop results in a displacement of `b`.\n    `poisson` is the material's poisson ratio, which affects the shape of dislocations with an edge component.\n\n    Adding the loop creates (or removes) a volume of $\\mathbf{b} \\cdot \\mathbf{n}A$, where $\\mathbf{n}A$ is the loop's\n    normal times its area. Consequently, this function adds or remove atoms to effect this volume change.\n\n    Args:\n      atoms: Structure to add dislocation loop to\n      center: Center of dislocation loop\n      b: Burgers vector of dislocation (defined so travelling upwards leads to a plastic displacment of `b`).\n      loop_r: Radius (side length/2) of dislocation loop\n      poisson: Poisson ratio of material\n\n    Returns:\n      Structure with a dislocation loop added\n    \"\"\"\n    poly = loop_r * numpy.array([(1, 1), (-1, 1), (-1, -1), (1, -1)])\n    return disloc_poly_z(atoms, b, poly, center, poisson=poisson)\n
    "},{"location":"api/defect/#atomlib.defect.disloc_poly_z","title":"disloc_poly_z","text":"
    disloc_poly_z(\n    atoms: HasAtomsT,\n    b: VecLike,\n    poly: ArrayLike,\n    center: Optional[VecLike] = None,\n    *,\n    poisson: float = 0.25\n) -> HasAtomsT\n

    Add a dislocation loop defined by the polygon poly, assuming an elastically isotropic material.

    poly should be a 2d polygon (shape (N, 2)). It will be placed at center, in the plane z=center[2]. For CCW winding order, traveling upwards through the plane of the loop results in a displacement of b. poisson is the material's poisson ratio, which affects the shape of dislocations with an edge component.

    Adding the loop creates (or removes) a volume of \\(\\mathbf{b} \\cdot \\mathbf{n}A\\), where \\(\\mathbf{n}A\\) is the loop's normal times its area. Consequently, this function adds or remove atoms to effect this volume change.

    Follows the solution in Hirth, J. P. & Lothe, J. (1982). Theory of Dislocations. ISBN 0-89464-617-6

    PARAMETER DESCRIPTION atoms

    Structure to add dislocation loop to

    TYPE: HasAtomsT

    b

    Burgers vector of dislocation (defined so travelling upwards leads to a plastic displacment of b).

    TYPE: VecLike

    poly

    2D polygon defining dislocation line. It is placed in the plane z=center[2].

    TYPE: ArrayLike

    center

    Center of dislocation loop (defaults to zero, applying no displacement to the gven polygon).

    TYPE: Optional[VecLike] DEFAULT: None

    poisson

    Poisson ratio of material

    TYPE: float DEFAULT: 0.25

    RETURNS DESCRIPTION HasAtomsT

    Structure with a dislocation loop added

    Source code in atomlib/defect.py
    def disloc_poly_z(atoms: HasAtomsT, b: VecLike, poly: ArrayLike, center: t.Optional[VecLike] = None,\n                  *, poisson: float = 0.25) -> HasAtomsT:\n    r\"\"\"\n    Add a dislocation loop defined by the polygon `poly`, assuming an elastically isotropic material.\n\n    `poly` should be a 2d polygon (shape `(N, 2)`). It will be placed at `center`, in the plane `z=center[2]`.\n    For CCW winding order, traveling upwards through the plane of the loop results in a displacement of `b`.\n    `poisson` is the material's poisson ratio, which affects the shape of dislocations with an edge component.\n\n    Adding the loop creates (or removes) a volume of $\\mathbf{b} \\cdot \\mathbf{n}A$, where $\\mathbf{n}A$ is the loop's\n    normal times its area. Consequently, this function adds or remove atoms to effect this volume change.\n\n    Follows the solution in [Hirth, J. P. & Lothe, J. (1982). Theory of Dislocations. ISBN 0-89464-617-6\n    ](https://www.google.com/books/edition/Theory_of_Dislocations/TAjwAAAAMAAJ)\n\n    Args:\n      atoms: Structure to add dislocation loop to\n      b: Burgers vector of dislocation (defined so travelling upwards leads to a plastic displacment of `b`).\n      poly: 2D polygon defining dislocation line. It is placed in the plane `z=center[2]`.\n      center: Center of dislocation loop (defaults to zero, applying no displacement to the gven polygon).\n      poisson: Poisson ratio of material\n\n    Returns:\n      Structure with a dislocation loop added\n    \"\"\"\n    center = to_vec3(center if center is not None else [0., 0., 0.])\n    b_vec = to_vec3(b)\n\n    poly = numpy.atleast_2d(poly)\n    if poly.ndim != 2 or poly.shape[-1] != 2:\n        raise ValueError(f\"Expected a 2d polygon. Instead got shape {poly.shape}\")\n\n    frame = atoms.get_atoms('local')\n    coords: NDArray[numpy.float64] = frame.coords() - center\n\n    branch = None\n    d = numpy.dot(b_vec, [0, 0, 1])\n    if abs(d) > 1e-8:\n        logging.info(\"Non-conservative dislocation.\")\n        windings = polygon_winding(poly, coords[..., :2])\n\n        z = coords[..., 2]\n        remove = (z >= windings * d/2.) & (z <= -windings * d/2.)\n        duplicate = (z >= -windings * d/2.) & (z <= windings * d/2.)\n\n        n_remove = numpy.sum(remove, dtype=int)\n        if n_remove:\n            logging.info(f\"Removing {n_remove} atoms\")\n            frame = frame.filter(polars.Series(~remove))\n            duplicate = duplicate[~remove]\n\n        n_duplicate = numpy.sum(duplicate, dtype=int)\n        if n_duplicate:\n            logging.info(f\"Duplicating {n_duplicate} atoms\")\n            frame = Atoms.concat((frame, frame.filter(polars.Series(duplicate))))\n\n            branch = numpy.ones(len(frame))\n            branch[-n_duplicate:] = -1  # flip branch of duplicated atoms\n\n        coords = frame.coords() - center\n\n    disp = _poly_disp_z(coords, b_vec, poly, poisson=poisson, branch=branch)\n\n    return atoms.with_atoms(frame.with_coords(coords + disp + center), 'local')\n
    "},{"location":"api/elem/","title":"atomlib.elem","text":""},{"location":"api/elem/#atomlib.elem.ELEMENTS","title":"ELEMENTS module-attribute","text":"
    ELEMENTS = {\n    \"h\": 1,\n    \"he\": 2,\n    \"li\": 3,\n    \"be\": 4,\n    \"b\": 5,\n    \"c\": 6,\n    \"n\": 7,\n    \"o\": 8,\n    \"f\": 9,\n    \"ne\": 10,\n    \"na\": 11,\n    \"mg\": 12,\n    \"al\": 13,\n    \"si\": 14,\n    \"p\": 15,\n    \"s\": 16,\n    \"cl\": 17,\n    \"ar\": 18,\n    \"k\": 19,\n    \"ca\": 20,\n    \"sc\": 21,\n    \"ti\": 22,\n    \"v\": 23,\n    \"cr\": 24,\n    \"mn\": 25,\n    \"fe\": 26,\n    \"co\": 27,\n    \"ni\": 28,\n    \"cu\": 29,\n    \"zn\": 30,\n    \"ga\": 31,\n    \"ge\": 32,\n    \"as\": 33,\n    \"se\": 34,\n    \"br\": 35,\n    \"kr\": 36,\n    \"rb\": 37,\n    \"sr\": 38,\n    \"y\": 39,\n    \"zr\": 40,\n    \"nb\": 41,\n    \"mo\": 42,\n    \"tc\": 43,\n    \"ru\": 44,\n    \"rh\": 45,\n    \"pd\": 46,\n    \"ag\": 47,\n    \"cd\": 48,\n    \"in\": 49,\n    \"sn\": 50,\n    \"sb\": 51,\n    \"te\": 52,\n    \"i\": 53,\n    \"xe\": 54,\n    \"cs\": 55,\n    \"ba\": 56,\n    \"la\": 57,\n    \"ce\": 58,\n    \"pr\": 59,\n    \"nd\": 60,\n    \"pm\": 61,\n    \"sm\": 62,\n    \"eu\": 63,\n    \"gd\": 64,\n    \"tb\": 65,\n    \"dy\": 66,\n    \"ho\": 67,\n    \"er\": 68,\n    \"tm\": 69,\n    \"yb\": 70,\n    \"lu\": 71,\n    \"hf\": 72,\n    \"ta\": 73,\n    \"w\": 74,\n    \"re\": 75,\n    \"os\": 76,\n    \"ir\": 77,\n    \"pt\": 78,\n    \"au\": 79,\n    \"hg\": 80,\n    \"tl\": 81,\n    \"pb\": 82,\n    \"bi\": 83,\n    \"po\": 84,\n    \"at\": 85,\n    \"rn\": 86,\n    \"fr\": 87,\n    \"ra\": 88,\n    \"ac\": 89,\n    \"th\": 90,\n    \"pa\": 91,\n    \"u\": 92,\n    \"np\": 93,\n    \"pu\": 94,\n    \"am\": 95,\n    \"cm\": 96,\n    \"bk\": 97,\n    \"cf\": 98,\n    \"es\": 99,\n    \"fm\": 100,\n    \"md\": 101,\n    \"no\": 102,\n    \"lr\": 103,\n    \"rf\": 104,\n    \"db\": 105,\n    \"sg\": 106,\n    \"bh\": 107,\n    \"hs\": 108,\n    \"mt\": 109,\n    \"ds\": 110,\n    \"rg\": 111,\n    \"cn\": 112,\n    \"nh\": 113,\n    \"fl\": 114,\n    \"mc\": 115,\n    \"lv\": 116,\n    \"ts\": 117,\n    \"og\": 118,\n}\n
    "},{"location":"api/elem/#atomlib.elem.ELEMENT_SYMBOLS","title":"ELEMENT_SYMBOLS module-attribute","text":"
    ELEMENT_SYMBOLS = [\n    \"H\",\n    \"He\",\n    \"Li\",\n    \"Be\",\n    \"B\",\n    \"C\",\n    \"N\",\n    \"O\",\n    \"F\",\n    \"Ne\",\n    \"Na\",\n    \"Mg\",\n    \"Al\",\n    \"Si\",\n    \"P\",\n    \"S\",\n    \"Cl\",\n    \"Ar\",\n    \"K\",\n    \"Ca\",\n    \"Sc\",\n    \"Ti\",\n    \"V\",\n    \"Cr\",\n    \"Mn\",\n    \"Fe\",\n    \"Co\",\n    \"Ni\",\n    \"Cu\",\n    \"Zn\",\n    \"Ga\",\n    \"Ge\",\n    \"As\",\n    \"Se\",\n    \"Br\",\n    \"Kr\",\n    \"Rb\",\n    \"Sr\",\n    \"Y\",\n    \"Zr\",\n    \"Nb\",\n    \"Mo\",\n    \"Tc\",\n    \"Ru\",\n    \"Rh\",\n    \"Pd\",\n    \"Ag\",\n    \"Cd\",\n    \"In\",\n    \"Sn\",\n    \"Sb\",\n    \"Te\",\n    \"I\",\n    \"Xe\",\n    \"Cs\",\n    \"Ba\",\n    \"La\",\n    \"Ce\",\n    \"Pr\",\n    \"Nd\",\n    \"Pm\",\n    \"Sm\",\n    \"Eu\",\n    \"Gd\",\n    \"Tb\",\n    \"Dy\",\n    \"Ho\",\n    \"Er\",\n    \"Tm\",\n    \"Yb\",\n    \"Lu\",\n    \"Hf\",\n    \"Ta\",\n    \"W\",\n    \"Re\",\n    \"Os\",\n    \"Ir\",\n    \"Pt\",\n    \"Au\",\n    \"Hg\",\n    \"Tl\",\n    \"Pb\",\n    \"Bi\",\n    \"Po\",\n    \"At\",\n    \"Rn\",\n    \"Fr\",\n    \"Ra\",\n    \"Ac\",\n    \"Th\",\n    \"Pa\",\n    \"U\",\n    \"Np\",\n    \"Pu\",\n    \"Am\",\n    \"Cm\",\n    \"Bk\",\n    \"Cf\",\n    \"Es\",\n    \"Fm\",\n    \"Md\",\n    \"No\",\n    \"Lr\",\n    \"Rf\",\n    \"Db\",\n    \"Sg\",\n    \"Bh\",\n    \"Hs\",\n    \"Mt\",\n    \"Ds\",\n    \"Rg\",\n    \"Cn\",\n    \"Nh\",\n    \"Fl\",\n    \"Mc\",\n    \"Lv\",\n    \"Ts\",\n    \"Og\",\n]\n
    "},{"location":"api/elem/#atomlib.elem.ELEMENT_SYMBOLS_POLARS","title":"ELEMENT_SYMBOLS_POLARS module-attribute","text":"
    ELEMENT_SYMBOLS_POLARS = Series(\n    [ELEMENT_SYMBOLS], dtype=List(Utf8)\n)\n
    "},{"location":"api/elem/#atomlib.elem.DATA_PATH","title":"DATA_PATH module-attribute","text":"
    DATA_PATH = files('atomlib.data')\n
    "},{"location":"api/elem/#atomlib.elem.get_elem","title":"get_elem","text":"
    get_elem(sym: Union[int, str, Series])\n
    Source code in atomlib/elem.py
    def get_elem(sym: t.Union[int, str, polars.Series]):\n    if isinstance(sym, int):\n        if not 0 < sym < len(ELEMENTS):\n            raise ValueError(f\"Invalid atomic number {sym}\")\n        return sym\n\n    if isinstance(sym, polars.Series):\n        # TODO: this is a mess\n        elem = sym.cast(polars.Utf8).str.extract(_SYM_RE, 0).str.to_lowercase() \\\n            .replace_strict(\n                old=list(ELEMENTS.keys()), new=list(ELEMENTS.values()),\n                default=None, return_dtype=polars.Int8\n            ).alias('elem')\n\n        if (invalid := sym.filter(sym.is_not_null() & elem.is_null()).to_list()):\n            raise ValueError(f\"Invalid element symbol(s) '{', '.join(map(str, invalid))}'\")\n\n        return elem\n\n    sym_s = re.search(_SYM_RE, str(sym))\n    try:\n        return ELEMENTS[sym_s[0].lower()]  # type: ignore\n    except (KeyError, IndexError):\n        raise ValueError(f\"Invalid element symbol '{sym}'\")\n
    "},{"location":"api/elem/#atomlib.elem.get_elems","title":"get_elems","text":"
    get_elems(sym: ElemsLike) -> List[Tuple[int, float]]\n
    Source code in atomlib/elem.py
    def get_elems(sym: ElemsLike) -> t.List[t.Tuple[int, float]]:\n    if not isinstance(sym, str):\n        if isinstance(sym, int):\n            return [(sym, 1.0)]\n        return [\n            (get_elem(v[0]), float(v[1]))  # type: ignore\n                if (hasattr(v, '__len__') and not isinstance(v, str))\n                else (get_elem(v), 1.)  # type: ignore\n            for v in sym\n        ]\n\n    if len(sym) > 0:\n        sym = sym[0].upper() + sym[1:]\n    segments = [\n        (match[1], match[2]) for match in re.finditer(r'([A-Z][a-z]*)([0-9\\.]*[+-]?)', str(sym))\n    ]\n    if len(segments) == 0:\n        raise ValueError(f\"Invalid compound '{sym}'\")\n\n    elems = [ELEMENTS.get(seg[0].lower()) for seg in segments]\n\n    out = []\n    for ((elem_sym, num), elem) in zip(segments, elems):\n        if elem is None:\n            raise ValueError(f\"Unknown element '{elem_sym}' in '{sym}'. Compounds are case-sensitive.\")\n\n        try:\n            num = float(num) if len(num) and num[-1] not in ('+', '-') else 1.\n        except ValueError:\n            raise ValueError(f\"Unknown occupancy '{num}' for elem '{elem_sym}' in compound '{sym}'\")\n\n        out.append((elem, num))\n\n    return out\n
    "},{"location":"api/elem/#atomlib.elem.get_sym","title":"get_sym","text":"
    get_sym(elem: Union[int, Series])\n
    Source code in atomlib/elem.py
    def get_sym(elem: t.Union[int, polars.Series]):\n    if isinstance(elem, polars.Series):\n        sym = elem.cast(polars.Int64).replace_strict(\n            list(range(1, len(ELEMENT_SYMBOLS)+1)),\n            ELEMENT_SYMBOLS,\n            default=None,\n            return_dtype=polars.Utf8,\n        ).alias('symbol')\n\n        if (invalid := elem.filter(elem.is_not_null() & sym.is_null()).unique().to_list()):\n            raise ValueError(f\"Invalid atomic number(s) {', '.join(map(str, invalid))}\")\n\n        return sym\n\n    return _get_sym(elem)\n
    "},{"location":"api/elem/#atomlib.elem.get_mass","title":"get_mass","text":"
    get_mass(elem: Union[int, Sequence[int], ndarray, Series])\n

    Get the standard atomic mass for the given element.

    Follows the 2021 table of the IUPAC Commission on Isotopic Abundances and Atomic Weights <https://doi.org/10.1515/pac-2019-0603>_.

    Source code in atomlib/elem.py
    def get_mass(elem: t.Union[int, t.Sequence[int], numpy.ndarray, polars.Series]):\n    \"\"\"\n    Get the standard atomic mass for the given element.\n\n    Follows the `2021 table of the IUPAC Commission on Isotopic Abundances and Atomic Weights <https://doi.org/10.1515/pac-2019-0603>`_.\n    \"\"\"\n    global _ELEMENT_MASSES\n\n    if _ELEMENT_MASSES is None:\n        with _open_binary_data('masses.npy') as f:\n            _ELEMENT_MASSES = numpy.load(f, allow_pickle=False)\n\n    if isinstance(elem, polars.Series):\n        return polars.Series(values=_ELEMENT_MASSES)[elem-1]\n\n    if isinstance(elem, (int, numpy.ndarray)):\n        return _ELEMENT_MASSES[elem-1]  # type: ignore\n    return _ELEMENT_MASSES[[e-1 for e in elem]]  # type: ignore\n
    "},{"location":"api/elem/#atomlib.elem.get_ionic_radius","title":"get_ionic_radius","text":"
    get_ionic_radius(elem: int, charge: int) -> float\n

    Get crystal ionic radius in angstroms for elem in charge state charge.

    Follows R.D. Shannon, Acta Cryst. A32 (1976) <https://doi.org/10.1107/S0567739476001551>.

    Source code in atomlib/elem.py
    def get_ionic_radius(elem: int, charge: int) -> float:\n    \"\"\"\n    Get crystal ionic radius in angstroms for ``elem`` in charge state ``charge``.\n\n    Follows `R.D. Shannon, Acta Cryst. A32 (1976) <https://doi.org/10.1107/S0567739476001551>`.\n    \"\"\"\n    global _ION_RADII\n\n    import json\n\n    if _ION_RADII is None:\n        with _open_text_data('ion_radii.json') as f:\n            _ION_RADII = json.load(f)\n        assert _ION_RADII is not None\n\n    s = f\"{get_sym(elem)}{charge:+d}\"\n\n    try:\n        return _ION_RADII[s]\n    except KeyError:\n        raise ValueError(f\"Unknown radius for ion '{s}'\") from None\n
    "},{"location":"api/elem/#atomlib.elem.get_radius","title":"get_radius","text":"
    get_radius(\n    elem: Union[int, Sequence[int], ndarray, Series]\n)\n

    Get the neutral atomic radius for the given element(s), in angstroms.

    Follows the calculated values in E. Clementi et. al, J. Chem. Phys. 47 (1967) <https://doi.org/10.1063/1.1712084>_.

    Source code in atomlib/elem.py
    def get_radius(elem: t.Union[int, t.Sequence[int], numpy.ndarray, polars.Series]):\n    \"\"\"\n    Get the neutral atomic radius for the given element(s), in angstroms.\n\n    Follows the calculated values in `E. Clementi et. al, J. Chem. Phys. 47 (1967) <https://doi.org/10.1063/1.1712084>`_.\n    \"\"\"\n    global _ELEMENT_RADII\n\n    if _ELEMENT_RADII is None:\n        with _open_binary_data('radii.npy') as f:\n            _ELEMENT_RADII = numpy.load(f, allow_pickle=False)\n\n    if isinstance(elem, polars.Series):\n        return polars.Series(values=_ELEMENT_RADII)[elem-1]\n\n    if isinstance(elem, (int, numpy.ndarray)):\n        return _ELEMENT_RADII[elem-1]  # type: ignore\n    return _ELEMENT_RADII[[e-1 for e in elem]]  # type: ignore\n
    "},{"location":"api/expr/","title":"atomlib.expr","text":""},{"location":"api/expr/#atomlib.expr.V2","title":"V2 module-attribute","text":"
    V2 = TypeVar('V2')\n
    "},{"location":"api/expr/#atomlib.expr.WSPACE_RE","title":"WSPACE_RE module-attribute","text":"
    WSPACE_RE = compile('\\\\s+')\n
    "},{"location":"api/expr/#atomlib.expr.NUMERIC_OPS","title":"NUMERIC_OPS module-attribute","text":"
    NUMERIC_OPS: Sequence[Op[SupportsNum]] = [\n    BinaryOrUnaryOp([\"-\"], sub, False, 5),\n    BinaryOp([\"+\"], add, 5),\n    BinaryOp([\"*\"], mul, 6),\n    BinaryOp([\"/\"], truediv, 6),\n    BinaryOp([\"//\"], floordiv, 6),\n    BinaryOp([\"^\", \"**\"], pow, 7),\n]\n
    "},{"location":"api/expr/#atomlib.expr.NUMERIC_PARSER","title":"NUMERIC_PARSER module-attribute","text":"
    NUMERIC_PARSER = Parser(NUMERIC_OPS, parse_numeric)\n
    "},{"location":"api/expr/#atomlib.expr.BOOLEAN_OPS","title":"BOOLEAN_OPS module-attribute","text":"
    BOOLEAN_OPS: Sequence[Op[SupportsBool]] = [\n    BinaryOp([\"=\", \"==\"], eq, 3),\n    BinaryOp([\"!=\", \"<>\"], ne, 3),\n    BinaryOp([\"|\", \"||\"], or_, 4),\n    BinaryOp([\"&\", \"&&\"], and_, 5),\n    BinaryOp([\"^\"], xor, 6),\n    UnaryOp([\"!\", \"~\"], invert),\n]\n
    "},{"location":"api/expr/#atomlib.expr.BOOLEAN_PARSER","title":"BOOLEAN_PARSER module-attribute","text":"
    BOOLEAN_PARSER = Parser(BOOLEAN_OPS, parse_boolean)\n

    Parser for boolean expressions ([1 || false && true])

    "},{"location":"api/expr/#atomlib.expr.VECTOR_OPS","title":"VECTOR_OPS module-attribute","text":"
    VECTOR_OPS: Sequence[Op[ndarray]] = [\n    *cast(Op[ndarray], op)\n    for op in NUMERIC_OPS,\n    NaryOp([\",\"], call=stack, precedence=3),\n]\n
    "},{"location":"api/expr/#atomlib.expr.VECTOR_PARSER","title":"VECTOR_PARSER module-attribute","text":"
    VECTOR_PARSER = Parser(\n    VECTOR_OPS, lambda v: array(parse_numeric(v))\n)\n
    "},{"location":"api/expr/#atomlib.expr.VariadicCallable","title":"VariadicCallable","text":"

    Bases: Protocol, Generic[V]

    Source code in atomlib/expr.py
    class VariadicCallable(t.Protocol, t.Generic[V]):\n    def __call__(self, *args: V) -> V:\n        ...\n
    "},{"location":"api/expr/#atomlib.expr.Op","title":"Op dataclass","text":"

    Bases: ABC, Generic[V]

    Source code in atomlib/expr.py
    @dataclass\nclass Op(ABC, t.Generic[V]):\n    aliases: t.List[str]\n    call: t.Callable = field(repr=False)\n\n    @property\n    @abstractmethod\n    def requires_whitespace(self) -> bool:\n        ...\n
    "},{"location":"api/expr/#atomlib.expr.Op.aliases","title":"aliases instance-attribute","text":"
    aliases: List[str]\n
    "},{"location":"api/expr/#atomlib.expr.Op.call","title":"call class-attribute instance-attribute","text":"
    call: Callable = field(repr=False)\n
    "},{"location":"api/expr/#atomlib.expr.Op.requires_whitespace","title":"requires_whitespace abstractmethod property","text":"
    requires_whitespace: bool\n
    "},{"location":"api/expr/#atomlib.expr.NaryOp","title":"NaryOp dataclass","text":"

    Bases: Op[V]

    Source code in atomlib/expr.py
    @dataclass\nclass NaryOp(Op[V]):\n    call: VariadicCallable[V] = field(repr=False)  # type: ignore\n    precedence: int = 5\n    requires_whitespace: bool = False  # type: ignore\n\n    def __call__(self, *args: V) -> V:\n        return self.call(*args)\n\n    def precedes(self, other: t.Union[BinaryOp, NaryOp, int]) -> bool:\n        if isinstance(other, (BinaryOp, NaryOp)):\n            other = other.precedence\n\n        # default to lower precedence for Nary\n        return self.precedence > other\n
    "},{"location":"api/expr/#atomlib.expr.NaryOp.aliases","title":"aliases instance-attribute","text":"
    aliases: List[str]\n
    "},{"location":"api/expr/#atomlib.expr.NaryOp.call","title":"call class-attribute instance-attribute","text":"
    call: VariadicCallable[V] = field(repr=False)\n
    "},{"location":"api/expr/#atomlib.expr.NaryOp.precedence","title":"precedence class-attribute instance-attribute","text":"
    precedence: int = 5\n
    "},{"location":"api/expr/#atomlib.expr.NaryOp.requires_whitespace","title":"requires_whitespace class-attribute instance-attribute","text":"
    requires_whitespace: bool = False\n
    "},{"location":"api/expr/#atomlib.expr.NaryOp.precedes","title":"precedes","text":"
    precedes(other: Union[BinaryOp, NaryOp, int]) -> bool\n
    Source code in atomlib/expr.py
    def precedes(self, other: t.Union[BinaryOp, NaryOp, int]) -> bool:\n    if isinstance(other, (BinaryOp, NaryOp)):\n        other = other.precedence\n\n    # default to lower precedence for Nary\n    return self.precedence > other\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOp","title":"BinaryOp dataclass","text":"

    Bases: Op[V]

    Source code in atomlib/expr.py
    @dataclass\nclass BinaryOp(Op[V]):\n    call: t.Callable[[V, V], V] = field(repr=False)  # type: ignore\n    precedence: int = 5\n    right_assoc: bool = False\n    requires_whitespace: bool = False  # type: ignore\n\n    def __call__(self, lhs: V, rhs: V) -> V:\n        return self.call(lhs, rhs)\n\n    def precedes(self, other: t.Union[BinaryOp, NaryOp, int]) -> bool:\n        \"\"\"Returns true if self is higher precedence than other.\n        Handles a tie using the associativity of self.\n        \"\"\"\n        if isinstance(other, (BinaryOp, NaryOp)):\n            other = other.precedence\n\n        if self.precedence == other:\n            return self.right_assoc\n        return self.precedence > other\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOp.aliases","title":"aliases instance-attribute","text":"
    aliases: List[str]\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOp.call","title":"call class-attribute instance-attribute","text":"
    call: Callable[[V, V], V] = field(repr=False)\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOp.precedence","title":"precedence class-attribute instance-attribute","text":"
    precedence: int = 5\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOp.right_assoc","title":"right_assoc class-attribute instance-attribute","text":"
    right_assoc: bool = False\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOp.requires_whitespace","title":"requires_whitespace class-attribute instance-attribute","text":"
    requires_whitespace: bool = False\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOp.precedes","title":"precedes","text":"
    precedes(other: Union[BinaryOp, NaryOp, int]) -> bool\n

    Returns true if self is higher precedence than other. Handles a tie using the associativity of self.

    Source code in atomlib/expr.py
    def precedes(self, other: t.Union[BinaryOp, NaryOp, int]) -> bool:\n    \"\"\"Returns true if self is higher precedence than other.\n    Handles a tie using the associativity of self.\n    \"\"\"\n    if isinstance(other, (BinaryOp, NaryOp)):\n        other = other.precedence\n\n    if self.precedence == other:\n        return self.right_assoc\n    return self.precedence > other\n
    "},{"location":"api/expr/#atomlib.expr.UnaryOp","title":"UnaryOp dataclass","text":"

    Bases: Op[V]

    Source code in atomlib/expr.py
    @dataclass\nclass UnaryOp(Op[V]):\n    call: t.Callable[[V], V] = field(repr=False)  # type: ignore\n    requires_whitespace: bool = False  # type: ignore\n\n    def __call__(self, inner: V) -> V:\n        return self.call(inner)\n
    "},{"location":"api/expr/#atomlib.expr.UnaryOp.aliases","title":"aliases instance-attribute","text":"
    aliases: List[str]\n
    "},{"location":"api/expr/#atomlib.expr.UnaryOp.call","title":"call class-attribute instance-attribute","text":"
    call: Callable[[V], V] = field(repr=False)\n
    "},{"location":"api/expr/#atomlib.expr.UnaryOp.requires_whitespace","title":"requires_whitespace class-attribute instance-attribute","text":"
    requires_whitespace: bool = False\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOrUnaryOp","title":"BinaryOrUnaryOp dataclass","text":"

    Bases: BinaryOp[V], UnaryOp[V]

    Source code in atomlib/expr.py
    @dataclass\nclass BinaryOrUnaryOp(BinaryOp[V], UnaryOp[V]):\n    call: t.Callable[[V, t.Optional[V]], V] = field(repr=False)  # type: ignore\n\n    def __call__(self, lhs: V, rhs: t.Optional[V] = None) -> V:\n        return self.call(lhs, rhs)\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOrUnaryOp.aliases","title":"aliases instance-attribute","text":"
    aliases: List[str]\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOrUnaryOp.requires_whitespace","title":"requires_whitespace class-attribute instance-attribute","text":"
    requires_whitespace: bool = False\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOrUnaryOp.precedence","title":"precedence class-attribute instance-attribute","text":"
    precedence: int = 5\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOrUnaryOp.right_assoc","title":"right_assoc class-attribute instance-attribute","text":"
    right_assoc: bool = False\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOrUnaryOp.call","title":"call class-attribute instance-attribute","text":"
    call: Callable[[V, Optional[V]], V] = field(repr=False)\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOrUnaryOp.precedes","title":"precedes","text":"
    precedes(other: Union[BinaryOp, NaryOp, int]) -> bool\n

    Returns true if self is higher precedence than other. Handles a tie using the associativity of self.

    Source code in atomlib/expr.py
    def precedes(self, other: t.Union[BinaryOp, NaryOp, int]) -> bool:\n    \"\"\"Returns true if self is higher precedence than other.\n    Handles a tie using the associativity of self.\n    \"\"\"\n    if isinstance(other, (BinaryOp, NaryOp)):\n        other = other.precedence\n\n    if self.precedence == other:\n        return self.right_assoc\n    return self.precedence > other\n
    "},{"location":"api/expr/#atomlib.expr.Token","title":"Token dataclass","text":"

    Bases: ABC, Generic[T_co, V]

    Source code in atomlib/expr.py
    @dataclass\nclass Token(ABC, t.Generic[T_co, V]):\n    raw: str\n    line: int\n    span: t.Tuple[int, int]\n\n    def __str__(self):\n        return self.raw\n
    "},{"location":"api/expr/#atomlib.expr.Token.raw","title":"raw instance-attribute","text":"
    raw: str\n
    "},{"location":"api/expr/#atomlib.expr.Token.line","title":"line instance-attribute","text":"
    line: int\n
    "},{"location":"api/expr/#atomlib.expr.Token.span","title":"span instance-attribute","text":"
    span: Tuple[int, int]\n
    "},{"location":"api/expr/#atomlib.expr.OpToken","title":"OpToken dataclass","text":"

    Bases: Token[T_co, V]

    Source code in atomlib/expr.py
    @dataclass\nclass OpToken(Token[T_co, V]):\n    op: Op[V]\n
    "},{"location":"api/expr/#atomlib.expr.OpToken.raw","title":"raw instance-attribute","text":"
    raw: str\n
    "},{"location":"api/expr/#atomlib.expr.OpToken.line","title":"line instance-attribute","text":"
    line: int\n
    "},{"location":"api/expr/#atomlib.expr.OpToken.span","title":"span instance-attribute","text":"
    span: Tuple[int, int]\n
    "},{"location":"api/expr/#atomlib.expr.OpToken.op","title":"op instance-attribute","text":"
    op: Op[V]\n
    "},{"location":"api/expr/#atomlib.expr.GroupOpenToken","title":"GroupOpenToken dataclass","text":"

    Bases: Token

    Source code in atomlib/expr.py
    @dataclass\nclass GroupOpenToken(Token):\n    ...\n
    "},{"location":"api/expr/#atomlib.expr.GroupOpenToken.raw","title":"raw instance-attribute","text":"
    raw: str\n
    "},{"location":"api/expr/#atomlib.expr.GroupOpenToken.line","title":"line instance-attribute","text":"
    line: int\n
    "},{"location":"api/expr/#atomlib.expr.GroupOpenToken.span","title":"span instance-attribute","text":"
    span: Tuple[int, int]\n
    "},{"location":"api/expr/#atomlib.expr.GroupCloseToken","title":"GroupCloseToken dataclass","text":"

    Bases: Token

    Source code in atomlib/expr.py
    @dataclass\nclass GroupCloseToken(Token):\n    ...\n
    "},{"location":"api/expr/#atomlib.expr.GroupCloseToken.raw","title":"raw instance-attribute","text":"
    raw: str\n
    "},{"location":"api/expr/#atomlib.expr.GroupCloseToken.line","title":"line instance-attribute","text":"
    line: int\n
    "},{"location":"api/expr/#atomlib.expr.GroupCloseToken.span","title":"span instance-attribute","text":"
    span: Tuple[int, int]\n
    "},{"location":"api/expr/#atomlib.expr.WhitespaceToken","title":"WhitespaceToken dataclass","text":"

    Bases: Token

    Source code in atomlib/expr.py
    class WhitespaceToken(Token):\n    ...\n
    "},{"location":"api/expr/#atomlib.expr.WhitespaceToken.raw","title":"raw instance-attribute","text":"
    raw: str\n
    "},{"location":"api/expr/#atomlib.expr.WhitespaceToken.line","title":"line instance-attribute","text":"
    line: int\n
    "},{"location":"api/expr/#atomlib.expr.WhitespaceToken.span","title":"span instance-attribute","text":"
    span: Tuple[int, int]\n
    "},{"location":"api/expr/#atomlib.expr.ValueToken","title":"ValueToken dataclass","text":"

    Bases: Token[T_co, V]

    Source code in atomlib/expr.py
    @dataclass\nclass ValueToken(Token[T_co, V]):\n    val: T_co\n
    "},{"location":"api/expr/#atomlib.expr.ValueToken.raw","title":"raw instance-attribute","text":"
    raw: str\n
    "},{"location":"api/expr/#atomlib.expr.ValueToken.line","title":"line instance-attribute","text":"
    line: int\n
    "},{"location":"api/expr/#atomlib.expr.ValueToken.span","title":"span instance-attribute","text":"
    span: Tuple[int, int]\n
    "},{"location":"api/expr/#atomlib.expr.ValueToken.val","title":"val instance-attribute","text":"
    val: T_co\n
    "},{"location":"api/expr/#atomlib.expr.Expr","title":"Expr","text":"

    Bases: ABC, Generic[T_co, V]

    Source code in atomlib/expr.py
    class Expr(ABC, t.Generic[T_co, V]):\n    @abstractmethod\n    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n        ...\n\n    @abstractmethod\n    def format(self,\n               format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n               format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n        ...\n\n    def __str__(self) -> str:\n        return self.format()\n\n    def __repr__(self) -> str:\n        ...\n
    "},{"location":"api/expr/#atomlib.expr.Expr.eval","title":"eval abstractmethod","text":"
    eval(map_f: Callable[[T_co], V]) -> V\n
    Source code in atomlib/expr.py
    @abstractmethod\ndef eval(self, map_f: t.Callable[[T_co], V]) -> V:\n    ...\n
    "},{"location":"api/expr/#atomlib.expr.Expr.format","title":"format abstractmethod","text":"
    format(\n    format_scalar: Callable[\n        [ValueToken[T_co, V]], str\n    ] = str,\n    format_op: Callable[[OpToken[T_co, V]], str] = str,\n) -> str\n
    Source code in atomlib/expr.py
    @abstractmethod\ndef format(self,\n           format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n           format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n    ...\n
    "},{"location":"api/expr/#atomlib.expr.UnaryExpr","title":"UnaryExpr dataclass","text":"

    Bases: Expr[T_co, V]

    Source code in atomlib/expr.py
    @dataclass\nclass UnaryExpr(Expr[T_co, V]):\n    op_token: OpToken[T_co, V]\n    op: UnaryOp[V] = field(init=False)\n    inner: Expr[T_co, V]\n    lspace: str = \"\"\n\n    def __post_init__(self):\n        if not isinstance(self.op_token.op, UnaryOp):\n            raise TypeError()\n        self.op = self.op_token.op\n\n    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n        return self.op.call(self.inner.eval(map_f))\n\n    def format(self,\n               format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n               format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n        return f\"{self.lspace}{format_op(self.op_token)}{self.inner.format(format_scalar, format_op)}\"\n
    "},{"location":"api/expr/#atomlib.expr.UnaryExpr.op_token","title":"op_token instance-attribute","text":"
    op_token: OpToken[T_co, V]\n
    "},{"location":"api/expr/#atomlib.expr.UnaryExpr.op","title":"op class-attribute instance-attribute","text":"
    op: UnaryOp[V] = field(init=False)\n
    "},{"location":"api/expr/#atomlib.expr.UnaryExpr.inner","title":"inner instance-attribute","text":"
    inner: Expr[T_co, V]\n
    "},{"location":"api/expr/#atomlib.expr.UnaryExpr.lspace","title":"lspace class-attribute instance-attribute","text":"
    lspace: str = ''\n
    "},{"location":"api/expr/#atomlib.expr.UnaryExpr.eval","title":"eval","text":"
    eval(map_f: Callable[[T_co], V]) -> V\n
    Source code in atomlib/expr.py
    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n    return self.op.call(self.inner.eval(map_f))\n
    "},{"location":"api/expr/#atomlib.expr.UnaryExpr.format","title":"format","text":"
    format(\n    format_scalar: Callable[\n        [ValueToken[T_co, V]], str\n    ] = str,\n    format_op: Callable[[OpToken[T_co, V]], str] = str,\n) -> str\n
    Source code in atomlib/expr.py
    def format(self,\n           format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n           format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n    return f\"{self.lspace}{format_op(self.op_token)}{self.inner.format(format_scalar, format_op)}\"\n
    "},{"location":"api/expr/#atomlib.expr.BinaryExpr","title":"BinaryExpr dataclass","text":"

    Bases: Expr[T_co, V]

    Source code in atomlib/expr.py
    @dataclass\nclass BinaryExpr(Expr[T_co, V]):\n    op_token: OpToken[T_co, V]\n    op: BinaryOp[V] = field(init=False)\n    lhs: Expr[T_co, V]\n    rhs: Expr[T_co, V]\n\n    def __post_init__(self):\n        if not isinstance(self.op_token.op, BinaryOp):\n            raise TypeError()\n        self.op = self.op_token.op\n\n    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n        return self.op.call(self.lhs.eval(map_f), self.rhs.eval(map_f))\n\n    def format(self,\n               format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n               format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n        return f\"{self.lhs.format(format_scalar, format_op)}{format_op(self.op_token)}{self.rhs.format(format_scalar, format_op)}\"\n
    "},{"location":"api/expr/#atomlib.expr.BinaryExpr.op_token","title":"op_token instance-attribute","text":"
    op_token: OpToken[T_co, V]\n
    "},{"location":"api/expr/#atomlib.expr.BinaryExpr.op","title":"op class-attribute instance-attribute","text":"
    op: BinaryOp[V] = field(init=False)\n
    "},{"location":"api/expr/#atomlib.expr.BinaryExpr.lhs","title":"lhs instance-attribute","text":"
    lhs: Expr[T_co, V]\n
    "},{"location":"api/expr/#atomlib.expr.BinaryExpr.rhs","title":"rhs instance-attribute","text":"
    rhs: Expr[T_co, V]\n
    "},{"location":"api/expr/#atomlib.expr.BinaryExpr.eval","title":"eval","text":"
    eval(map_f: Callable[[T_co], V]) -> V\n
    Source code in atomlib/expr.py
    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n    return self.op.call(self.lhs.eval(map_f), self.rhs.eval(map_f))\n
    "},{"location":"api/expr/#atomlib.expr.BinaryExpr.format","title":"format","text":"
    format(\n    format_scalar: Callable[\n        [ValueToken[T_co, V]], str\n    ] = str,\n    format_op: Callable[[OpToken[T_co, V]], str] = str,\n) -> str\n
    Source code in atomlib/expr.py
    def format(self,\n           format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n           format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n    return f\"{self.lhs.format(format_scalar, format_op)}{format_op(self.op_token)}{self.rhs.format(format_scalar, format_op)}\"\n
    "},{"location":"api/expr/#atomlib.expr.NaryExpr","title":"NaryExpr dataclass","text":"

    Bases: Expr[T_co, V]

    Source code in atomlib/expr.py
    @dataclass\nclass NaryExpr(Expr[T_co, V]):\n    op_tokens: t.Sequence[OpToken[T_co, V]]\n    op: NaryOp[V] = field(init=False)\n    args: t.Sequence[Expr[T_co, V]]\n    rspace: str = \"\"\n\n    def __post_init__(self):\n        op = next(token.op for token in self.op_tokens)\n        if not all(token.op == op for token in self.op_tokens[1:]):\n            raise ValueError(\"All `op`s must be identical inside a NaryExpr\")\n        if not isinstance(op, NaryOp):\n            raise TypeError()\n        self.op = op\n\n        assert len(self.op_tokens) == len(self.args) - 1\n\n    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n        return self.op.call(*map(lambda expr: expr.eval(map_f), self.args))\n\n    def format(self,\n               format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n               format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n        return \"\".join(interleave(\n            map(lambda expr: expr.format(format_scalar, format_op), self.args),\n            map(format_op, self.op_tokens)\n        )) + self.rspace\n
    "},{"location":"api/expr/#atomlib.expr.NaryExpr.op_tokens","title":"op_tokens instance-attribute","text":"
    op_tokens: Sequence[OpToken[T_co, V]]\n
    "},{"location":"api/expr/#atomlib.expr.NaryExpr.op","title":"op class-attribute instance-attribute","text":"
    op: NaryOp[V] = field(init=False)\n
    "},{"location":"api/expr/#atomlib.expr.NaryExpr.args","title":"args instance-attribute","text":"
    args: Sequence[Expr[T_co, V]]\n
    "},{"location":"api/expr/#atomlib.expr.NaryExpr.rspace","title":"rspace class-attribute instance-attribute","text":"
    rspace: str = ''\n
    "},{"location":"api/expr/#atomlib.expr.NaryExpr.eval","title":"eval","text":"
    eval(map_f: Callable[[T_co], V]) -> V\n
    Source code in atomlib/expr.py
    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n    return self.op.call(*map(lambda expr: expr.eval(map_f), self.args))\n
    "},{"location":"api/expr/#atomlib.expr.NaryExpr.format","title":"format","text":"
    format(\n    format_scalar: Callable[\n        [ValueToken[T_co, V]], str\n    ] = str,\n    format_op: Callable[[OpToken[T_co, V]], str] = str,\n) -> str\n
    Source code in atomlib/expr.py
    def format(self,\n           format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n           format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n    return \"\".join(interleave(\n        map(lambda expr: expr.format(format_scalar, format_op), self.args),\n        map(format_op, self.op_tokens)\n    )) + self.rspace\n
    "},{"location":"api/expr/#atomlib.expr.GroupExpr","title":"GroupExpr dataclass","text":"

    Bases: Expr[T_co, V]

    Source code in atomlib/expr.py
    @dataclass\nclass GroupExpr(Expr[T_co, V]):\n    open: GroupOpenToken\n    inner: Expr[T_co, V]\n    close: GroupCloseToken\n    lspace: str = \"\"\n    rspace: str = \"\"\n\n    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n        return self.inner.eval(map_f)\n\n    def format(self,\n               format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n               format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n        return f\"{self.lspace}{self.open}{self.inner.format(format_scalar, format_op)}{self.close}{self.rspace}\"\n
    "},{"location":"api/expr/#atomlib.expr.GroupExpr.open","title":"open instance-attribute","text":"
    open: GroupOpenToken\n
    "},{"location":"api/expr/#atomlib.expr.GroupExpr.inner","title":"inner instance-attribute","text":"
    inner: Expr[T_co, V]\n
    "},{"location":"api/expr/#atomlib.expr.GroupExpr.close","title":"close instance-attribute","text":"
    close: GroupCloseToken\n
    "},{"location":"api/expr/#atomlib.expr.GroupExpr.lspace","title":"lspace class-attribute instance-attribute","text":"
    lspace: str = ''\n
    "},{"location":"api/expr/#atomlib.expr.GroupExpr.rspace","title":"rspace class-attribute instance-attribute","text":"
    rspace: str = ''\n
    "},{"location":"api/expr/#atomlib.expr.GroupExpr.eval","title":"eval","text":"
    eval(map_f: Callable[[T_co], V]) -> V\n
    Source code in atomlib/expr.py
    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n    return self.inner.eval(map_f)\n
    "},{"location":"api/expr/#atomlib.expr.GroupExpr.format","title":"format","text":"
    format(\n    format_scalar: Callable[\n        [ValueToken[T_co, V]], str\n    ] = str,\n    format_op: Callable[[OpToken[T_co, V]], str] = str,\n) -> str\n
    Source code in atomlib/expr.py
    def format(self,\n           format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n           format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n    return f\"{self.lspace}{self.open}{self.inner.format(format_scalar, format_op)}{self.close}{self.rspace}\"\n
    "},{"location":"api/expr/#atomlib.expr.ValueExpr","title":"ValueExpr dataclass","text":"

    Bases: Expr[T_co, V]

    Source code in atomlib/expr.py
    @dataclass\nclass ValueExpr(Expr[T_co, V]):\n    token: ValueToken[T_co, V]\n    lspace: str = \"\"\n    rspace: str = \"\"\n\n    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n        return map_f(self.token.val)\n\n    def format(self,\n               format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n               format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n        return f\"{self.lspace}{format_scalar(self.token)}{self.rspace}\"\n
    "},{"location":"api/expr/#atomlib.expr.ValueExpr.token","title":"token instance-attribute","text":"
    token: ValueToken[T_co, V]\n
    "},{"location":"api/expr/#atomlib.expr.ValueExpr.lspace","title":"lspace class-attribute instance-attribute","text":"
    lspace: str = ''\n
    "},{"location":"api/expr/#atomlib.expr.ValueExpr.rspace","title":"rspace class-attribute instance-attribute","text":"
    rspace: str = ''\n
    "},{"location":"api/expr/#atomlib.expr.ValueExpr.eval","title":"eval","text":"
    eval(map_f: Callable[[T_co], V]) -> V\n
    Source code in atomlib/expr.py
    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n    return map_f(self.token.val)\n
    "},{"location":"api/expr/#atomlib.expr.ValueExpr.format","title":"format","text":"
    format(\n    format_scalar: Callable[\n        [ValueToken[T_co, V]], str\n    ] = str,\n    format_op: Callable[[OpToken[T_co, V]], str] = str,\n) -> str\n
    Source code in atomlib/expr.py
    def format(self,\n           format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n           format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n    return f\"{self.lspace}{format_scalar(self.token)}{self.rspace}\"\n
    "},{"location":"api/expr/#atomlib.expr.Parser","title":"Parser dataclass","text":"

    Bases: Generic[T_co, V]

    Source code in atomlib/expr.py
    @dataclass(init=False)\nclass Parser(t.Generic[T_co, V]):\n    parse_scalar: t.Callable[[str], T_co]\n    ops: t.Dict[str, Op[V]]\n    group_open: t.Dict[str, int]\n    group_close: t.Dict[str, int]\n\n    token_re: re.Pattern = field(init=False)\n    \"\"\"Regex matching operators, brackets, and whitespace\"\"\"\n\n    @t.overload\n    def __init__(self: Parser[str, V2], ops: t.Sequence[Op[V2]],\n                 parse_scalar: t.Optional[t.Callable[[str], str]] = None,\n                 groups: t.Optional[t.Sequence[t.Tuple[str, str]]] = None):\n        ...\n\n    @t.overload\n    def __init__(self: Parser[T, V2], ops: t.Sequence[Op[V2]],\n                 parse_scalar: t.Callable[[str], T],\n                 groups: t.Optional[t.Sequence[t.Tuple[str, str]]] = None):\n        ...\n\n    def __init__(self, ops: t.Sequence[Op[V]],\n                 parse_scalar: t.Optional[t.Callable[[str], t.Union[str, T_co]]] = None,\n                 groups: t.Optional[t.Sequence[t.Tuple[str, str]]] = None):\n        self.parse_scalar = t.cast(t.Callable[[str], T_co], parse_scalar or (lambda s: s))\n\n        if groups is None:\n            groups = [('(', ')'), ('[', ']')]\n\n        match_dict: t.Dict[str, t.Optional[Op[V]]] = {}\n\n        self.group_open = {}\n        self.group_close = {}\n        for (i, (group_open, group_close)) in enumerate(groups):\n            if group_open in match_dict:\n                raise ValueError(f\"Group open token '{group_open}' already defined\")\n            if group_close in match_dict:\n                raise ValueError(f\"Group close token '{group_close}' already defined\")\n            self.group_open[group_open] = i\n            self.group_close[group_close] = i\n            match_dict[group_open] = None\n            match_dict[group_close] = None\n\n        nary_precedences = set()\n        for op in ops:\n            if isinstance(op, NaryOp):\n                if op.precedence in nary_precedences:\n                    raise ValueError(\"N-ary operators must have distinct precedences. \"\n                                     f\"Precedence {op.precedence} conflicts with {op!r}\")\n                nary_precedences.add(op.precedence)\n            for alias in op.aliases:\n                if alias in match_dict:\n                    raise ValueError(f\"Alias '{alias}' already defined\")\n                match_dict[alias] = op\n\n        match_list = list(match_dict.items())\n        match_list.sort(key=lambda a: -len(a[0]))  #longer operators match first\n        self.ops = {k: v for (k, v) in match_list if v is not None}\n\n        def op_to_regex(tup: t.Tuple[str, t.Optional[Op[V]]]):\n            alias, op = tup\n            s = re.escape(alias)  #escape operator for use in regex\n            if op is not None and op.requires_whitespace:\n                #assert operator must be surrounded by whitespace\n                s = r\"(?<=\\s){}(?=\\s)\".format(s)\n            return s\n\n        op_alternation = \"|\".join(map(op_to_regex, match_list))\n        self.token_re = re.compile(f\"(\\\\s+|{op_alternation})\")\n\n    def parse(self, reader: TextIOBase) -> Expr[T_co, V]:\n        state = ParseState(self, reader)\n        expr = state.parse_expr()\n        if not state.empty():\n            raise ValueError(f\"At {state.line}:{state.char}, expected binary operator or end of expression, instead got token {state.peek()}\")\n        return expr\n
    "},{"location":"api/expr/#atomlib.expr.Parser.parse_scalar","title":"parse_scalar instance-attribute","text":"
    parse_scalar: Callable[[str], T_co] = cast(Callable[[str], T_co], parse_scalar or lambda s: s)\n
    "},{"location":"api/expr/#atomlib.expr.Parser.group_open","title":"group_open instance-attribute","text":"
    group_open: Dict[str, int] = {}\n
    "},{"location":"api/expr/#atomlib.expr.Parser.group_close","title":"group_close instance-attribute","text":"
    group_close: Dict[str, int] = {}\n
    "},{"location":"api/expr/#atomlib.expr.Parser.ops","title":"ops instance-attribute","text":"
    ops: Dict[str, Op[V]] = {k: _efor (k, v) in match_list if v is not None}\n
    "},{"location":"api/expr/#atomlib.expr.Parser.token_re","title":"token_re class-attribute instance-attribute","text":"
    token_re: Pattern = compile(f'(\\s+|{op_alternation})')\n

    Regex matching operators, brackets, and whitespace

    "},{"location":"api/expr/#atomlib.expr.Parser.parse","title":"parse","text":"
    parse(reader: TextIOBase) -> Expr[T_co, V]\n
    Source code in atomlib/expr.py
    def parse(self, reader: TextIOBase) -> Expr[T_co, V]:\n    state = ParseState(self, reader)\n    expr = state.parse_expr()\n    if not state.empty():\n        raise ValueError(f\"At {state.line}:{state.char}, expected binary operator or end of expression, instead got token {state.peek()}\")\n    return expr\n
    "},{"location":"api/expr/#atomlib.expr.ParseState","title":"ParseState","text":"

    Bases: Generic[T_co, V]

    Source code in atomlib/expr.py
    class ParseState(t.Generic[T_co, V]):\n    def __init__(self, parser: Parser[T_co, V], reader: TextIOBase):\n        self.parser: Parser[T_co, V] = parser\n        self._reader = reader\n        self._buf: t.Optional[str] = None\n        self._peek: t.List[Token[T_co, V]] = []\n        self.line = 0\n        self.char = 1\n\n    def empty(self) -> bool:\n        return self.peek() is None\n\n    def _get_buf(self) -> t.Optional[str]:\n        if self._buf is not None:\n            return self._buf\n        try:\n            self._buf = next(self._reader)\n            self.line += 1\n            self.char = 1\n        except StopIteration:\n            pass\n        return self._buf\n\n    def _refill_peek(self):\n        if len(self._peek) > 0:\n            return\n        try:\n            buf = next(self._reader)\n            self.line += 1\n            self.char = 1\n        except StopIteration:\n            return None\n        if buf is None or len(buf) == 0:  # type: ignore\n            return None\n\n        split = self.parser.token_re.split(buf)\n        for s in split:\n            if len(s) == 0:\n                continue\n            span = (self.char, self.char + len(s))\n            self.char += len(s)\n            self._peek.append(self.make_token(s, self.line, span))\n\n        self._peek.reverse()\n\n    def peek(self) -> t.Optional[Token[T_co, V]]:\n        self._refill_peek()\n        return self._peek[-1] if len(self._peek) > 0 else None\n\n    def collect_wspace(self) -> str:\n        wspace = \"\"\n        while True:\n            token = self.peek()\n            if not isinstance(token, WhitespaceToken):\n                break\n            wspace += token.raw\n            self.next()\n        return wspace\n\n    def make_token(self, s: str, line: int, span: t.Tuple[int, int]) -> Token[T_co, V]:\n        if s in self.parser.group_open:\n            return GroupOpenToken(s, line, span)\n        if s in self.parser.group_close:\n            return GroupCloseToken(s, line, span)\n        if s in self.parser.ops:\n            return OpToken(s, line, span, self.parser.ops[s])\n        if WSPACE_RE.fullmatch(s):\n            return WhitespaceToken(s, line, span)\n\n        try:\n            return ValueToken(s, line, span, self.parser.parse_scalar(s))\n        except (ValueError, TypeError):\n            raise ValueError(f\"Syntax error at {line}:{span[0]}: Unexpected token '{s}'\") from None\n\n    def next(self) -> t.Optional[Token[T_co, V]]:\n        token = self.peek()\n        if token is not None:\n            self._peek.pop()\n        return token\n\n    def parse_expr(self) -> Expr[T_co, V]:\n        \"\"\"\n            EXPR := PRIMARY, [ BINARY ]\n        \"\"\"\n        logging.debug(\"parse_expr()\")\n        lhs = self.parse_primary()\n        return self.parse_nary(lhs)\n\n    def parse_nary(self, lhs: Expr[T_co, V], level: t.Optional[int] = None) -> Expr[T_co, V]:\n        \"\"\"\n            NARY := { BINARY_OP | NARY_OP, ( PRIMARY, ? higher precedence NARY ? ) }\n        \"\"\"\n        logging.debug(f\"parse_nary({lhs}, level={level})\")\n        token = self.peek()\n        logging.debug(f\"token: '{token!r}'\")\n\n        while token is not None:\n            if not isinstance(token, OpToken) or \\\n               not isinstance(token.op, (NaryOp, BinaryOp)):\n                break\n\n            if level is not None and not token.op.precedes(level):\n                # next op has lower precedence, it needs to be parsed at a higher level\n                break\n\n            self.next()\n            rhs = self.parse_primary()\n            logging.debug(f\"rhs: '{rhs}'\")\n\n            inner = self.peek()\n            if inner is not None and isinstance(inner, OpToken):\n                inner_op = inner.op\n                if isinstance(inner_op, (NaryOp, BinaryOp)) and \\\n                    inner_op.precedes(token.op):\n                    #rhs is actually lhs of an inner expression\n                    rhs = self.parse_nary(rhs, token.op.precedence)\n\n            # append rhs to lhs and loop\n            if isinstance(token.op, NaryOp):\n                if isinstance(lhs, NaryExpr) and token.op == lhs.op:\n                    # append to existing n-ary node\n                    lhs = NaryExpr(list(lhs.op_tokens) + [token], list(lhs.args) + [rhs])\n                else:\n                    # make new n-ary expression\n                    lhs = NaryExpr([token], [lhs, rhs])\n            else:\n                # or make binary expression\n                lhs = BinaryExpr(token, lhs, rhs)\n\n            token = self.peek()\n\n        return lhs\n\n    def parse_primary(self) -> Expr[T_co, V]:\n        \"\"\"\n            PRIMARY := GROUP_OPEN, EXPR, GROUP_CLOSE | UNARY_OP, PRIMARY | SCALAR\n        \"\"\"\n\n        logging.debug(\"parse_primary()\")\n        lspace = self.collect_wspace()\n        token = self.peek()\n        logging.debug(f\"token: '{token!r}'\")\n        if token is None:\n            raise ValueError(\"Unexpected EOF while parsing expression\")\n\n        if isinstance(token, GroupOpenToken):\n            self.next()\n            inner = self.parse_expr()\n            close = self.next()\n            rspace = self.collect_wspace()\n            if close is None:\n                raise ValueError(f\"Unclosed delimeter '{token.raw}' opened at {token.line}:{token.span[0]}\")\n            if not isinstance(close, GroupCloseToken):\n                raise ValueError(f\"At {close.line}:{close.span[0]}: Expected operator or group close, instead got '{close.raw}'\")\n            if self.parser.group_open[token.raw] != self.parser.group_close[close.raw]:\n                raise ValueError(f\"At {token.line}:{token.span[0]}-{close.span[1]}: Mismatched delimeters: '{token.raw}' closed with '{close.raw}'\")\n            return GroupExpr(token, inner, close, lspace, rspace)\n\n        if isinstance(token, GroupCloseToken):\n            raise ValueError(f\"At {token.line}:{token.span[0]}: Unexpected delimeter '{token.raw.strip()}'\")\n\n        if isinstance(token, OpToken):\n            self.next()\n            if not isinstance(token.op, UnaryOp):\n                raise ValueError(f\"At {token.line}:{token.span[0]}: Unexpected operator '{token.raw}'. Expected a value or prefix operator.\")\n            inner = self.parse_primary()\n            return UnaryExpr(token, inner, lspace)\n\n        if isinstance(token, ValueToken):\n            self.next()\n            rspace = self.collect_wspace()\n            return ValueExpr(token, lspace, rspace)\n\n        raise TypeError(f\"Unknown token type '{type(token)}'\")\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.parser","title":"parser instance-attribute","text":"
    parser: Parser[T_co, V] = parser\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.line","title":"line instance-attribute","text":"
    line = 0\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.char","title":"char instance-attribute","text":"
    char = 1\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.empty","title":"empty","text":"
    empty() -> bool\n
    Source code in atomlib/expr.py
    def empty(self) -> bool:\n    return self.peek() is None\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.peek","title":"peek","text":"
    peek() -> Optional[Token[T_co, V]]\n
    Source code in atomlib/expr.py
    def peek(self) -> t.Optional[Token[T_co, V]]:\n    self._refill_peek()\n    return self._peek[-1] if len(self._peek) > 0 else None\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.collect_wspace","title":"collect_wspace","text":"
    collect_wspace() -> str\n
    Source code in atomlib/expr.py
    def collect_wspace(self) -> str:\n    wspace = \"\"\n    while True:\n        token = self.peek()\n        if not isinstance(token, WhitespaceToken):\n            break\n        wspace += token.raw\n        self.next()\n    return wspace\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.make_token","title":"make_token","text":"
    make_token(\n    s: str, line: int, span: Tuple[int, int]\n) -> Token[T_co, V]\n
    Source code in atomlib/expr.py
    def make_token(self, s: str, line: int, span: t.Tuple[int, int]) -> Token[T_co, V]:\n    if s in self.parser.group_open:\n        return GroupOpenToken(s, line, span)\n    if s in self.parser.group_close:\n        return GroupCloseToken(s, line, span)\n    if s in self.parser.ops:\n        return OpToken(s, line, span, self.parser.ops[s])\n    if WSPACE_RE.fullmatch(s):\n        return WhitespaceToken(s, line, span)\n\n    try:\n        return ValueToken(s, line, span, self.parser.parse_scalar(s))\n    except (ValueError, TypeError):\n        raise ValueError(f\"Syntax error at {line}:{span[0]}: Unexpected token '{s}'\") from None\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.next","title":"next","text":"
    next() -> Optional[Token[T_co, V]]\n
    Source code in atomlib/expr.py
    def next(self) -> t.Optional[Token[T_co, V]]:\n    token = self.peek()\n    if token is not None:\n        self._peek.pop()\n    return token\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.parse_expr","title":"parse_expr","text":"
    parse_expr() -> Expr[T_co, V]\n

    EXPR := PRIMARY, [ BINARY ]

    Source code in atomlib/expr.py
    def parse_expr(self) -> Expr[T_co, V]:\n    \"\"\"\n        EXPR := PRIMARY, [ BINARY ]\n    \"\"\"\n    logging.debug(\"parse_expr()\")\n    lhs = self.parse_primary()\n    return self.parse_nary(lhs)\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.parse_nary","title":"parse_nary","text":"
    parse_nary(\n    lhs: Expr[T_co, V], level: Optional[int] = None\n) -> Expr[T_co, V]\n

    NARY := { BINARY_OP | NARY_OP, ( PRIMARY, ? higher precedence NARY ? ) }

    Source code in atomlib/expr.py
    def parse_nary(self, lhs: Expr[T_co, V], level: t.Optional[int] = None) -> Expr[T_co, V]:\n    \"\"\"\n        NARY := { BINARY_OP | NARY_OP, ( PRIMARY, ? higher precedence NARY ? ) }\n    \"\"\"\n    logging.debug(f\"parse_nary({lhs}, level={level})\")\n    token = self.peek()\n    logging.debug(f\"token: '{token!r}'\")\n\n    while token is not None:\n        if not isinstance(token, OpToken) or \\\n           not isinstance(token.op, (NaryOp, BinaryOp)):\n            break\n\n        if level is not None and not token.op.precedes(level):\n            # next op has lower precedence, it needs to be parsed at a higher level\n            break\n\n        self.next()\n        rhs = self.parse_primary()\n        logging.debug(f\"rhs: '{rhs}'\")\n\n        inner = self.peek()\n        if inner is not None and isinstance(inner, OpToken):\n            inner_op = inner.op\n            if isinstance(inner_op, (NaryOp, BinaryOp)) and \\\n                inner_op.precedes(token.op):\n                #rhs is actually lhs of an inner expression\n                rhs = self.parse_nary(rhs, token.op.precedence)\n\n        # append rhs to lhs and loop\n        if isinstance(token.op, NaryOp):\n            if isinstance(lhs, NaryExpr) and token.op == lhs.op:\n                # append to existing n-ary node\n                lhs = NaryExpr(list(lhs.op_tokens) + [token], list(lhs.args) + [rhs])\n            else:\n                # make new n-ary expression\n                lhs = NaryExpr([token], [lhs, rhs])\n        else:\n            # or make binary expression\n            lhs = BinaryExpr(token, lhs, rhs)\n\n        token = self.peek()\n\n    return lhs\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.parse_primary","title":"parse_primary","text":"
    parse_primary() -> Expr[T_co, V]\n

    PRIMARY := GROUP_OPEN, EXPR, GROUP_CLOSE | UNARY_OP, PRIMARY | SCALAR

    Source code in atomlib/expr.py
    def parse_primary(self) -> Expr[T_co, V]:\n    \"\"\"\n        PRIMARY := GROUP_OPEN, EXPR, GROUP_CLOSE | UNARY_OP, PRIMARY | SCALAR\n    \"\"\"\n\n    logging.debug(\"parse_primary()\")\n    lspace = self.collect_wspace()\n    token = self.peek()\n    logging.debug(f\"token: '{token!r}'\")\n    if token is None:\n        raise ValueError(\"Unexpected EOF while parsing expression\")\n\n    if isinstance(token, GroupOpenToken):\n        self.next()\n        inner = self.parse_expr()\n        close = self.next()\n        rspace = self.collect_wspace()\n        if close is None:\n            raise ValueError(f\"Unclosed delimeter '{token.raw}' opened at {token.line}:{token.span[0]}\")\n        if not isinstance(close, GroupCloseToken):\n            raise ValueError(f\"At {close.line}:{close.span[0]}: Expected operator or group close, instead got '{close.raw}'\")\n        if self.parser.group_open[token.raw] != self.parser.group_close[close.raw]:\n            raise ValueError(f\"At {token.line}:{token.span[0]}-{close.span[1]}: Mismatched delimeters: '{token.raw}' closed with '{close.raw}'\")\n        return GroupExpr(token, inner, close, lspace, rspace)\n\n    if isinstance(token, GroupCloseToken):\n        raise ValueError(f\"At {token.line}:{token.span[0]}: Unexpected delimeter '{token.raw.strip()}'\")\n\n    if isinstance(token, OpToken):\n        self.next()\n        if not isinstance(token.op, UnaryOp):\n            raise ValueError(f\"At {token.line}:{token.span[0]}: Unexpected operator '{token.raw}'. Expected a value or prefix operator.\")\n        inner = self.parse_primary()\n        return UnaryExpr(token, inner, lspace)\n\n    if isinstance(token, ValueToken):\n        self.next()\n        rspace = self.collect_wspace()\n        return ValueExpr(token, lspace, rspace)\n\n    raise TypeError(f\"Unknown token type '{type(token)}'\")\n
    "},{"location":"api/expr/#atomlib.expr.SupportsBool","title":"SupportsBool","text":"

    Bases: Protocol

    Source code in atomlib/expr.py
    class SupportsBool(t.Protocol):\n    def __and__(self: SupportsBoolSelf, other: SupportsBoolSelf) -> SupportsBoolSelf:\n        ...\n\n    def __or__(self: SupportsBoolSelf, other: SupportsBoolSelf) -> SupportsBoolSelf:\n        ...\n\n    def __xor__(self: SupportsBoolSelf, other: SupportsBoolSelf) -> SupportsBoolSelf:\n        ...\n\n    def __invert__(self: SupportsBoolSelf) -> SupportsBoolSelf:\n        ...\n
    "},{"location":"api/expr/#atomlib.expr.SupportsNum","title":"SupportsNum","text":"

    Bases: Protocol

    Source code in atomlib/expr.py
    class SupportsNum(t.Protocol):\n    def __add__(self: SupportsNumSelf, other: SupportsNumSelf) -> SupportsNumSelf:\n        ...\n\n    def __sub__(self: SupportsNumSelf, other: SupportsNumSelf) -> SupportsNumSelf:\n        ...\n\n    def __mul__(self: SupportsNumSelf, other: SupportsNumSelf) -> SupportsNumSelf:\n        ...\n\n    def __truediv__(self: SupportsNumSelf, other: SupportsNumSelf) -> SupportsNumSelf:\n        ...\n\n    def __floordiv__(self: SupportsNumSelf, other: SupportsNumSelf) -> SupportsNumSelf:\n        ...\n\n    def __mod__(self: SupportsNumSelf, other: SupportsNumSelf) -> SupportsNumSelf:\n        ...\n\n    def __pow__(self: SupportsNumSelf, other: SupportsNumSelf) -> SupportsNumSelf:\n        ...\n\n    def __neg__(self: SupportsNumSelf) -> SupportsNumSelf:\n        ...\n\n    def __pos__(self: SupportsNumSelf) -> SupportsNumSelf:\n        ...\n
    "},{"location":"api/expr/#atomlib.expr.interleave","title":"interleave","text":"
    interleave(\n    l1: Iterable[T_co], l2: Iterable[T_co]\n) -> Iterator[T_co]\n
    Source code in atomlib/expr.py
    def interleave(l1: t.Iterable[T_co], l2: t.Iterable[T_co]) -> t.Iterator[T_co]:\n    for (v1, v2) in zip_longest(l1, l2):\n        yield v1\n        if v2 is not None:\n            yield v2\n
    "},{"location":"api/expr/#atomlib.expr.parse_numeric","title":"parse_numeric","text":"
    parse_numeric(s: str) -> Union[int, float]\n
    Source code in atomlib/expr.py
    def parse_numeric(s: str) -> t.Union[int, float]:\n    try:\n        return int(s)\n    except ValueError:\n        pass\n    return float(s)\n
    "},{"location":"api/expr/#atomlib.expr.sub","title":"sub","text":"
    sub(lhs: SupportsNum, rhs: Optional[SupportsNum] = None)\n
    Source code in atomlib/expr.py
    def sub(lhs: SupportsNum, rhs: t.Optional[SupportsNum] = None):\n    if rhs is None:\n        return -lhs\n    return lhs-rhs\n
    "},{"location":"api/expr/#atomlib.expr.add","title":"add","text":"
    add(lhs: SupportsNum, rhs: Optional[SupportsNum] = None)\n
    Source code in atomlib/expr.py
    def add(lhs: SupportsNum, rhs: t.Optional[SupportsNum] = None):\n    if rhs is None:\n        return +lhs\n    return lhs+rhs\n
    "},{"location":"api/expr/#atomlib.expr.parse_boolean","title":"parse_boolean","text":"
    parse_boolean(s: str) -> bool\n
    Source code in atomlib/expr.py
    def parse_boolean(s: str) -> bool:\n    if s.lower() in (\"0\", \"false\", \"f\"):\n        return False\n    elif s.lower() in (\"1\", \"true\", \"t\"):\n        return True\n    raise ValueError(f\"Can't parse '{s}' as boolean\")\n
    "},{"location":"api/expr/#atomlib.expr.stack","title":"stack","text":"
    stack(*vs: ndarray) -> ndarray\n
    Source code in atomlib/expr.py
    def stack(*vs: numpy.ndarray) -> numpy.ndarray:\n    return numpy.stack(vs, axis=0)\n
    "},{"location":"api/make/","title":"atomlib.make","text":"

    Functions to create structures and cells.

    "},{"location":"api/make/#atomlib.make.CellType","title":"CellType module-attribute","text":"
    CellType: TypeAlias = Literal['conv', 'prim', 'ortho']\n
    "},{"location":"api/make/#atomlib.make.fcc","title":"fcc","text":"
    fcc(\n    elem: ElemLike,\n    a: Num,\n    *,\n    cell: CellType = \"conv\",\n    additional: Optional[IntoAtoms] = None\n) -> AtomCell\n

    Make a FCC lattice of the specified element, with the given cell. If cell='conv' (the default), return the conventional cell, four atoms with cubic cell symmetry. If cell='prim', return the primitive cell, a single atom with rhombohedral cell symmetry. If cell='ortho', return an orthogonal cell, two atoms in a cell of size [a/sqrt(2), a/sqrt(2), a].

    If additional is specified, those atoms will be added to the lattice (in fractional coordinates).

    PARAMETER DESCRIPTION elem

    Element to add (e.g. 'Al' or 13)

    TYPE: ElemLike

    a

    Lattice parameter (Angstrom)

    TYPE: Num

    cell

    Cell type to return ('conv', 'prim', or 'ortho')

    TYPE: CellType DEFAULT: 'conv'

    additional

    Additional atoms to add to the structure.

    TYPE: Optional[IntoAtoms] DEFAULT: None

    RETURNS DESCRIPTION AtomCell

    Periodic FCC unit cell

    Source code in atomlib/make/__init__.py
    def fcc(elem: ElemLike, a: Num, *, cell: CellType = 'conv', additional: t.Optional[IntoAtoms] = None) -> AtomCell:\n    \"\"\"\n    Make a FCC lattice of the specified element, with the given cell.\n    If `cell='conv'` (the default), return the conventional cell, four atoms with cubic cell symmetry.\n    If `cell='prim'`, return the primitive cell, a single atom with rhombohedral cell symmetry.\n    If `cell='ortho'`, return an orthogonal cell, two atoms in a cell of size `[a/sqrt(2), a/sqrt(2), a]`.\n\n    If `additional` is specified, those atoms will be added to the lattice (in fractional coordinates).\n\n    Args:\n      elem: Element to add (e.g. `'Al'` or `13`)\n      a: Lattice parameter (Angstrom)\n      cell: Cell type to return ('conv', 'prim', or 'ortho')\n      additional: Additional atoms to add to the structure.\n\n    Returns:\n      Periodic FCC unit cell\n    \"\"\"\n\n    elems = [get_elem(elem)]\n    cell = t.cast(CellType, str(cell).lower())\n\n    if cell == 'prim':\n        xs = ys = zs = [0.]\n        ortho = LinearTransform3D(a / 2. * numpy.array([\n            [0., 1., 1.],\n            [1., 0., 1.],\n            [1., 1., 0.],\n        ]))\n    elif cell == 'ortho':\n        elems *= 2\n        xs = ys = zs = [0., 0.5]\n        b = a / numpy.sqrt(2)\n        ortho = LinearTransform3D.scale(b, b, a)\n    elif cell == 'conv':\n        elems *= 4\n        xs = [0., 0., 0.5, 0.5]\n        ys = [0., 0.5, 0., 0.5]\n        zs = [0., 0.5, 0.5, 0.]\n        ortho = LinearTransform3D.scale(all=a)\n    else:\n        raise ValueError(f\"Unknown cell type '{cell}'. Expected 'conv', 'prim', or 'ortho'.\")\n\n    frame = Atoms(dict(x=xs, y=ys, z=zs, elem=elems))\n    if additional is not None:\n        frame = Atoms.concat((frame, additional), how='vertical')\n\n    return AtomCell.from_ortho(frame, ortho, frame='cell_frac')\n
    "},{"location":"api/make/#atomlib.make.wurtzite","title":"wurtzite","text":"
    wurtzite(\n    elems: ElemsLike,\n    a: Num,\n    c: Optional[Num] = None,\n    d: Optional[Num] = None,\n    *,\n    cell: CellType = \"conv\"\n) -> AtomCell\n

    Create a wurzite lattice of the specified two elements, with the given cell. a and c are the hexagonal cell parameters. d is the fractional distance between the two sublattices.

    If cell='prim' or cell='conv' (the default), return a hexagonal unit cell. If cell='ortho', return an orthogonal unit cell constructed from the hexagonal unit cell as \\(\\hat{\\mathbf{a}} = \\mathbf{a}\\), \\(\\hat{\\mathbf{b}} = \\mathbf{a} + 2 \\mathbf{b}\\), \\(\\hat{\\mathbf{c}} = \\mathbf{c}\\).

    PARAMETER DESCRIPTION elems

    Elements to add (e.g. 'AlN' or ('Al', 'N'))

    TYPE: ElemsLike

    a

    Lattice parameter (Angstrom)

    TYPE: Num

    c

    Vertical lattice parameter (Angstrom)

    TYPE: Optional[Num] DEFAULT: None

    d

    Vertical distance between the two sublattices (fractional)

    TYPE: Optional[Num] DEFAULT: None

    cell

    Cell type to return ('conv', 'prim', or 'ortho')

    TYPE: CellType DEFAULT: 'conv'

    RETURNS DESCRIPTION AtomCell

    Periodic wurtzite unit cell

    Source code in atomlib/make/__init__.py
    def wurtzite(elems: ElemsLike, a: Num, c: t.Optional[Num] = None,\n             d: t.Optional[Num] = None, *, cell: CellType = 'conv') -> AtomCell:\n    r\"\"\"\n    Create a wurzite lattice of the specified two elements, with the given cell.\n    `a` and `c` are the hexagonal cell parameters. `d` is the fractional distance\n    between the two sublattices.\n\n    If `cell='prim'` or `cell='conv'` (the default), return a hexagonal unit cell.\n    If `cell='ortho'`, return an orthogonal unit cell constructed from the hexagonal unit cell as\n    $\\hat{\\mathbf{a}} = \\mathbf{a}$, $\\hat{\\mathbf{b}} = \\mathbf{a} + 2 \\mathbf{b}$, $\\hat{\\mathbf{c}} = \\mathbf{c}$.\n\n    Args:\n      elems: Elements to add (e.g. `'AlN'` or `('Al', 'N')`)\n      a: Lattice parameter (Angstrom)\n      c: Vertical lattice parameter (Angstrom)\n      d: Vertical distance between the two sublattices (fractional)\n      cell: Cell type to return ('conv', 'prim', or 'ortho')\n\n    Returns:\n      Periodic wurtzite unit cell\n    \"\"\"\n    elems = [t[0] for t in get_elems(elems)]\n\n    if len(elems) != 2:\n        raise ValueError(\"Expected two elements.\")\n\n    # default to ideal c/a\n    c_a = float(numpy.sqrt(8. / 3.) if c is None else c / a)\n\n    d = 0.25 + 1 / (3 * c_a**2) if d is None else d\n    if not 0 < d < 0.5:\n        raise ValueError(f\"Invalid 'd' parameter: {d}\")\n\n    cell = t.cast(CellType, str(cell).lower())\n    if cell not in ('prim', 'conv', 'ortho'):\n        raise ValueError(f\"Unknown cell type '{cell}'. Expected 'conv', 'prim', or 'ortho'.\")\n\n    ortho = cell_to_ortho(\n        a * numpy.array([1., 1., c_a]), \n        numpy.pi * numpy.array([1/2., 1/2., 2/3.])\n    )\n    xs = [2/3, 2/3, 1/3, 1/3]\n    ys = [1/3, 1/3, 2/3, 2/3]\n    #zs = [1. - d, 0., 0.5 - d, 0.5]\n    zs = [0.5, 0.5 + d, 0., d]\n    elems *= 2\n\n    frame = Atoms(dict(x=xs, y=ys, z=zs, elem=elems))\n    atoms = AtomCell.from_ortho(frame, ortho, frame='cell_frac')\n    if cell == 'ortho':\n        return _ortho_hexagonal(atoms)\n    return atoms\n
    "},{"location":"api/make/#atomlib.make.graphite","title":"graphite","text":"
    graphite(\n    elem: Union[str, ElemLike, None] = None,\n    a: Optional[Num] = None,\n    c: Optional[Num] = None,\n    *,\n    cell: CellType = \"conv\"\n)\n
    Source code in atomlib/make/__init__.py
    def graphite(elem: t.Union[str, ElemLike, None] = None, a: t.Optional[Num] = None,\n             c: t.Optional[Num] = None, *, cell: CellType = 'conv'):\n    if elem is None:\n        elem = 6\n    else:\n        elem = get_elem(elem)\n        if elem != 6 and a is None or c is None:\n            raise ValueError(\"'a' and 'c' must be specified for non-graphite elements.\")\n\n    if a is None:\n        a = 2.47\n    if c is None:\n        c = 8.69\n\n    cell = t.cast(CellType, str(cell).lower())\n    if cell not in ('prim', 'conv', 'ortho'):\n        raise ValueError(f\"Unknown cell type '{cell}'. Expected 'conv', 'prim', or 'ortho'.\")\n\n    ortho = cell_to_ortho(\n        numpy.array([a, a, c]), \n        numpy.pi * numpy.array([1/2., 1/2., 2/3.])\n    )\n    xs = [0., 2/3, 0., 1/3]\n    ys = [0., 1/3, 0., 2/3]\n    zs = [0., 0., 1/2, 1/2]\n    elems = [elem] * 4\n\n    frame = Atoms(dict(x=xs, y=ys, z=zs, elem=elems))\n    atoms = AtomCell.from_ortho(frame, ortho, frame='cell_frac')\n\n    if cell == 'ortho':\n        return _ortho_hexagonal(atoms)\n    return atoms\n
    "},{"location":"api/make/#atomlib.make.rocksalt","title":"rocksalt","text":"
    rocksalt(\n    elems: ElemsLike, a: Num, *, cell: CellType = \"conv\"\n) -> AtomCell\n

    Create a rock salt FCC structure AB. Returns the same cell types as fcc.

    PARAMETER DESCRIPTION elems

    Elements to add (e.g. 'NaCl' or ('Na', 'Cl'))

    TYPE: ElemsLike

    a

    Lattice parameter (Angstrom)

    TYPE: Num

    cell

    Cell type to return ('conv', 'prim', or 'ortho'). Returns the same cell types as fcc.

    TYPE: CellType DEFAULT: 'conv'

    RETURNS DESCRIPTION AtomCell

    Periodic rocksalt unit cell

    Source code in atomlib/make/__init__.py
    def rocksalt(elems: ElemsLike, a: Num, *,\n             cell: CellType = 'conv') -> AtomCell:\n    \"\"\"\n    Create a rock salt FCC structure AB. Returns the same cell types as `fcc`.\n\n    Args:\n      elems: Elements to add (e.g. `'NaCl'` or `('Na', 'Cl')`)\n      a: Lattice parameter (Angstrom)\n      cell: Cell type to return ('conv', 'prim', or 'ortho'). Returns\n            the same cell types as `fcc`.\n\n    Returns:\n      Periodic rocksalt unit cell\n    \"\"\"\n    elems = [t[0] for t in get_elems(elems)]\n\n    if len(elems) != 2:\n        raise ValueError(\"Expected two elements.\")\n\n    if cell == 'prim':\n        additional: t.Dict[str, t.Any] = {\n            'x': [-0.5],\n            'y': [0.5],\n            'z': [0.5],\n            'elem': [elems[1]],\n        }\n    elif cell == 'ortho':\n        additional: t.Dict[str, t.Any] = {\n            'x': [0.5, 0.0],\n            'y': [0.5, 0.0],\n            'z': [0.0, 0.5],\n            'elem': [elems[1]] * 2,\n        }\n    elif cell == 'conv':\n        additional: t.Dict[str, t.Any] = {\n            'x': [0.5, 0.0, 0.0, 0.5],\n            'y': [0.0, 0.5, 0.0, 0.5],\n            'z': [0.0, 0.0, 0.5, 0.5],\n            'elem': [elems[1]] * 4,\n        }\n    else:\n        raise ValueError(f\"Unknown cell type '{cell}'. Expected 'conv', 'prim', or 'ortho'.\")\n\n    return fcc(elems[0], a, cell=cell, additional=additional)\n
    "},{"location":"api/make/#atomlib.make.zincblende","title":"zincblende","text":"
    zincblende(\n    elems: ElemsLike, a: Num, *, cell: CellType = \"conv\"\n) -> AtomCell\n

    Create a zinc-blende FCC structure AB.

    PARAMETER DESCRIPTION elems

    Elements to add (e.g. 'ZnS' or ('Zn', 'S'))

    TYPE: ElemsLike

    a

    Lattice parameter (Angstrom)

    TYPE: Num

    cell

    Cell type to return ('conv', 'prim', or 'ortho'). Returns the same cell types as fcc.

    TYPE: CellType DEFAULT: 'conv'

    RETURNS DESCRIPTION AtomCell

    Periodic zinc-blende unit cell

    Source code in atomlib/make/__init__.py
    def zincblende(elems: ElemsLike, a: Num, *,\n               cell: CellType = 'conv') -> AtomCell:\n    \"\"\"\n    Create a zinc-blende FCC structure AB.\n\n    Args:\n      elems: Elements to add (e.g. `'ZnS'` or `('Zn', 'S')`)\n      a: Lattice parameter (Angstrom)\n      cell: Cell type to return ('conv', 'prim', or 'ortho'). Returns\n            the same cell types as `fcc`.\n\n    Returns:\n      Periodic zinc-blende unit cell\n    \"\"\"\n    elems = [t[0] for t in get_elems(elems)]\n\n    if len(elems) != 2:\n        raise ValueError(\"Expected two elements.\")\n\n    if cell == 'prim':\n        d = [0.25]\n        additional: t.Dict[str, t.Any] = {\n            'x': d,\n            'y': d,\n            'z': d,\n            'elem': [elems[1]],\n        }\n    elif cell == 'ortho':\n        additional: t.Dict[str, t.Any] = {\n            'x': [0.5, 0.0],\n            'y': [0.0, 0.5],\n            'z': [0.25, 0.75],\n            'elem': [elems[1]] * 2,\n        }\n    elif cell == 'conv':\n        additional: t.Dict[str, t.Any] = {\n            'x': [0.25, 0.25, 0.75, 0.75],\n            'y': [0.25, 0.75, 0.25, 0.75],\n            'z': [0.25, 0.75, 0.75, 0.25],\n            'elem': [elems[1]] * 4,\n        }\n    else:\n        raise ValueError(f\"Unknown cell type '{cell}'. Expected 'conv', 'prim', or 'ortho'.\")\n\n    return fcc(elems[0], a, cell=cell, additional=additional)\n
    "},{"location":"api/make/#atomlib.make.diamond","title":"diamond","text":"
    diamond(\n    elem: Optional[ElemLike] = None,\n    a: Optional[Num] = None,\n    *,\n    cell: CellType = \"conv\"\n) -> AtomCell\n

    Create a diamond cubic FCC structure. elem and a can be left unspecified to return a diamond structure. Otherwise, both must be specified.

    PARAMETER DESCRIPTION elem

    Element to add (e.g. 'C')

    TYPE: Optional[ElemLike] DEFAULT: None

    a

    Lattice parameter (Angstrom)

    TYPE: Optional[Num] DEFAULT: None

    cell

    Cell type to return ('conv', 'prim', or 'ortho'). Returns the same cell types as fcc.

    TYPE: CellType DEFAULT: 'conv'

    RETURNS DESCRIPTION AtomCell

    Periodic diamond cubic unit cell

    Source code in atomlib/make/__init__.py
    def diamond(elem: t.Optional[ElemLike] = None, a: t.Optional[Num] = None, *,\n            cell: CellType = 'conv') -> AtomCell:\n    \"\"\"\n    Create a diamond cubic FCC structure. `elem` and `a` can be left\n    unspecified to return a diamond structure. Otherwise, both\n    must be specified.\n\n    Args:\n      elem: Element to add (e.g. `'C'`)\n      a: Lattice parameter (Angstrom)\n      cell: Cell type to return ('conv', 'prim', or 'ortho'). Returns\n            the same cell types as `fcc`.\n\n    Returns:\n      Periodic diamond cubic unit cell\n    \"\"\"\n    if elem is None:\n        elems = (6, 6)\n    else:\n        elem = get_elem(elem)\n        elems = (elem, elem)\n\n    if a is None:\n        if elems == (6, 6):\n            # diamond lattice parameter\n            a = 3.567\n        else:\n            raise ValueError(\"Must specify lattice parameter 'a'.\")\n\n    return zincblende(elems, a, cell=cell)\n
    "},{"location":"api/make/#atomlib.make.fluorite","title":"fluorite","text":"
    fluorite(\n    elems: ElemsLike, a: Num, *, cell: CellType = \"conv\"\n) -> AtomCell\n

    Create a fluorite FCC structure \\(\\mathrm{AB_2}\\). Returns the same cell types as fcc.

    PARAMETER DESCRIPTION elems

    Elements to add (e.g. 'CaF' or ('Ca', 'F'))

    TYPE: ElemsLike

    a

    Lattice parameter (Angstrom)

    TYPE: Num

    cell

    Cell type to return ('conv', 'prim', or 'ortho'). Returns the same cell types as fcc.

    TYPE: CellType DEFAULT: 'conv'

    RETURNS DESCRIPTION AtomCell

    Periodic fluorite unit cell

    Source code in atomlib/make/__init__.py
    def fluorite(elems: ElemsLike, a: Num, *,\n             cell: CellType = 'conv') -> AtomCell:\n    \"\"\"\n    Create a fluorite FCC structure $\\\\mathrm{AB_2}$. Returns the same cell types as `fcc`.\n\n    Args:\n      elems: Elements to add (e.g. `'CaF'` or `('Ca', 'F')`)\n      a: Lattice parameter (Angstrom)\n      cell: Cell type to return ('conv', 'prim', or 'ortho'). Returns\n            the same cell types as `fcc`.\n\n    Returns:\n      Periodic fluorite unit cell\n    \"\"\"\n    elems = [t[0] for t in get_elems(elems)]\n\n    if len(elems) != 2:\n        raise ValueError(\"Expected two elements.\")\n\n    if cell == 'prim':\n        d = [0.25, 0.75]\n        additional: t.Dict[str, t.Any] = {\n            'x': d, 'y': d, 'z': d,\n            'elem': [elems[1]] * 2,\n        }\n    elif cell == 'ortho':\n        additional: t.Dict[str, t.Any] = {\n            'x': [0.5, 0.5, 0.0, 0.0],\n            'y': [0.0, 0.0, 0.5, 0.5],\n            'z': [0.25, 0.75, 0.25, 0.75],\n            'elem': [elems[1]] * 4,\n        }\n    elif cell == 'conv':\n        additional: t.Dict[str, t.Any] = {\n            'x': [0.25] * 4 + [0.75] * 4,\n            'y': [0.25, 0.25, 0.75, 0.75, 0.25, 0.25, 0.75, 0.75],\n            'z': [0.25, 0.75, 0.25, 0.75, 0.25, 0.75, 0.25, 0.75],\n            'elem': [elems[1]] * 8,\n        }\n    else:\n        raise ValueError(f\"Unknown cell type '{cell}'. Expected 'conv', 'prim', or 'ortho'.\")\n\n    return fcc(elems[0], a, cell=cell, additional=additional)\n
    "},{"location":"api/make/#atomlib.make.cesium_chloride","title":"cesium_chloride","text":"
    cesium_chloride(\n    elems: ElemsLike = \"CsCl\",\n    a: Optional[Num] = None,\n    *,\n    d: Optional[Num] = None,\n    cell: CellType = \"conv\"\n) -> AtomCell\n

    Create a cesium chloride structure \\(\\mathrm{AB}\\). CsCl is simple cubic, so all cell types are the same.

    Only one of a (lattice parameter) or d (bond distance) needs to be specified.

    PARAMETER DESCRIPTION elems

    Elements to add (e.g. 'CsCl' or ('Cs', 'Cl'))

    TYPE: ElemsLike DEFAULT: 'CsCl'

    a

    Lattice parameter (Angstrom)

    TYPE: Optional[Num] DEFAULT: None

    d

    Nearest-neighbor bond distance (Angstrom)

    TYPE: Optional[Num] DEFAULT: None

    cell

    Cell type to return ('conv', 'prim', or 'ortho'). All are identical for this structure

    TYPE: CellType DEFAULT: 'conv'

    RETURNS DESCRIPTION AtomCell

    Periodic cesium chloride unit cell

    Source code in atomlib/make/__init__.py
    def cesium_chloride(elems: ElemsLike = 'CsCl', a: t.Optional[Num] = None, *,\n                    d: t.Optional[Num] = None, cell: CellType = 'conv') -> AtomCell:\n    \"\"\"\n    Create a cesium chloride structure $\\\\mathrm{AB}$.\n    CsCl is simple cubic, so all cell types are the same.\n\n    Only one of `a` (lattice parameter) or `d` (bond distance) needs to be specified.\n\n    Args:\n      elems: Elements to add (e.g. `'CsCl'` or `('Cs', 'Cl')`)\n      a: Lattice parameter (Angstrom)\n      d: Nearest-neighbor bond distance (Angstrom)\n      cell: Cell type to return ('conv', 'prim', or 'ortho').\n            All are identical for this structure\n\n    Returns:\n      Periodic cesium chloride unit cell\n    \"\"\"\n    elems = [t[0] for t in get_elems(elems)]\n\n    if len(elems) != 2:\n        raise ValueError(\"Expected two elements.\")\n\n    if a is not None and d is not None:\n        raise ValueError(\"Both 'a' and 'd' cannot be specified.\")\n\n    if a is None:\n        if d is not None:\n            a_ = d * 2/numpy.sqrt(3)\n        elif elems == [55, 17]:\n            # CsCl lattice parameter\n            a_ = 4.123\n        else:\n            raise ValueError(\"Must specify either 'a' or 'd' lattice parameter\")\n    else:\n        a_ = a\n\n    ortho = cell_to_ortho([a_] * 3)\n\n    frame = Atoms(dict(x=[0., 0.5], y=[0., 0.5], z=[0., 0.5], elem=elems))\n    return AtomCell.from_ortho(frame, ortho, frame='cell_frac')\n
    "},{"location":"api/make/#atomlib.make.perovskite","title":"perovskite","text":"
    perovskite(\n    elems: ElemsLike,\n    cell_size: VecLike,\n    *,\n    cell: CellType = \"conv\"\n) -> AtomCell\n

    Create a perovskite structure \\(\\mathrm{ABX_3}\\).

    A is placed at the origin and B is placed at the cell center. cell_size determines whether a cubic, tetragonal, or orthorhombic structure is created. For instance, cell_size=3. returns a cubic structure, while cell_size=[3., 5.] returns a tetragonal structure a=3, c=5.

    All cell types are the same for perovskite, so the cell parameter has no effect.

    PARAMETER DESCRIPTION elems

    Elements to add (e.g. 'CaTiO' or ('Ca', 'Ti', 'O'))

    TYPE: ElemsLike

    cell_size

    Lattice parameters (e.g. 3.0 (cubic), [3.0, 5.0] (tetragonal), or [3.0, 4.0, 5.0] (orthorhombic)).

    TYPE: VecLike

    cell

    Cell type to return ('conv', 'prim', or 'ortho'). All are identical for this structure

    TYPE: CellType DEFAULT: 'conv'

    RETURNS DESCRIPTION AtomCell

    Periodic perovskite unit cell.

    Source code in atomlib/make/__init__.py
    def perovskite(elems: ElemsLike, cell_size: VecLike, *,\n               cell: CellType = 'conv') -> AtomCell:\n    \"\"\"\n    Create a perovskite structure $\\\\mathrm{ABX_3}$.\n\n    `A` is placed at the origin and `B` is placed at the cell center.\n    `cell_size` determines whether a cubic, tetragonal, or orthorhombic\n    structure is created. For instance, `cell_size=3.` returns a cubic\n    structure, while `cell_size=[3., 5.]` returns a tetragonal structure\n    `a=3`, `c=5`.\n\n    All cell types are the same for perovskite, so the `cell` parameter\n    has no effect.\n\n    Args:\n      elems: Elements to add (e.g. `'CaTiO'` or `('Ca', 'Ti', 'O')`)\n      cell_size: Lattice parameters (e.g. `3.0` (cubic), `[3.0, 5.0]`\n                 (tetragonal), or `[3.0, 4.0, 5.0]` (orthorhombic)).\n      cell: Cell type to return ('conv', 'prim', or 'ortho').\n            All are identical for this structure\n\n    Returns:\n      Periodic perovskite unit cell.\n    \"\"\"\n    elems = [t[0] for t in get_elems(elems)]\n\n    if len(elems) != 3:\n        raise ValueError(\"Expected three elements.\")\n\n    cell_size = numpy.atleast_1d(cell_size)\n    if cell_size.squeeze().ndim > 1:\n        raise ValueError(\"Expected a 1D vector\")\n    if len(cell_size) == 2:\n        # tetragonal shortcut\n        cell_size = numpy.array([cell_size[0], cell_size[0], cell_size[1]])\n\n    if cell not in ('prim', 'ortho', 'conv'):\n        raise ValueError(f\"Unknown cell type '{cell}'. Expected 'conv', 'prim', or 'ortho'.\")\n\n    xs = [0., 0.5, 0., 0.5, 0.5]\n    ys = [0., 0.5, 0.5, 0., 0.5]\n    zs = [0., 0.5, 0.5, 0.5, 0.]\n    elems = [elems[0], elems[1], *([elems[2]] * 3)]\n\n    atoms = Atoms(dict(x=xs, y=ys, z=zs, elem=elems))\n    return AtomCell(atoms, Cell.from_unit_cell(cell_size), frame='cell_frac')\n
    "},{"location":"api/make/#atomlib.make.random","title":"random","text":"
    random(\n    cell: Union[Cell, VecLike],\n    elems: ElemsLike,\n    density: float,\n    seed: Optional[object] = None,\n    **extra_cols: Any\n) -> AtomCell\n

    Make a random arrangement of atoms inside cell (Cell or cell_size vector).

    PARAMETER DESCRIPTION elems

    Elements to add (e.g. 'C', 6, or SiO2)

    TYPE: ElemsLike

    density

    Mean mass density to target (g/cm^3)

    TYPE: float

    seed

    Deterministic random seed to add (any object)

    TYPE: Optional[object] DEFAULT: None

    extra_cols

    Extra parameters to add to each atom

    TYPE: Any DEFAULT: {}

    RETURNS DESCRIPTION AtomCell

    A random arrangement of atoms

    Source code in atomlib/make/__init__.py
    def random(cell: t.Union[Cell, VecLike], elems: ElemsLike, density: float,\n           seed: t.Optional[object] = None, **extra_cols: t.Any) -> AtomCell:\n    \"\"\"\n    Make a random arrangement of atoms inside `cell`\n    ([`Cell`][atomlib.cell.Cell] or cell_size vector).\n\n    Args:\n      elems: Elements to add (e.g. `'C'`, `6`, or `SiO2`)\n      density: Mean mass density to target (g/cm^3)\n      seed: Deterministic random seed to add (any object)\n      extra_cols: Extra parameters to add to each atom\n\n    Returns:\n      A random arrangement of atoms\n    \"\"\"\n    if not isinstance(cell, Cell):\n        cell = Cell.from_unit_cell(cell, pbc=[True, True, True])\n\n    elems = get_elems(elems)\n    # normalize formula unit\n    total_num = sum(elem[1] for elem in elems)\n    elems = [(elem, num / total_num) for (elem, num) in elems]\n\n    total_mass = sum(get_mass(elem) * num for (elem, num) in elems)\n    # g/cm^3 / g/mol * 6.022e23/mol * 1e-24 cm^3/angstrom^3\n    total_number_density = density / total_mass * 0.60221408\n\n    rng = numpy.random.RandomState(proc_seed(seed, 'make.random'))\n    atoms = []\n\n    for (elem, frac) in elems:\n        n = int(numpy.round(numpy.prod(cell.box_size) * total_number_density * frac).astype(int))\n        pos = rng.uniform(0., 1., size=(3, n))\n\n        atoms.append(Atoms({\n            'x': pos[0], 'y': pos[1], 'z': pos[2],\n            'elem': [elem] * n,\n            **{k: [v] * n for (k, v) in extra_cols.items()}\n        }))\n\n    return AtomCell(Atoms.concat(atoms), cell=cell, frame='cell_box')\n
    "},{"location":"api/make/#atomlib.make.slab","title":"slab","text":"
    slab(\n    atoms: HasAtomCellT,\n    zone: VecLike = (0.0, 0.0, 1.0),\n    horz: VecLike = (1.0, 0.0, 0.0),\n    *,\n    max_n: int = 50,\n    tol: float = 0.001\n) -> HasAtomCellT\n

    Create an periodic orthogonal slab of the periodic cell atoms.

    zone in the original crystal will point along the +z-axis, and horz (minus the zone component) wil point along the +x-axis.

    Finds a periodic orthogonal slab with less than tol amount of strain, and no more than max_n cells on one side.

    PARAMETER DESCRIPTION atoms

    Input structure

    TYPE: HasAtomCellT

    zone

    Zone to align with the +z-axis

    TYPE: VecLike DEFAULT: (0.0, 0.0, 1.0)

    horz

    Zone to align with the +x-axis

    TYPE: VecLike DEFAULT: (1.0, 0.0, 0.0)

    max_n

    Maximum number of unit cells to search

    TYPE: int DEFAULT: 50

    tol

    Maximum strain tolerance

    TYPE: float DEFAULT: 0.001

    RETURNS DESCRIPTION HasAtomCellT

    A periodic, orthogonal cell

    Source code in atomlib/make/__init__.py
    def slab(atoms: HasAtomCellT, zone: VecLike = (0., 0., 1.), horz: VecLike = (1., 0., 0.), *,\n         max_n: int = 50, tol: float = 0.001) -> HasAtomCellT:\n    \"\"\"\n    Create an periodic orthogonal slab of the periodic cell `atoms`.\n\n    `zone` in the original crystal will point along the +z-axis,\n    and `horz` (minus the `zone` component) wil point along the +x-axis.\n\n    Finds a periodic orthogonal slab with less than `tol` amount of strain,\n    and no more than `max_n` cells on one side.\n\n    Args:\n      atoms: Input structure\n      zone: Zone to align with the +z-axis\n      horz: Zone to align with the +x-axis\n      max_n: Maximum number of unit cells to search\n      tol: Maximum strain tolerance\n\n    Returns:\n      A periodic, orthogonal cell\n    \"\"\"\n\n    # align `zone` with the z-axis, and `horz` with the x-axis\n    zone = reduce_vec(to_vec3(zone))  # ensure `zone` is a lattice vector\n    # TODO should this go from 'local' or 'ortho'?\n    cell_transform = atoms.get_transform('local', 'cell_frac').to_linear()\n    align_transform = LinearTransform3D.align(cell_transform @ zone, cell_transform @ horz)\n    transform = (align_transform @ cell_transform)\n    z = transform @ zone\n    numpy.testing.assert_allclose(z / numpy.linalg.norm(z), [0., 0., 1.], atol=1e-6)\n\n    # generate lattice points\n    lattice_coords = numpy.stack(numpy.meshgrid(*[numpy.arange(-max_n, max_n)]*3), axis=-1).reshape(-1, 3)\n    realspace_coords = transform @ lattice_coords\n    realspace_norm = numpy.linalg.norm(realspace_coords, axis=-1)\n\n    # sort coordinates from smallest to largest (TODO this method is slow)\n    sorting = realspace_norm.argsort()[1:]\n    realspace_norm = realspace_norm[sorting]\n    lattice_coords = lattice_coords[sorting]\n    realspace_coords = realspace_coords[sorting]\n    tols = realspace_norm * tol\n\n    # find lattice points which are acceptablly close to orthogonal\n    (x_close, y_close, z_close) = split_arr(numpy.abs(realspace_coords) < tols[:, None], axis=-1)\n\n    try:\n        x_i = numpy.argwhere(z_close & ~x_close & y_close)[0, 0],\n        y_i = numpy.argwhere(z_close & ~y_close & x_close)[0, 0],\n\n        logging.info(f\"x: {lattice_coords[x_i]} transforms to {realspace_coords[x_i]}\")\n        logging.info(f\"y: {lattice_coords[y_i]} transforms to {realspace_coords[y_i]}\")\n        logging.info(f\"z: {zone} transforms to {z}\")\n    except IndexError:\n        raise ValueError(\"Couldn't find a viable surface along zone {zone}\") from None\n\n    # orient vectors correctly\n    x = realspace_coords[x_i] * numpy.sign(realspace_coords[x_i][0])\n    y = realspace_coords[y_i] * numpy.sign(realspace_coords[y_i][1])\n\n    # repeat original lattice to cover orthogonal lattice\n    pts = transform.inverse() @ BBox3D.from_pts([numpy.zeros(3), x, y, z]).corners()\n    raw_atoms = atoms._repeat_to_contain(pts).get_atoms('local').transform(align_transform)\n    cell = Cell.from_ortho(LinearTransform3D(numpy.stack([x, y, z], axis=0)))\n\n    # strain cell to orthogonal (with atoms in the ``cell`` frame)\n    raw_atoms = raw_atoms.transform(cell.get_transform('cell', 'local'))\n    cell = cell.strain_orthogonal()\n    return atoms.with_cell(cell).with_atoms(raw_atoms, 'cell').crop_to_box()\n
    "},{"location":"api/make/#atomlib.make.stacking_sequence","title":"stacking_sequence","text":"
    stacking_sequence(\n    layer: AtomCell,\n    sequence: str,\n    shift_vector: VecLike = (1, 0, 0),\n    *,\n    n_layers: int = 3\n) -> AtomCell\n

    Create an arbitrary stacking sequence from a single layer layer.

    PARAMETER DESCRIPTION layer

    Layer to stack into a stacking sequence. Will be stacked along the c axis.

    TYPE: AtomCell

    sequence

    Stacking sequence. Each layer should be \"A\", \"B\", or \"C\" (in the common case where there are three layers). Example: \"ABCABC\".

    TYPE: str

    shift_vector

    Shift to apply, in fractional coordinates. The shift between each layer will be shift_vector/n_layers. Typically an integer value, to preserve periodicity. Defaults to [100].

    TYPE: VecLike DEFAULT: (1, 0, 0)

    n_layers

    Number of layers which corresponds to a shift of a complete lattice vector. Defaults to 3, the case for FCC and HCP.

    TYPE: int DEFAULT: 3

    RETURNS DESCRIPTION AtomCell

    An AtomCell containing the stacked structure.

    Source code in atomlib/make/__init__.py
    def stacking_sequence(layer: AtomCell, sequence: str, shift_vector: VecLike = (1, 0, 0), *,\n                      n_layers: int = 3) -> AtomCell:\n    \"\"\"\n    Create an arbitrary stacking sequence from a single layer `layer`.\n\n    Args:\n      layer: Layer to stack into a stacking sequence. Will be stacked along the c axis.\n      sequence: Stacking sequence. Each layer should be \"A\", \"B\", or \"C\" (in the common case\n                where there are three layers). Example: `\"ABCABC\"`.\n      shift_vector: Shift to apply, in fractional coordinates. The shift between each layer\n                    will be `shift_vector/n_layers`. Typically an integer value, to\n                    preserve periodicity. Defaults to `[100]`.\n      n_layers: Number of layers which corresponds to a shift of a complete lattice vector.\n                Defaults to `3`, the case for FCC and HCP.\n\n    Returns:\n     An [`AtomCell`][atomlib.atomcell.AtomCell] containing the stacked structure.\n    \"\"\"\n\n    layers = string.ascii_uppercase[:n_layers]\n\n    # TODO generalize this to arbitrary number of layers\n    sequence = sequence.upper()\n    if any(s not in layers for s in sequence):\n        raise ValueError(f\"Invalid sequence '{sequence}'. Expected values in '{layers}'\")\n\n    # c vector to shift along\n    c_vec = layer.to_ortho().transform_vec([0, 0, 1])\n    # new cell is original cell tiled by the number of layers\n    cell = layer.get_cell().repeat([1, 1, len(sequence)])\n\n    # convert shift_vector to local coordinates\n    shift_vector = layer.get_cell().get_transform('local', 'cell_frac').transform_vec(shift_vector)\n\n    atoms = layer.get_atoms('local')\n    return AtomCell(Atoms.concat(\n        # translate by the shift vector and translate to the correct layer\n        atoms.transform(AffineTransform3D.translate(shift_vector * (layers.find(c)) / n_layers + i*c_vec))\n        for (i, c) in enumerate(sequence)\n    ), cell).wrap()\n
    "},{"location":"api/mixins/","title":"atomlib.mixins","text":""},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin","title":"AtomsIOMixin","text":"

    Bases: _HasAtoms, ABC

    Mix-in to add IO methods to HasAtoms.

    All concrete subclasses of HasAtoms should also subclass this.

    Source code in atomlib/mixins.py
    class AtomsIOMixin(_HasAtoms, abc.ABC):\n    \"\"\"\n    Mix-in to add IO methods to [`HasAtoms`][atomlib.atoms.HasAtoms].\n\n    All concrete subclasses of [`HasAtoms`][atomlib.atoms.HasAtoms] should also subclass this.\n    \"\"\"\n\n    @t.overload\n    @classmethod\n    def read(cls: t.Type[HasAtomsT], path: FileOrPath, ty: FileType) -> HasAtomsT:\n        ...\n\n    @t.overload\n    @classmethod\n    def read(cls: t.Type[HasAtomsT], path: t.Union[str, Path, t.TextIO], ty: t.Literal[None] = None) -> HasAtomsT:\n        ...\n\n    @classmethod\n    def read(cls: t.Type[HasAtomsT], path: FileOrPath, ty: t.Optional[FileType] = None) -> HasAtomsT:\n        \"\"\"\n        Read a structure from a file.\n\n        Supported types can be found in the [io][atomlib.io] module.\n        If no `ty` is specified, it is inferred from the file's extension.\n        \"\"\"\n        from .io import read\n        return _cast_atoms(read(path, ty), cls)  # type: ignore\n\n    @classmethod\n    def read_cif(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CIF, CIFDataBlock], block: t.Union[int, str, None] = None) -> HasAtomsT:\n        \"\"\"\n        Read a structure from a CIF file.\n\n        If `block` is specified, read data from the given block of the CIF file (index or name).\n        \"\"\"\n        from .io import read_cif\n        return _cast_atoms(read_cif(f, block), cls)\n\n    @classmethod\n    def read_xyz(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XYZ]) -> HasAtomsT:\n        \"\"\"Read a structure from an XYZ file.\"\"\"\n        from .io import read_xyz\n        return _cast_atoms(read_xyz(f), cls)\n\n    @classmethod\n    def read_xsf(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XSF]) -> HasAtomsT:\n        \"\"\"Read a structure from an XSF file.\"\"\"\n        from .io import read_xsf\n        return _cast_atoms(read_xsf(f), cls)\n\n    @classmethod\n    def read_cfg(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CFG]) -> HasAtomsT:\n        \"\"\"Read a structure from a CFG file.\"\"\"\n        from .io import read_cfg\n        return _cast_atoms(read_cfg(f), cls)\n\n    @classmethod\n    def read_lmp(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, LMP], type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> HasAtomsT:\n        \"\"\"Read a structure from a LAAMPS data file.\"\"\"\n        from .io import read_lmp\n        return _cast_atoms(read_lmp(f, type_map=type_map), cls)\n\n    def write_cif(self, f: FileOrPath):\n        from .io import write_cif\n        write_cif(self, f)\n\n    def write_xyz(self, f: FileOrPath, fmt: XYZFormat = 'exyz'):\n        from .io import write_xyz\n        write_xyz(self, f, fmt)\n\n    def write_xsf(self, f: FileOrPath):\n        from .io import write_xsf\n        write_xsf(self, f)\n\n    def write_cfg(self, f: FileOrPath):\n        from .io import write_cfg\n        write_cfg(self, f)\n\n    def write_lmp(self, f: FileOrPath):\n        from .io import write_lmp\n        write_lmp(self, f)\n\n    @t.overload\n    def write(self, path: FileOrPath, ty: FileType):\n        ...\n\n    @t.overload\n    def write(self, path: t.Union[str, Path, t.TextIO], ty: t.Literal[None] = None):\n        ...\n\n    def write(self, path: FileOrPath, ty: t.Optional[FileType] = None):\n        \"\"\"\n        Write this structure to a file.\n\n        A file type may be specified using `ty`.\n        If no `ty` is specified, it is inferred from the path's extension.\n        \"\"\"\n        from .io import write\n        write(self, path, ty)  # type: ignore\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.read","title":"read classmethod","text":"
    read(\n    path: FileOrPath, ty: Optional[FileType] = None\n) -> HasAtomsT\n

    Read a structure from a file.

    Supported types can be found in the io module. If no ty is specified, it is inferred from the file's extension.

    Source code in atomlib/mixins.py
    @classmethod\ndef read(cls: t.Type[HasAtomsT], path: FileOrPath, ty: t.Optional[FileType] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a file.\n\n    Supported types can be found in the [io][atomlib.io] module.\n    If no `ty` is specified, it is inferred from the file's extension.\n    \"\"\"\n    from .io import read\n    return _cast_atoms(read(path, ty), cls)  # type: ignore\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.read_cif","title":"read_cif classmethod","text":"
    read_cif(\n    f: Union[FileOrPath, CIF, CIFDataBlock],\n    block: Union[int, str, None] = None,\n) -> HasAtomsT\n

    Read a structure from a CIF file.

    If block is specified, read data from the given block of the CIF file (index or name).

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cif(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CIF, CIFDataBlock], block: t.Union[int, str, None] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a CIF file.\n\n    If `block` is specified, read data from the given block of the CIF file (index or name).\n    \"\"\"\n    from .io import read_cif\n    return _cast_atoms(read_cif(f, block), cls)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.read_xyz","title":"read_xyz classmethod","text":"
    read_xyz(f: Union[FileOrPath, XYZ]) -> HasAtomsT\n

    Read a structure from an XYZ file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xyz(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XYZ]) -> HasAtomsT:\n    \"\"\"Read a structure from an XYZ file.\"\"\"\n    from .io import read_xyz\n    return _cast_atoms(read_xyz(f), cls)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.read_xsf","title":"read_xsf classmethod","text":"
    read_xsf(f: Union[FileOrPath, XSF]) -> HasAtomsT\n

    Read a structure from an XSF file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xsf(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XSF]) -> HasAtomsT:\n    \"\"\"Read a structure from an XSF file.\"\"\"\n    from .io import read_xsf\n    return _cast_atoms(read_xsf(f), cls)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.read_cfg","title":"read_cfg classmethod","text":"
    read_cfg(f: Union[FileOrPath, CFG]) -> HasAtomsT\n

    Read a structure from a CFG file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cfg(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CFG]) -> HasAtomsT:\n    \"\"\"Read a structure from a CFG file.\"\"\"\n    from .io import read_cfg\n    return _cast_atoms(read_cfg(f), cls)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.read_lmp","title":"read_lmp classmethod","text":"
    read_lmp(\n    f: Union[FileOrPath, LMP],\n    type_map: Optional[Dict[int, Union[str, int]]] = None,\n) -> HasAtomsT\n

    Read a structure from a LAAMPS data file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_lmp(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, LMP], type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> HasAtomsT:\n    \"\"\"Read a structure from a LAAMPS data file.\"\"\"\n    from .io import read_lmp\n    return _cast_atoms(read_lmp(f, type_map=type_map), cls)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.write_cif","title":"write_cif","text":"
    write_cif(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cif(self, f: FileOrPath):\n    from .io import write_cif\n    write_cif(self, f)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.write_xyz","title":"write_xyz","text":"
    write_xyz(f: FileOrPath, fmt: XYZFormat = 'exyz')\n
    Source code in atomlib/mixins.py
    def write_xyz(self, f: FileOrPath, fmt: XYZFormat = 'exyz'):\n    from .io import write_xyz\n    write_xyz(self, f, fmt)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.write_xsf","title":"write_xsf","text":"
    write_xsf(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_xsf(self, f: FileOrPath):\n    from .io import write_xsf\n    write_xsf(self, f)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.write_cfg","title":"write_cfg","text":"
    write_cfg(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cfg(self, f: FileOrPath):\n    from .io import write_cfg\n    write_cfg(self, f)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.write_lmp","title":"write_lmp","text":"
    write_lmp(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_lmp(self, f: FileOrPath):\n    from .io import write_lmp\n    write_lmp(self, f)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.write","title":"write","text":"
    write(path: FileOrPath, ty: Optional[FileType] = None)\n

    Write this structure to a file.

    A file type may be specified using ty. If no ty is specified, it is inferred from the path's extension.

    Source code in atomlib/mixins.py
    def write(self, path: FileOrPath, ty: t.Optional[FileType] = None):\n    \"\"\"\n    Write this structure to a file.\n\n    A file type may be specified using `ty`.\n    If no `ty` is specified, it is inferred from the path's extension.\n    \"\"\"\n    from .io import write\n    write(self, path, ty)  # type: ignore\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin","title":"AtomCellIOMixin","text":"

    Bases: _HasAtomCell, AtomsIOMixin

    Mix-in to add IO methods to HasAtomCell.

    All concrete subclasses of HasAtomCell should also subclass this.

    Source code in atomlib/mixins.py
    class AtomCellIOMixin(_HasAtomCell, AtomsIOMixin):\n    \"\"\"\n    Mix-in to add IO methods to [`HasAtomCell`][atomlib.atomcell.HasAtomCell].\n\n    All concrete subclasses of [`HasAtomCell`][atomlib.atomcell.HasAtomCell] should also subclass this.\n    \"\"\"\n\n    def write_mslice(self, f: BinaryFileOrPath, template: t.Optional[MSliceFile] = None, *,\n                 slice_thickness: t.Optional[float] = None,  # angstrom\n                 scan_points: t.Optional[ArrayLike] = None,\n                 scan_extent: t.Optional[ArrayLike] = None,\n                 noise_sigma: t.Optional[float] = None,  # angstrom\n                 conv_angle: t.Optional[float] = None,  # mrad\n                 energy: t.Optional[float] = None,  # keV\n                 defocus: t.Optional[float] = None,  # angstrom\n                 tilt: t.Optional[t.Tuple[float, float]] = None,  # (mrad, mrad)\n                 tds: t.Optional[bool] = None,\n                 n_cells: t.Optional[ArrayLike] = None):\n        \"\"\"\n        Write a structure to an mslice file.\n\n        `template` may be a file, path, or `ElementTree` containing an existing mslice file.\n        Its structure will be modified to make the final output. If not specified, a default\n        template will be used.\n\n        Additional options modify simulation properties. If an option is not specified, the\n        template's properties are used.\n        \"\"\"\n        from .io import write_mslice\n        return write_mslice(self, f, template, slice_thickness=slice_thickness,\n                            scan_points=scan_points, scan_extent=scan_extent,\n                            conv_angle=conv_angle, energy=energy, defocus=defocus,\n                            noise_sigma=noise_sigma, tilt=tilt, tds=tds, n_cells=n_cells)\n\n    def write_qe(self, f: FileOrPath, pseudo: t.Optional[t.Mapping[str, str]] = None):\n        \"\"\"\n        Write a structure to a Quantum Espresso pw.x file.\n\n        Args:\n          f: File or path to write to\n          pseudo: Mapping from atom symbol\n        \"\"\"\n        from .io import write_qe\n        write_qe(self, f, pseudo)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.read","title":"read classmethod","text":"
    read(\n    path: FileOrPath, ty: Optional[FileType] = None\n) -> HasAtomsT\n

    Read a structure from a file.

    Supported types can be found in the io module. If no ty is specified, it is inferred from the file's extension.

    Source code in atomlib/mixins.py
    @classmethod\ndef read(cls: t.Type[HasAtomsT], path: FileOrPath, ty: t.Optional[FileType] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a file.\n\n    Supported types can be found in the [io][atomlib.io] module.\n    If no `ty` is specified, it is inferred from the file's extension.\n    \"\"\"\n    from .io import read\n    return _cast_atoms(read(path, ty), cls)  # type: ignore\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.read_cif","title":"read_cif classmethod","text":"
    read_cif(\n    f: Union[FileOrPath, CIF, CIFDataBlock],\n    block: Union[int, str, None] = None,\n) -> HasAtomsT\n

    Read a structure from a CIF file.

    If block is specified, read data from the given block of the CIF file (index or name).

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cif(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CIF, CIFDataBlock], block: t.Union[int, str, None] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a CIF file.\n\n    If `block` is specified, read data from the given block of the CIF file (index or name).\n    \"\"\"\n    from .io import read_cif\n    return _cast_atoms(read_cif(f, block), cls)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.read_xyz","title":"read_xyz classmethod","text":"
    read_xyz(f: Union[FileOrPath, XYZ]) -> HasAtomsT\n

    Read a structure from an XYZ file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xyz(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XYZ]) -> HasAtomsT:\n    \"\"\"Read a structure from an XYZ file.\"\"\"\n    from .io import read_xyz\n    return _cast_atoms(read_xyz(f), cls)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.read_xsf","title":"read_xsf classmethod","text":"
    read_xsf(f: Union[FileOrPath, XSF]) -> HasAtomsT\n

    Read a structure from an XSF file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xsf(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XSF]) -> HasAtomsT:\n    \"\"\"Read a structure from an XSF file.\"\"\"\n    from .io import read_xsf\n    return _cast_atoms(read_xsf(f), cls)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.read_cfg","title":"read_cfg classmethod","text":"
    read_cfg(f: Union[FileOrPath, CFG]) -> HasAtomsT\n

    Read a structure from a CFG file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cfg(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CFG]) -> HasAtomsT:\n    \"\"\"Read a structure from a CFG file.\"\"\"\n    from .io import read_cfg\n    return _cast_atoms(read_cfg(f), cls)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.read_lmp","title":"read_lmp classmethod","text":"
    read_lmp(\n    f: Union[FileOrPath, LMP],\n    type_map: Optional[Dict[int, Union[str, int]]] = None,\n) -> HasAtomsT\n

    Read a structure from a LAAMPS data file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_lmp(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, LMP], type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> HasAtomsT:\n    \"\"\"Read a structure from a LAAMPS data file.\"\"\"\n    from .io import read_lmp\n    return _cast_atoms(read_lmp(f, type_map=type_map), cls)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.write_cif","title":"write_cif","text":"
    write_cif(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cif(self, f: FileOrPath):\n    from .io import write_cif\n    write_cif(self, f)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.write_xyz","title":"write_xyz","text":"
    write_xyz(f: FileOrPath, fmt: XYZFormat = 'exyz')\n
    Source code in atomlib/mixins.py
    def write_xyz(self, f: FileOrPath, fmt: XYZFormat = 'exyz'):\n    from .io import write_xyz\n    write_xyz(self, f, fmt)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.write_xsf","title":"write_xsf","text":"
    write_xsf(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_xsf(self, f: FileOrPath):\n    from .io import write_xsf\n    write_xsf(self, f)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.write_cfg","title":"write_cfg","text":"
    write_cfg(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cfg(self, f: FileOrPath):\n    from .io import write_cfg\n    write_cfg(self, f)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.write_lmp","title":"write_lmp","text":"
    write_lmp(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_lmp(self, f: FileOrPath):\n    from .io import write_lmp\n    write_lmp(self, f)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.write","title":"write","text":"
    write(path: FileOrPath, ty: Optional[FileType] = None)\n

    Write this structure to a file.

    A file type may be specified using ty. If no ty is specified, it is inferred from the path's extension.

    Source code in atomlib/mixins.py
    def write(self, path: FileOrPath, ty: t.Optional[FileType] = None):\n    \"\"\"\n    Write this structure to a file.\n\n    A file type may be specified using `ty`.\n    If no `ty` is specified, it is inferred from the path's extension.\n    \"\"\"\n    from .io import write\n    write(self, path, ty)  # type: ignore\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.write_mslice","title":"write_mslice","text":"
    write_mslice(\n    f: BinaryFileOrPath,\n    template: Optional[MSliceFile] = None,\n    *,\n    slice_thickness: Optional[float] = None,\n    scan_points: Optional[ArrayLike] = None,\n    scan_extent: Optional[ArrayLike] = None,\n    noise_sigma: Optional[float] = None,\n    conv_angle: Optional[float] = None,\n    energy: Optional[float] = None,\n    defocus: Optional[float] = None,\n    tilt: Optional[Tuple[float, float]] = None,\n    tds: Optional[bool] = None,\n    n_cells: Optional[ArrayLike] = None\n)\n

    Write a structure to an mslice file.

    template may be a file, path, or ElementTree containing an existing mslice file. Its structure will be modified to make the final output. If not specified, a default template will be used.

    Additional options modify simulation properties. If an option is not specified, the template's properties are used.

    Source code in atomlib/mixins.py
    def write_mslice(self, f: BinaryFileOrPath, template: t.Optional[MSliceFile] = None, *,\n             slice_thickness: t.Optional[float] = None,  # angstrom\n             scan_points: t.Optional[ArrayLike] = None,\n             scan_extent: t.Optional[ArrayLike] = None,\n             noise_sigma: t.Optional[float] = None,  # angstrom\n             conv_angle: t.Optional[float] = None,  # mrad\n             energy: t.Optional[float] = None,  # keV\n             defocus: t.Optional[float] = None,  # angstrom\n             tilt: t.Optional[t.Tuple[float, float]] = None,  # (mrad, mrad)\n             tds: t.Optional[bool] = None,\n             n_cells: t.Optional[ArrayLike] = None):\n    \"\"\"\n    Write a structure to an mslice file.\n\n    `template` may be a file, path, or `ElementTree` containing an existing mslice file.\n    Its structure will be modified to make the final output. If not specified, a default\n    template will be used.\n\n    Additional options modify simulation properties. If an option is not specified, the\n    template's properties are used.\n    \"\"\"\n    from .io import write_mslice\n    return write_mslice(self, f, template, slice_thickness=slice_thickness,\n                        scan_points=scan_points, scan_extent=scan_extent,\n                        conv_angle=conv_angle, energy=energy, defocus=defocus,\n                        noise_sigma=noise_sigma, tilt=tilt, tds=tds, n_cells=n_cells)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.write_qe","title":"write_qe","text":"
    write_qe(\n    f: FileOrPath,\n    pseudo: Optional[Mapping[str, str]] = None,\n)\n

    Write a structure to a Quantum Espresso pw.x file.

    PARAMETER DESCRIPTION f

    File or path to write to

    TYPE: FileOrPath

    pseudo

    Mapping from atom symbol

    TYPE: Optional[Mapping[str, str]] DEFAULT: None

    Source code in atomlib/mixins.py
    def write_qe(self, f: FileOrPath, pseudo: t.Optional[t.Mapping[str, str]] = None):\n    \"\"\"\n    Write a structure to a Quantum Espresso pw.x file.\n\n    Args:\n      f: File or path to write to\n      pseudo: Mapping from atom symbol\n    \"\"\"\n    from .io import write_qe\n    write_qe(self, f, pseudo)\n
    "},{"location":"api/testing/","title":"atomlib.testing","text":""},{"location":"api/testing/#atomlib.testing.CallableT","title":"CallableT module-attribute","text":"
    CallableT = TypeVar('CallableT', bound=Callable)\n
    "},{"location":"api/testing/#atomlib.testing.OUTPUT_PATH","title":"OUTPUT_PATH module-attribute","text":"
    OUTPUT_PATH = parents[2] / 'tests/baseline_files'\n
    "},{"location":"api/testing/#atomlib.testing.INPUT_PATH","title":"INPUT_PATH module-attribute","text":"
    INPUT_PATH = parents[2] / 'tests/input_files'\n
    "},{"location":"api/testing/#atomlib.testing.assert_files_equal","title":"assert_files_equal","text":"
    assert_files_equal(\n    expected_path: Union[str, Path],\n    actual_path: Union[str, Path],\n)\n
    Source code in atomlib/testing/__init__.py
    def assert_files_equal(expected_path: t.Union[str, Path], actual_path: t.Union[str, Path]):\n    with open(OUTPUT_PATH / expected_path, 'r') as f:\n        expected = re.sub('\\r\\n', '\\n', f.read())\n    with open(actual_path, 'r') as f:\n        actual = re.sub('\\r\\n', '\\n', f.read())\n\n    assert expected == actual\n
    "},{"location":"api/testing/#atomlib.testing.check_equals_file","title":"check_equals_file","text":"
    check_equals_file(\n    name: Union[str, Path], *, skip_lines: int = 0\n) -> Callable[[Callable[..., Any]], Callable[..., None]]\n
    Source code in atomlib/testing/__init__.py
    def check_equals_file(name: t.Union[str, Path], *, skip_lines: int = 0) -> t.Callable[[t.Callable[..., t.Any]], t.Callable[..., None]]:\n    def decorator(f: t.Callable[..., str]):\n        @pytest.mark.expected_filename(name)\n        def wrapper(expected_contents_text: str, *args, **kwargs):  # type: ignore\n            buf = StringIO()\n            f(buf, *args, **kwargs)\n            if skip_lines > 0:\n                lhs = \"\".join(buf.getvalue().splitlines(True)[skip_lines:])\n                rhs = \"\".join(expected_contents_text.splitlines(True)[skip_lines:])\n                assert lhs == rhs\n            else:\n                assert buf.getvalue() == expected_contents_text\n\n        return _wrap_pytest(wrapper, f, lambda params: [inspect.Parameter('expected_contents_text', inspect.Parameter.POSITIONAL_OR_KEYWORD), *params[1:]])\n\n    return decorator\n
    "},{"location":"api/testing/#atomlib.testing.check_equals_binary_file","title":"check_equals_binary_file","text":"
    check_equals_binary_file(\n    name: Union[str, Path]\n) -> Callable[[Callable[..., Any]], Callable[..., None]]\n
    Source code in atomlib/testing/__init__.py
    def check_equals_binary_file(name: t.Union[str, Path]) -> t.Callable[[t.Callable[..., t.Any]], t.Callable[..., None]]:\n    def decorator(f: t.Callable[..., str]):\n        @pytest.mark.expected_filename(name)\n        def wrapper(expected_contents_binary: bytes, *args, **kwargs):  # type: ignore\n            buf = BytesIO()\n            f(buf, *args, **kwargs)\n            assert buf.getvalue() == expected_contents_binary\n\n        return _wrap_pytest(wrapper, f, lambda params: [inspect.Parameter('expected_contents_binary', inspect.Parameter.POSITIONAL_OR_KEYWORD), *params[1:]])\n\n    return decorator\n
    "},{"location":"api/testing/#atomlib.testing.assert_structure_equal","title":"assert_structure_equal","text":"
    assert_structure_equal(\n    expected_path: Union[str, Path],\n    actual: Union[str, Path, AtomsIOMixin],\n)\n
    Source code in atomlib/testing/__init__.py
    def assert_structure_equal(expected_path: t.Union[str, Path], actual: t.Union[str, Path, AtomsIOMixin]):\n    from atomlib.io import read\n\n    expected = read(OUTPUT_PATH / expected_path)\n\n    try:\n        if isinstance(actual, (str, Path)):\n            actual = t.cast('AtomsIOMixin', read(actual))\n    except Exception:\n        print(\"Failed to load structure under test.\")\n        raise\n\n    try:\n        if hasattr(actual, 'assert_equal'):\n            actual.assert_equal(expected)  # type: ignore\n        else:\n            assert actual == expected\n    except AssertionError:\n        try:\n            actual_path = Path(expected_path).with_stem(Path(expected_path).stem + '_actual').name\n            print(f\"Saving result structure to '{actual_path}'\")\n            actual.write(OUTPUT_PATH / actual_path)\n        except Exception:\n            print(\"Failed to save result structure.\")\n        raise\n
    "},{"location":"api/testing/#atomlib.testing.check_equals_structure","title":"check_equals_structure","text":"
    check_equals_structure(\n    name: Union[str, Path]\n) -> Callable[\n    [Callable[..., AtomsIOMixin]], Callable[..., None]\n]\n

    Test that the wrapped function returns the same structure as contained in name.

    Source code in atomlib/testing/__init__.py
    def check_equals_structure(name: t.Union[str, Path]) -> t.Callable[[t.Callable[..., AtomsIOMixin]], t.Callable[..., None]]:\n    \"\"\"Test that the wrapped function returns the same structure as contained in `name`.\"\"\"\n    def decorator(f: t.Callable[..., 'AtomsIOMixin']):\n        @pytest.mark.expected_filename(name)\n        def wrapper(expected_structure: 'HasAtoms', *args, **kwargs):  # type: ignore\n            result = f(*args, **kwargs)\n            try:\n                if hasattr(result, 'assert_equal'):\n                    result.assert_equal(expected_structure)  # type: ignore\n                else:\n                    assert result == expected_structure\n            except AssertionError:\n                try:\n                    actual_path = Path(name).with_stem(Path(name).stem + '_actual').name\n                    print(f\"Saving result structure to '{actual_path}'\")\n                    result.write(OUTPUT_PATH / actual_path)\n                except Exception:\n                    print(\"Failed to save result structure.\")\n                raise\n\n        return _wrap_pytest(wrapper, f, lambda params: [inspect.Parameter('expected_structure', inspect.Parameter.POSITIONAL_OR_KEYWORD), *params])\n\n    return decorator\n
    "},{"location":"api/testing/#atomlib.testing.check_parse_structure","title":"check_parse_structure","text":"
    check_parse_structure(\n    name: Union[str, Path]\n) -> Callable[\n    [Callable[..., HasAtoms]], Callable[..., None]\n]\n

    Test that name parses to the same structure as given in the function body.

    Source code in atomlib/testing/__init__.py
    def check_parse_structure(name: t.Union[str, Path]) -> t.Callable[[t.Callable[..., HasAtoms]], t.Callable[..., None]]:\n    \"\"\"Test that `name` parses to the same structure as given in the function body.\"\"\"\n    def decorator(f: t.Callable[..., 'HasAtoms']):\n        def wrapper(*args, **kwargs):  # type: ignore\n            expected = f(*args, **kwargs)\n\n            from atomlib.io import read\n            result = read(INPUT_PATH / name)\n\n            if hasattr(result, 'assert_equal'):\n                result.assert_equal(expected)  # type: ignore\n            else:\n                assert result == expected\n\n        return _wrap_pytest(wrapper, f)\n    return decorator\n
    "},{"location":"api/testing/#atomlib.testing.check_figure_draw","title":"check_figure_draw","text":"
    check_figure_draw(\n    name: Union[str, Path, Sequence[Union[str, Path]]],\n    savefig_kwarg: Optional[Dict[str, Any]] = None,\n) -> Callable[[Callable[..., None]], Callable[..., None]]\n

    Test that the wrapped function draws an identical figure to name in baseline_images.

    Source code in atomlib/testing/__init__.py
    def check_figure_draw(name: t.Union[str, Path, t.Sequence[t.Union[str, Path]]],\n                      savefig_kwarg: t.Optional[t.Dict[str, t.Any]] = None) -> t.Callable[[t.Callable[..., None]], t.Callable[..., None]]:\n    \"\"\"Test that the wrapped function draws an identical figure to `name` in `baseline_images`.\"\"\"\n\n    if isinstance(name, (str, Path)):\n        names = (str(name),)\n    else:\n        names = tuple(map(str, name))\n\n    def decorator(f: t.Callable[..., None]):\n        from matplotlib.testing.decorators import image_comparison\n        return image_comparison(list(names), savefig_kwarg=savefig_kwarg)(f)\n\n    return decorator\n
    "},{"location":"api/transform/","title":"atomlib.transform","text":""},{"location":"api/transform/#atomlib.transform.IntoTransform3D","title":"IntoTransform3D module-attribute","text":"
    IntoTransform3D: TypeAlias = Union[\n    \"Transform3D\",\n    Callable[[NDArray[floating]], ndarray],\n    ndarray,\n]\n

    Type which is coercable into a Transform3D.

    Includes transformations, numpy arrays (3x3 or 4x4), and functions (which should take a Nx3 ndarray and return an ndarray of the same shape).

    "},{"location":"api/transform/#atomlib.transform.Transform3D","title":"Transform3D","text":"

    Bases: ABC

    Arbitrary 3D transformation. Superclass of all 3D transformation types.

    Transformations can be composed: t3 = t1 @ t2, or applied to points: transformed = transform @ points using the @ operator.

    Alternatively, points can be transformed using functional notation: transformed = transform(points).

    Source code in atomlib/transform.py
    class Transform3D(ABC):\n    \"\"\"\n    Arbitrary 3D transformation. Superclass of all 3D transformation types.\n\n    Transformations can be composed: `t3 = t1 @ t2`, or applied\n    to points: `transformed = transform @ points` using the `@`\n    operator.\n\n    Alternatively, points can be transformed using functional notation:\n    `transformed = transform(points)`.\n    \"\"\"\n\n    @staticmethod\n    @abstractmethod\n    def identity() -> Transform3D:\n        \"\"\"Return an identity transformation.\"\"\"\n        ...\n\n    @staticmethod\n    def make(data: IntoTransform3D) -> Transform3D:\n        \"\"\"Make a transformation from a function or numpy array (3x3 or 4x4).\"\"\"\n        if isinstance(data, Transform3D):\n            return data\n        if not isinstance(data, numpy.ndarray) and hasattr(data, '__call__'):\n            return FuncTransform3D(data)\n        data = numpy.array(data)\n        if data.shape == (3, 3):\n            return LinearTransform3D(data)\n        if data.shape == (4, 4):\n            return AffineTransform3D(data)\n        raise ValueError(f\"Transform3D of invalid shape {data.shape}\")\n\n    @abstractmethod\n    def compose(self, other: Transform3D) -> Transform3D:\n        \"\"\"Compose this transformation with another.\"\"\"\n        ...\n\n    @t.overload\n    @abstractmethod\n    def transform(self, points: BBox3D) -> BBox3D:\n        ...\n\n    @t.overload\n    @abstractmethod\n    def transform(self, points: ArrayLike) -> NDArray[numpy.floating]:\n        ...\n\n    @abstractmethod\n    def transform(self, points: Pts3DLike) -> t.Union[BBox3D, NDArray[numpy.floating]]:\n        \"\"\"Transform points according to the given transformation.\"\"\"\n        ...\n\n    __call__ = transform\n\n    def transform_vec(self, vecs: ArrayLike) -> NDArray[numpy.floating]:\n        \"\"\"Transform vector quantities. This excludes translation, as would be expected when transforming vectors.\"\"\"\n        a = numpy.atleast_1d(vecs)\n        return self.transform(a) - self.transform(numpy.zeros_like(a))\n\n    @t.overload\n    def __matmul__(self, other: Transform3D) -> Transform3D:\n        ...\n\n    @t.overload\n    def __matmul__(self, other: BBox3D) -> BBox3D:\n        ...\n\n    @t.overload\n    def __matmul__(self, other: ArrayLike) -> NDArray[numpy.floating]:\n        ...\n\n    def __matmul__(self, other: t.Union[Transform3D, Pts3DLike]) -> t.Union[Transform3D, BBox3D, NDArray[numpy.floating]]:\n        \"\"\"Compose this transformation, or apply it to a given set of points.\"\"\"\n        if isinstance(other, Transform3D):\n            return other.compose(self)\n        return self.transform(other)\n\n    def __rmatmul__(self, other: t.Any):\n        raise ValueError(\"Transform must be applied to points, not the other way around.\")\n
    "},{"location":"api/transform/#atomlib.transform.Transform3D.identity","title":"identity abstractmethod staticmethod","text":"
    identity() -> Transform3D\n

    Return an identity transformation.

    Source code in atomlib/transform.py
    @staticmethod\n@abstractmethod\ndef identity() -> Transform3D:\n    \"\"\"Return an identity transformation.\"\"\"\n    ...\n
    "},{"location":"api/transform/#atomlib.transform.Transform3D.make","title":"make staticmethod","text":"
    make(data: IntoTransform3D) -> Transform3D\n

    Make a transformation from a function or numpy array (3x3 or 4x4).

    Source code in atomlib/transform.py
    @staticmethod\ndef make(data: IntoTransform3D) -> Transform3D:\n    \"\"\"Make a transformation from a function or numpy array (3x3 or 4x4).\"\"\"\n    if isinstance(data, Transform3D):\n        return data\n    if not isinstance(data, numpy.ndarray) and hasattr(data, '__call__'):\n        return FuncTransform3D(data)\n    data = numpy.array(data)\n    if data.shape == (3, 3):\n        return LinearTransform3D(data)\n    if data.shape == (4, 4):\n        return AffineTransform3D(data)\n    raise ValueError(f\"Transform3D of invalid shape {data.shape}\")\n
    "},{"location":"api/transform/#atomlib.transform.Transform3D.compose","title":"compose abstractmethod","text":"
    compose(other: Transform3D) -> Transform3D\n

    Compose this transformation with another.

    Source code in atomlib/transform.py
    @abstractmethod\ndef compose(self, other: Transform3D) -> Transform3D:\n    \"\"\"Compose this transformation with another.\"\"\"\n    ...\n
    "},{"location":"api/transform/#atomlib.transform.Transform3D.transform","title":"transform abstractmethod","text":"
    transform(\n    points: Pts3DLike,\n) -> Union[BBox3D, NDArray[floating]]\n

    Transform points according to the given transformation.

    Source code in atomlib/transform.py
    @abstractmethod\ndef transform(self, points: Pts3DLike) -> t.Union[BBox3D, NDArray[numpy.floating]]:\n    \"\"\"Transform points according to the given transformation.\"\"\"\n    ...\n
    "},{"location":"api/transform/#atomlib.transform.Transform3D.transform_vec","title":"transform_vec","text":"
    transform_vec(vecs: ArrayLike) -> NDArray[floating]\n

    Transform vector quantities. This excludes translation, as would be expected when transforming vectors.

    Source code in atomlib/transform.py
    def transform_vec(self, vecs: ArrayLike) -> NDArray[numpy.floating]:\n    \"\"\"Transform vector quantities. This excludes translation, as would be expected when transforming vectors.\"\"\"\n    a = numpy.atleast_1d(vecs)\n    return self.transform(a) - self.transform(numpy.zeros_like(a))\n
    "},{"location":"api/transform/#atomlib.transform.FuncTransform3D","title":"FuncTransform3D","text":"

    Bases: Transform3D

    Transformation which applies a function to the given points.

    Source code in atomlib/transform.py
    class FuncTransform3D(Transform3D):\n    \"\"\"Transformation which applies a function to the given points.\"\"\"\n\n    def __init__(self, f: t.Callable[[numpy.ndarray], numpy.ndarray]):\n        self.f: t.Callable[[numpy.ndarray], numpy.ndarray] = f\n        \"\"\"Wrapped function\"\"\"\n\n    @staticmethod\n    def identity() -> FuncTransform3D:\n        \"\"\"Return an identity transformation.\"\"\"\n        return FuncTransform3D(lambda pts: pts)\n\n    @t.overload\n    def transform(self, points: BBox3D) -> BBox3D:\n        ...\n\n    @t.overload\n    def transform(self, points: ArrayLike) -> NDArray[numpy.floating]:\n        ...\n\n    def transform(self, points: Pts3DLike) -> t.Union[BBox3D, NDArray[numpy.floating]]:\n        \"\"\"Transform points according to the given transformation.\"\"\"\n        if isinstance(points, BBox3D):\n            return points.from_pts(self.transform(points.corners()))\n\n        return self.f(numpy.atleast_1d(points))\n\n    def compose(self, other: Transform3D) -> FuncTransform3D:\n        \"\"\"Compose this transformation with another.\"\"\"\n        return FuncTransform3D(lambda pts: other.transform(self.f(pts)))\n\n    def _rcompose(self, after: Transform3D) -> FuncTransform3D:\n        return FuncTransform3D(lambda pts: self.f(after.transform(pts)))\n\n    __call__ = transform\n
    "},{"location":"api/transform/#atomlib.transform.FuncTransform3D.f","title":"f instance-attribute","text":"
    f: Callable[[ndarray], ndarray] = f\n

    Wrapped function

    "},{"location":"api/transform/#atomlib.transform.FuncTransform3D.make","title":"make staticmethod","text":"
    make(data: IntoTransform3D) -> Transform3D\n

    Make a transformation from a function or numpy array (3x3 or 4x4).

    Source code in atomlib/transform.py
    @staticmethod\ndef make(data: IntoTransform3D) -> Transform3D:\n    \"\"\"Make a transformation from a function or numpy array (3x3 or 4x4).\"\"\"\n    if isinstance(data, Transform3D):\n        return data\n    if not isinstance(data, numpy.ndarray) and hasattr(data, '__call__'):\n        return FuncTransform3D(data)\n    data = numpy.array(data)\n    if data.shape == (3, 3):\n        return LinearTransform3D(data)\n    if data.shape == (4, 4):\n        return AffineTransform3D(data)\n    raise ValueError(f\"Transform3D of invalid shape {data.shape}\")\n
    "},{"location":"api/transform/#atomlib.transform.FuncTransform3D.transform_vec","title":"transform_vec","text":"
    transform_vec(vecs: ArrayLike) -> NDArray[floating]\n

    Transform vector quantities. This excludes translation, as would be expected when transforming vectors.

    Source code in atomlib/transform.py
    def transform_vec(self, vecs: ArrayLike) -> NDArray[numpy.floating]:\n    \"\"\"Transform vector quantities. This excludes translation, as would be expected when transforming vectors.\"\"\"\n    a = numpy.atleast_1d(vecs)\n    return self.transform(a) - self.transform(numpy.zeros_like(a))\n
    "},{"location":"api/transform/#atomlib.transform.FuncTransform3D.identity","title":"identity staticmethod","text":"
    identity() -> FuncTransform3D\n

    Return an identity transformation.

    Source code in atomlib/transform.py
    @staticmethod\ndef identity() -> FuncTransform3D:\n    \"\"\"Return an identity transformation.\"\"\"\n    return FuncTransform3D(lambda pts: pts)\n
    "},{"location":"api/transform/#atomlib.transform.FuncTransform3D.transform","title":"transform","text":"
    transform(\n    points: Pts3DLike,\n) -> Union[BBox3D, NDArray[floating]]\n

    Transform points according to the given transformation.

    Source code in atomlib/transform.py
    def transform(self, points: Pts3DLike) -> t.Union[BBox3D, NDArray[numpy.floating]]:\n    \"\"\"Transform points according to the given transformation.\"\"\"\n    if isinstance(points, BBox3D):\n        return points.from_pts(self.transform(points.corners()))\n\n    return self.f(numpy.atleast_1d(points))\n
    "},{"location":"api/transform/#atomlib.transform.FuncTransform3D.compose","title":"compose","text":"
    compose(other: Transform3D) -> FuncTransform3D\n

    Compose this transformation with another.

    Source code in atomlib/transform.py
    def compose(self, other: Transform3D) -> FuncTransform3D:\n    \"\"\"Compose this transformation with another.\"\"\"\n    return FuncTransform3D(lambda pts: other.transform(self.f(pts)))\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D","title":"AffineTransform3D","text":"

    Bases: Transform3D

    Source code in atomlib/transform.py
    class AffineTransform3D(Transform3D):\n    __array_ufunc__ = None\n\n    def __init__(self, array: t.Optional[ArrayLike] = None):\n        if array is None:\n            array = numpy.eye(4)\n        self.inner = numpy.broadcast_to(array, (4, 4))\n\n    @property\n    def __array_interface__(self):\n        return self.inner.__array_interface__\n\n    def __repr__(self) -> str:\n        return f\"AffineTransform3D(\\n{self.inner!r}\\n)\"\n\n    @staticmethod\n    def identity() -> AffineTransform3D:\n        \"\"\"Return an identity transformation.\"\"\"\n        return AffineTransform3D()\n\n    def round_near_zero(self: Affine3DSelf) -> Affine3DSelf:\n        \"\"\"Round near-zero matrix elements in self.\"\"\"\n        return type(self)(\n            numpy.where(numpy.abs(self.inner) < 1e-15, 0., self.inner)\n        )\n\n    @staticmethod\n    def from_linear(linear: LinearTransform3D) -> AffineTransform3D:\n        \"\"\"Make an affine transformation from a linear transformation.\"\"\"\n        dtype = linear.inner.dtype\n        return AffineTransform3D(numpy.block([\n            [linear.inner, numpy.zeros((3, 1), dtype=dtype)],\n            [numpy.zeros((1, 3), dtype=dtype), numpy.ones((), dtype=dtype)]\n        ]))  # type: ignore\n\n    def to_linear(self) -> LinearTransform3D:\n        \"\"\"Return the linear part of an affine transformation.\"\"\"\n        return LinearTransform3D(self.inner[:3, :3])\n\n    def to_translation(self) -> AffineTransform3D:\n        \"\"\"Extract the translation component of `self`, and return it.\"\"\"\n        return AffineTransform3D.translate(self.translation())\n\n    def det(self) -> float:\n        \"\"\"Return the determinant of an affine transformation.\"\"\"\n        return numpy.linalg.det(self.inner[:3, :3])\n\n    def translation(self) -> numpy.ndarray:\n        \"\"\"Extract the translation component of `self`, and return it as a vector.\"\"\"\n        return self.inner[:3, -1]\n\n    def inverse(self) -> AffineTransform3D:\n        \"\"\"Return the inverse of an affine transformation.\"\"\"\n        linear_inv = LinearTransform3D(self.inner[:3, :3]).inverse()\n        # first undo translation, then undo linear transformation\n        return linear_inv @ AffineTransform3D.translate(*-self.translation())\n\n    @t.overload\n    @classmethod\n    def translate(cls, x: VecLike, /) -> AffineTransform3D:\n        ...\n\n    @t.overload\n    @classmethod\n    def translate(cls, x: Num = 0., y: Num = 0., z: Num = 0.) -> AffineTransform3D:\n        ...\n\n    @opt_classmethod\n    def translate(self, x: t.Union[Num, VecLike] = 0., y: Num = 0., z: Num = 0.) -> AffineTransform3D:\n        \"\"\"\n        Create or append an affine translation.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        if isinstance(x, t.Sized) and len(x) > 1:\n            try:\n                (x, y, z) = to_vec3(x)\n            except ValueError:\n                raise ValueError(\"translate() must be called with a sequence or three numbers.\")\n\n        if isinstance(self, LinearTransform3D):\n            self = AffineTransform3D.from_linear(self)\n\n        a = self.inner.copy()\n        a[:3, -1] += [x, y, z]\n        return AffineTransform3D(a)\n\n    @t.overload\n    @classmethod\n    def scale(cls, x: VecLike, /) -> AffineTransform3D:\n        ...\n\n    @t.overload\n    @classmethod\n    def scale(cls, x: Num = 1., y: Num = 1., z: Num = 1., *,\n              all: Num = 1.) -> AffineTransform3D:\n        ...\n\n    @opt_classmethod\n    def scale(self, x: t.Union[Num, VecLike] = 1., y: Num = 1., z: Num = 1., *,\n              all: Num = 1.) -> AffineTransform3D:\n        \"\"\"\n        Create or append a scaling transformation.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        return self.compose(LinearTransform3D.scale(x, y, z, all=all))  # type: ignore\n\n    @opt_classmethod\n    def rotate(self, v: VecLike, theta: Num) -> AffineTransform3D:\n        \"\"\"\n        Create or append a rotation transformation of `theta`\n        radians CCW around the given vector `v`.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        return self.compose(LinearTransform3D.rotate(v, theta))\n\n    @opt_classmethod\n    def rotate_euler(self, x: Num = 0., y: Num = 0., z: Num = 0.) -> AffineTransform3D:\n        \"\"\"\n        Create or append a Euler rotation transformation.\n        Rotation is performed on the x axis first, then y axis and z axis.\n        Values are specified in radians.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        return self.compose(LinearTransform3D.rotate_euler(x, y, z))\n\n    @t.overload\n    @classmethod\n    def mirror(cls, a: VecLike, /) -> AffineTransform3D:\n        ...\n\n    @t.overload\n    @classmethod\n    def mirror(cls, a: Num, b: Num, c: Num) -> AffineTransform3D:\n        ...\n\n    @opt_classmethod\n    def mirror(self, a: t.Union[Num, VecLike],\n               b: t.Optional[Num] = None,\n               c: t.Optional[Num] = None) -> AffineTransform3D:\n        \"\"\"\n        Create or append a mirror transformation across the given plane.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        return self.compose(LinearTransform3D.mirror(a, b, c))  # type: ignore\n\n    @opt_classmethod\n    def strain(self, strain: float, v: VecLike = (0, 0, 1), poisson: float = 0.) -> AffineTransform3D:\n        \"\"\"\n        Apply a strain of ``strain`` in direction ``v``, assuming an elastically isotropic material.\n\n        Strain is applied relative to the origin.\n\n        With ``poisson=0`` (default), a uniaxial strain is applied.\n        With ``poisson=-1``, hydrostatic strain is applied.\n        Otherwise, a uniaxial stress is applied for a material with Poisson ratio `poisson`,\n        which results in shrinkage perpendicular to the direction strain is applied.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        return self.compose(LinearTransform3D.strain(strain, v, poisson))\n\n    def align_standard(self) -> AffineTransform3D:\n        \"\"\"\n        Align `self` so `v1` is in the x-axis and `v2` is in the xy-plane.\n\n        This is equivalent to a $QR$ decomposition which keeps only the\n        right-triangular matrix $R$.\n\n        For an affine transformation, this rotates the transformation\n        around the global origin (including any transformation).\n        \"\"\"\n        if self.det() <= 0:\n            raise ValueError(\"align_standard() requires a right-handed transformation\")\n\n        translation = self.translation()\n\n        import scipy.linalg\n        q, r = t.cast(t.Tuple[numpy.ndarray, numpy.ndarray], scipy.linalg.qr(self.to_linear().inner))\n        # qr unique up to the sign of the digonal\n        # we choose the case where r is positive definite\n        q = q * numpy.sign(r.diagonal())\n        r = r * numpy.sign(r.diagonal())[:, None]\n        assert numpy.linalg.det(r) > 0\n        # we need to remove the rotation from the translation component as well\n        # q is orthogonal, so q^-1 = q.T\n        return LinearTransform3D(r).translate(q.T @ translation).round_near_zero()\n\n    @t.overload\n    def transform(self, points: BBox3D) -> BBox3D:\n        ...\n\n    @t.overload\n    def transform(self, points: ArrayLike) -> NDArray[numpy.floating]:\n        ...\n\n    def transform(self, points: Pts3DLike) -> t.Union[BBox3D, NDArray[numpy.floating]]:\n        \"\"\"Transform points according to the given transformation.\"\"\"\n        if isinstance(points, BBox3D):\n            return points.from_pts(self.transform(points.corners()))\n\n        pts: NDArray[numpy.floating] = numpy.atleast_1d(points).astype(self.inner.dtype)\n        pts = numpy.concatenate((points, numpy.broadcast_to(1., (*pts.shape[:-1], 1))), axis=-1)\n\n        # carefully handle inf and nan\n        isnan = numpy.bitwise_or.reduce(numpy.isnan(pts), axis=-1)\n\n        with numpy.errstate(invalid='ignore'):\n            out = pts @ self.inner.T\n            isinf = numpy.isnan(out[..., 0]) & ~isnan\n\n            prod = numpy.multiply(self.inner, pts[isinf, None, :])\n\n        # inf * 0 = 0\n        prod[numpy.isnan(prod)] = 0.\n        out[isinf] = numpy.sum(prod, axis=-1)\n\n        return out[..., :3]\n\n    __call__ = transform\n\n    def transform_vec(self, vecs: ArrayLike) -> NDArray[numpy.floating]:\n        return self.to_linear().transform(vecs)\n\n    @t.overload\n    def compose(self, other: AffineTransform3D) -> AffineTransform3D:\n        ...\n\n    @t.overload\n    def compose(self, other: Transform3DT) -> Transform3DT:\n        ...\n\n    def compose(self, other: Transform3D) -> Transform3D:\n        \"\"\"Compose this transformation with another.\"\"\"\n        if not isinstance(other, Transform3D):\n            raise TypeError(f\"Expected a Transform3D, got {type(other)}\")\n        if isinstance(other, LinearTransform3D):\n            return self.compose(AffineTransform3D.from_linear(other))\n        if isinstance(other, AffineTransform3D):\n            return AffineTransform3D(other.inner @ self.inner)\n        elif hasattr(other, '_rcompose'):\n            return other._rcompose(self)  # type: ignore\n        else:\n            raise NotImplementedError()\n\n    @t.overload\n    def conjugate(self, transform: AffineTransform3D) -> AffineTransform3D:\n        ...\n\n    @t.overload\n    def conjugate(self, transform: Transform3DT) -> Transform3DT:\n        ...\n\n    def conjugate(self, transform: Transform3D) -> Transform3D:\n        \"\"\"\n        Apply ``transform`` in the coordinate frame of ``self``.\n\n        Equivalent to an (inverse) conjugation in group theory, or :math:`T^-1 A T`\n        \"\"\"\n        return self.inverse() @ transform @ self\n\n    @t.overload\n    def __matmul__(self, other: AffineTransform3D) -> AffineTransform3D:\n        ...\n\n    @t.overload\n    def __matmul__(self, other: Transform3D) -> Transform3D:\n        ...\n\n    @t.overload\n    def __matmul__(self, other: BBox3D) -> BBox3D:\n        ...\n\n    @t.overload\n    def __matmul__(self, other: ArrayLike) -> NDArray[numpy.floating]:\n        ...\n\n    def __matmul__(self, other: t.Union[Transform3D, ArrayLike, BBox3D]):  # type: ignore (spurious)\n        \"\"\"Compose this transformation, or apply it to a given set of points.\"\"\"\n        if isinstance(other, Transform3D):\n            return other.compose(self)\n        return self.transform(other)\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.inner","title":"inner instance-attribute","text":"
    inner = broadcast_to(array, (4, 4))\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.make","title":"make staticmethod","text":"
    make(data: IntoTransform3D) -> Transform3D\n

    Make a transformation from a function or numpy array (3x3 or 4x4).

    Source code in atomlib/transform.py
    @staticmethod\ndef make(data: IntoTransform3D) -> Transform3D:\n    \"\"\"Make a transformation from a function or numpy array (3x3 or 4x4).\"\"\"\n    if isinstance(data, Transform3D):\n        return data\n    if not isinstance(data, numpy.ndarray) and hasattr(data, '__call__'):\n        return FuncTransform3D(data)\n    data = numpy.array(data)\n    if data.shape == (3, 3):\n        return LinearTransform3D(data)\n    if data.shape == (4, 4):\n        return AffineTransform3D(data)\n    raise ValueError(f\"Transform3D of invalid shape {data.shape}\")\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.identity","title":"identity staticmethod","text":"
    identity() -> AffineTransform3D\n

    Return an identity transformation.

    Source code in atomlib/transform.py
    @staticmethod\ndef identity() -> AffineTransform3D:\n    \"\"\"Return an identity transformation.\"\"\"\n    return AffineTransform3D()\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.round_near_zero","title":"round_near_zero","text":"
    round_near_zero() -> Affine3DSelf\n

    Round near-zero matrix elements in self.

    Source code in atomlib/transform.py
    def round_near_zero(self: Affine3DSelf) -> Affine3DSelf:\n    \"\"\"Round near-zero matrix elements in self.\"\"\"\n    return type(self)(\n        numpy.where(numpy.abs(self.inner) < 1e-15, 0., self.inner)\n    )\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.from_linear","title":"from_linear staticmethod","text":"
    from_linear(linear: LinearTransform3D) -> AffineTransform3D\n

    Make an affine transformation from a linear transformation.

    Source code in atomlib/transform.py
    @staticmethod\ndef from_linear(linear: LinearTransform3D) -> AffineTransform3D:\n    \"\"\"Make an affine transformation from a linear transformation.\"\"\"\n    dtype = linear.inner.dtype\n    return AffineTransform3D(numpy.block([\n        [linear.inner, numpy.zeros((3, 1), dtype=dtype)],\n        [numpy.zeros((1, 3), dtype=dtype), numpy.ones((), dtype=dtype)]\n    ]))  # type: ignore\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.to_linear","title":"to_linear","text":"
    to_linear() -> LinearTransform3D\n

    Return the linear part of an affine transformation.

    Source code in atomlib/transform.py
    def to_linear(self) -> LinearTransform3D:\n    \"\"\"Return the linear part of an affine transformation.\"\"\"\n    return LinearTransform3D(self.inner[:3, :3])\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.to_translation","title":"to_translation","text":"
    to_translation() -> AffineTransform3D\n

    Extract the translation component of self, and return it.

    Source code in atomlib/transform.py
    def to_translation(self) -> AffineTransform3D:\n    \"\"\"Extract the translation component of `self`, and return it.\"\"\"\n    return AffineTransform3D.translate(self.translation())\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.det","title":"det","text":"
    det() -> float\n

    Return the determinant of an affine transformation.

    Source code in atomlib/transform.py
    def det(self) -> float:\n    \"\"\"Return the determinant of an affine transformation.\"\"\"\n    return numpy.linalg.det(self.inner[:3, :3])\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.translation","title":"translation","text":"
    translation() -> ndarray\n

    Extract the translation component of self, and return it as a vector.

    Source code in atomlib/transform.py
    def translation(self) -> numpy.ndarray:\n    \"\"\"Extract the translation component of `self`, and return it as a vector.\"\"\"\n    return self.inner[:3, -1]\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.inverse","title":"inverse","text":"
    inverse() -> AffineTransform3D\n

    Return the inverse of an affine transformation.

    Source code in atomlib/transform.py
    def inverse(self) -> AffineTransform3D:\n    \"\"\"Return the inverse of an affine transformation.\"\"\"\n    linear_inv = LinearTransform3D(self.inner[:3, :3]).inverse()\n    # first undo translation, then undo linear transformation\n    return linear_inv @ AffineTransform3D.translate(*-self.translation())\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.translate","title":"translate","text":"
    translate(\n    x: Union[Num, VecLike] = 0.0, y: Num = 0.0, z: Num = 0.0\n) -> AffineTransform3D\n

    Create or append an affine translation.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef translate(self, x: t.Union[Num, VecLike] = 0., y: Num = 0., z: Num = 0.) -> AffineTransform3D:\n    \"\"\"\n    Create or append an affine translation.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    if isinstance(x, t.Sized) and len(x) > 1:\n        try:\n            (x, y, z) = to_vec3(x)\n        except ValueError:\n            raise ValueError(\"translate() must be called with a sequence or three numbers.\")\n\n    if isinstance(self, LinearTransform3D):\n        self = AffineTransform3D.from_linear(self)\n\n    a = self.inner.copy()\n    a[:3, -1] += [x, y, z]\n    return AffineTransform3D(a)\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.scale","title":"scale","text":"
    scale(\n    x: Union[Num, VecLike] = 1.0,\n    y: Num = 1.0,\n    z: Num = 1.0,\n    *,\n    all: Num = 1.0\n) -> AffineTransform3D\n

    Create or append a scaling transformation.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef scale(self, x: t.Union[Num, VecLike] = 1., y: Num = 1., z: Num = 1., *,\n          all: Num = 1.) -> AffineTransform3D:\n    \"\"\"\n    Create or append a scaling transformation.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    return self.compose(LinearTransform3D.scale(x, y, z, all=all))  # type: ignore\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.rotate","title":"rotate","text":"
    rotate(v: VecLike, theta: Num) -> AffineTransform3D\n

    Create or append a rotation transformation of theta radians CCW around the given vector v.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef rotate(self, v: VecLike, theta: Num) -> AffineTransform3D:\n    \"\"\"\n    Create or append a rotation transformation of `theta`\n    radians CCW around the given vector `v`.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    return self.compose(LinearTransform3D.rotate(v, theta))\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.rotate_euler","title":"rotate_euler","text":"
    rotate_euler(\n    x: Num = 0.0, y: Num = 0.0, z: Num = 0.0\n) -> AffineTransform3D\n

    Create or append a Euler rotation transformation. Rotation is performed on the x axis first, then y axis and z axis. Values are specified in radians.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef rotate_euler(self, x: Num = 0., y: Num = 0., z: Num = 0.) -> AffineTransform3D:\n    \"\"\"\n    Create or append a Euler rotation transformation.\n    Rotation is performed on the x axis first, then y axis and z axis.\n    Values are specified in radians.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    return self.compose(LinearTransform3D.rotate_euler(x, y, z))\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.mirror","title":"mirror","text":"
    mirror(\n    a: Union[Num, VecLike],\n    b: Optional[Num] = None,\n    c: Optional[Num] = None,\n) -> AffineTransform3D\n

    Create or append a mirror transformation across the given plane.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef mirror(self, a: t.Union[Num, VecLike],\n           b: t.Optional[Num] = None,\n           c: t.Optional[Num] = None) -> AffineTransform3D:\n    \"\"\"\n    Create or append a mirror transformation across the given plane.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    return self.compose(LinearTransform3D.mirror(a, b, c))  # type: ignore\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.strain","title":"strain","text":"
    strain(\n    strain: float,\n    v: VecLike = (0, 0, 1),\n    poisson: float = 0.0,\n) -> AffineTransform3D\n

    Apply a strain of strain in direction v, assuming an elastically isotropic material.

    Strain is applied relative to the origin.

    With poisson=0 (default), a uniaxial strain is applied. With poisson=-1, hydrostatic strain is applied. Otherwise, a uniaxial stress is applied for a material with Poisson ratio poisson, which results in shrinkage perpendicular to the direction strain is applied.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef strain(self, strain: float, v: VecLike = (0, 0, 1), poisson: float = 0.) -> AffineTransform3D:\n    \"\"\"\n    Apply a strain of ``strain`` in direction ``v``, assuming an elastically isotropic material.\n\n    Strain is applied relative to the origin.\n\n    With ``poisson=0`` (default), a uniaxial strain is applied.\n    With ``poisson=-1``, hydrostatic strain is applied.\n    Otherwise, a uniaxial stress is applied for a material with Poisson ratio `poisson`,\n    which results in shrinkage perpendicular to the direction strain is applied.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    return self.compose(LinearTransform3D.strain(strain, v, poisson))\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.align_standard","title":"align_standard","text":"
    align_standard() -> AffineTransform3D\n

    Align self so v1 is in the x-axis and v2 is in the xy-plane.

    This is equivalent to a \\(QR\\) decomposition which keeps only the right-triangular matrix \\(R\\).

    For an affine transformation, this rotates the transformation around the global origin (including any transformation).

    Source code in atomlib/transform.py
    def align_standard(self) -> AffineTransform3D:\n    \"\"\"\n    Align `self` so `v1` is in the x-axis and `v2` is in the xy-plane.\n\n    This is equivalent to a $QR$ decomposition which keeps only the\n    right-triangular matrix $R$.\n\n    For an affine transformation, this rotates the transformation\n    around the global origin (including any transformation).\n    \"\"\"\n    if self.det() <= 0:\n        raise ValueError(\"align_standard() requires a right-handed transformation\")\n\n    translation = self.translation()\n\n    import scipy.linalg\n    q, r = t.cast(t.Tuple[numpy.ndarray, numpy.ndarray], scipy.linalg.qr(self.to_linear().inner))\n    # qr unique up to the sign of the digonal\n    # we choose the case where r is positive definite\n    q = q * numpy.sign(r.diagonal())\n    r = r * numpy.sign(r.diagonal())[:, None]\n    assert numpy.linalg.det(r) > 0\n    # we need to remove the rotation from the translation component as well\n    # q is orthogonal, so q^-1 = q.T\n    return LinearTransform3D(r).translate(q.T @ translation).round_near_zero()\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.transform","title":"transform","text":"
    transform(\n    points: Pts3DLike,\n) -> Union[BBox3D, NDArray[floating]]\n

    Transform points according to the given transformation.

    Source code in atomlib/transform.py
    def transform(self, points: Pts3DLike) -> t.Union[BBox3D, NDArray[numpy.floating]]:\n    \"\"\"Transform points according to the given transformation.\"\"\"\n    if isinstance(points, BBox3D):\n        return points.from_pts(self.transform(points.corners()))\n\n    pts: NDArray[numpy.floating] = numpy.atleast_1d(points).astype(self.inner.dtype)\n    pts = numpy.concatenate((points, numpy.broadcast_to(1., (*pts.shape[:-1], 1))), axis=-1)\n\n    # carefully handle inf and nan\n    isnan = numpy.bitwise_or.reduce(numpy.isnan(pts), axis=-1)\n\n    with numpy.errstate(invalid='ignore'):\n        out = pts @ self.inner.T\n        isinf = numpy.isnan(out[..., 0]) & ~isnan\n\n        prod = numpy.multiply(self.inner, pts[isinf, None, :])\n\n    # inf * 0 = 0\n    prod[numpy.isnan(prod)] = 0.\n    out[isinf] = numpy.sum(prod, axis=-1)\n\n    return out[..., :3]\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.transform_vec","title":"transform_vec","text":"
    transform_vec(vecs: ArrayLike) -> NDArray[floating]\n
    Source code in atomlib/transform.py
    def transform_vec(self, vecs: ArrayLike) -> NDArray[numpy.floating]:\n    return self.to_linear().transform(vecs)\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.compose","title":"compose","text":"
    compose(other: Transform3D) -> Transform3D\n

    Compose this transformation with another.

    Source code in atomlib/transform.py
    def compose(self, other: Transform3D) -> Transform3D:\n    \"\"\"Compose this transformation with another.\"\"\"\n    if not isinstance(other, Transform3D):\n        raise TypeError(f\"Expected a Transform3D, got {type(other)}\")\n    if isinstance(other, LinearTransform3D):\n        return self.compose(AffineTransform3D.from_linear(other))\n    if isinstance(other, AffineTransform3D):\n        return AffineTransform3D(other.inner @ self.inner)\n    elif hasattr(other, '_rcompose'):\n        return other._rcompose(self)  # type: ignore\n    else:\n        raise NotImplementedError()\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.conjugate","title":"conjugate","text":"
    conjugate(transform: Transform3D) -> Transform3D\n

    Apply transform in the coordinate frame of self.

    Equivalent to an (inverse) conjugation in group theory, or :math:T^-1 A T

    Source code in atomlib/transform.py
    def conjugate(self, transform: Transform3D) -> Transform3D:\n    \"\"\"\n    Apply ``transform`` in the coordinate frame of ``self``.\n\n    Equivalent to an (inverse) conjugation in group theory, or :math:`T^-1 A T`\n    \"\"\"\n    return self.inverse() @ transform @ self\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D","title":"LinearTransform3D","text":"

    Bases: AffineTransform3D

    Source code in atomlib/transform.py
    class LinearTransform3D(AffineTransform3D):\n    def __init__(self, array: t.Optional[ArrayLike] = None):\n        if array is None:\n            array = numpy.eye(3, dtype=numpy.float64)\n        self.inner = numpy.broadcast_to(array, (3, 3))\n\n    @property\n    def T(self):\n        return LinearTransform3D(self.inner.T)\n\n    def __repr__(self) -> str:\n        return f\"LinearTransform3D(\\n{self.inner!r}\\n)\"\n\n    def translation(self):\n        \"\"\"Extract the translation component of `self`, and return it as a vector.\"\"\"\n        return numpy.zeros(3, dtype=self.inner.dtype)\n\n    @staticmethod\n    def identity() -> LinearTransform3D:\n        \"\"\"Return an identity transformation.\"\"\"\n        return LinearTransform3D()\n\n    def det(self) -> float:\n        \"\"\"Return the determinant of an affine transformation.\"\"\"\n        return numpy.linalg.det(self.inner)\n\n    def inverse(self) -> LinearTransform3D:\n        \"\"\"Return the inverse of an affine transformation.\"\"\"\n        return LinearTransform3D(numpy.linalg.inv(self.inner))\n\n    def to_linear(self) -> LinearTransform3D:\n        \"\"\"Return the linear part of an affine transformation.\"\"\"\n        return self\n\n    def is_diagonal(self, tol: float = 1e-10) -> bool:\n        \"\"\"\n        Return whether this transformation is diagonal (i.e. axis-aligned scaling only).\n        \"\"\"\n        d = self.inner.shape[0]\n        p, q = self.inner.strides\n        offdiag = numpy.lib.stride_tricks.as_strided(self.inner[:, 1:], (d-1, d), (p+q, q))\n        return bool((numpy.abs(offdiag) < tol).all())\n\n    def is_normal(self, rtol: float = 1e-5, atol: float = 1e-8) -> bool:\n        \"\"\"Returns `True` if `self` is a normal matrix.\"\"\"\n        return bool(numpy.allclose(\n            self.inner.T @ self.inner, self.inner @ self.inner.T,\n            rtol=rtol, atol=atol\n        ))\n\n    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n        \"\"\"\n        Returns `True` if `self` is an orthogonal matrix (i.e. a pure rotation or roto-reflection).\n        \"\"\"\n        return numpy.allclose(self.inner @ self.inner.T, numpy.eye(3), atol=tol)\n\n    def is_scaled_orthogonal(self, tol: float = 1e-8) -> bool:\n        \"\"\"\n        Returns `True` if `self` is a scaled orthogonal matrix (composed of orthogonal\n        basis vectors, i.e. a scaling + a rotation or roto-reflection)\n        \"\"\"\n        return is_diagonal(self.inner @ self.inner.T, tol=tol)\n\n    @t.overload\n    @classmethod\n    def mirror(cls, a: VecLike, /) -> LinearTransform3D:\n        ...\n\n    @t.overload\n    @classmethod\n    def mirror(cls, a: Num, b: Num, c: Num) -> LinearTransform3D:\n        ...\n\n    @opt_classmethod\n    def mirror(self, a: t.Union[Num, VecLike],\n               b: t.Optional[Num] = None,\n               c: t.Optional[Num] = None) -> LinearTransform3D:\n        \"\"\"\n        Create or append a mirror transformation across the given plane.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        if isinstance(a, t.Sized):\n            v = numpy.array(numpy.broadcast_to(a, 3), dtype=numpy.float64)\n            if b is not None or c is not None:\n                raise ValueError(\"mirror() must be passed a sequence or three numbers.\")\n        else:\n            v = numpy.array([a, b, c], dtype=numpy.float64)\n        v /= numpy.linalg.norm(v)\n        mirror = numpy.eye(3) - 2 * numpy.outer(v, v)\n        return LinearTransform3D(mirror @ self.inner)\n\n    @opt_classmethod\n    def strain(self, strain: float, v: VecLike = (0, 0, 1), poisson: float = 0.) -> LinearTransform3D:\n        \"\"\"\n        Apply a strain of ``strain`` in direction ``v``, assuming an elastically isotropic material.\n\n        Strain is applied relative to the origin.\n\n        With ``poisson=0`` (default), a uniaxial strain is applied.\n        With ``poisson=-1``, hydrostatic strain is applied.\n        Otherwise, a uniaxial stress is applied for a material with Poisson ratio `poisson`,\n        which results in shrinkage perpendicular to the direction strain is applied.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        shrink = (1 + strain) ** -poisson\n        return self.compose(LinearTransform3D.align(v).conjugate(\n                            LinearTransform3D.scale([shrink, shrink, 1. + strain])))\n\n    @opt_classmethod\n    def rotate(self, v: VecLike, theta: Num) -> LinearTransform3D:\n        \"\"\"\n        Create or append a rotation transformation of `theta`\n        radians CCW around the given vector `v`.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        theta = float(theta)\n        v = numpy.array(numpy.broadcast_to(v, (3,)), dtype=numpy.float64)\n        l = numpy.linalg.norm(v)\n        if numpy.isclose(l, 0.):\n            if numpy.isclose(theta, 0.):\n                # null rotation\n                return self\n            raise ValueError(\"rotate() about the zero vector is undefined.\")\n        v /= l\n\n        # Rodrigues rotation formula\n        w = numpy.array([[  0., -v[2],  v[1]],\n                         [ v[2],   0., -v[0]],\n                         [-v[1], v[0],   0.]], dtype=numpy.float64)\n        # I + sin(t) W + (1 - cos(t)) W^2 = I + sin(t) W + 2*sin^2(t/2) W^2\n        a = numpy.eye(3) + numpy.sin(theta) * w + 2 * (numpy.sin(theta / 2)**2) * w @ w\n        return LinearTransform3D(a @ self.inner)\n\n    @opt_classmethod\n    def rotate_euler(self, x: Num = 0., y: Num = 0., z: Num = 0.) -> LinearTransform3D:\n        \"\"\"\n        Create or append a Euler rotation transformation.\n        Rotation is performed on the x axis first, then y axis and z axis.\n        Values are specified in radians.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        angles = numpy.array([x, y, z], dtype=numpy.float64)\n        c, s = numpy.cos(angles), numpy.sin(angles)\n        a = numpy.array([\n            [c[1]*c[2], s[0]*s[1]*c[2] - c[0]*s[2], c[0]*s[1]*c[2] + s[0]*s[2]],\n            [c[1]*s[2], s[0]*s[1]*s[2] + c[0]*c[2], c[0]*s[1]*s[2] - s[0]*c[2]],\n            [-s[1],     s[0]*c[1],                  c[0]*c[1]],\n        ], dtype=numpy.float64)\n        return LinearTransform3D(a @ self.inner)\n\n    @opt_classmethod\n    def align(self, v1: VecLike, horz: t.Optional[VecLike] = None) -> LinearTransform3D:\n        \"\"\"\n        Create a transformation which transforms `v1` to align with [0, 0, 1].\n        If `horz` is specified, it will be aligned in the direction of [1, 0, 0].\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        v1 = numpy.broadcast_to(v1, 3)\n        v1 = v1 / numpy.linalg.norm(v1)\n        if horz is None:\n            if numpy.isclose(v1[0], 1.):\n                # zone is [1., 0., 0.], choose a different direction\n                horz = numpy.array([0., 1., 0.])\n            else:\n                horz = numpy.array([1., 0., 0.])\n        else:\n            horz = numpy.broadcast_to(horz, 3)\n\n        return self.align_to(v1, [0., 0., 1.], horz, [1., 0., 0.])\n\n    @t.overload\n    @classmethod\n    def align_to(cls, v1: VecLike, v2: VecLike, p1: t.Literal[None] = None, p2: t.Literal[None] = None) -> LinearTransform3D:\n        ...\n\n    @t.overload\n    @classmethod\n    def align_to(cls, v1: VecLike, v2: VecLike, p1: VecLike, p2: VecLike) -> LinearTransform3D:\n        ...\n\n    @opt_classmethod\n    def align_to(self, v1: VecLike, v2: VecLike,\n                 p1: t.Optional[VecLike] = None, p2: t.Optional[VecLike] = None) -> LinearTransform3D:\n        \"\"\"\n        Create a transformation which transforms `v1` to align with `v2`.\n        If specified, additionally ensure that `p1` aligns with `p2` in the plane of `v2`.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        v1 = numpy.broadcast_to(v1, 3)\n        v1 = v1 / numpy.linalg.norm(v1)\n        v2 = numpy.broadcast_to(v2, 3)\n        v2 = v2 / numpy.linalg.norm(v2)\n\n        v3 = numpy.cross(v1, v2)\n        # rotate along v1 x v2 (geodesic rotation)\n        theta = numpy.arctan2(numpy.linalg.norm(v3), numpy.dot(v1, v2))\n        if numpy.isclose(numpy.linalg.norm(v3), 0.):\n            # any non-v1/v2 vector works. We choose the unit vector with largest cross product\n            v3 = numpy.zeros_like(v3)\n            v3[numpy.argmin(numpy.abs(v1))] = 1.\n\n        aligned = self.rotate(v3, theta)\n\n        if p1 is None and p2 is None:\n            return aligned.round_near_zero()\n        if p1 is None:\n            raise ValueError(\"If `p2` is specified, `p1` must also be specified.\")\n        if p2 is None:\n            raise ValueError(\"If `p1` is specified, `p2` must also be specified.\")\n\n        p1_align = aligned.transform(numpy.broadcast_to(p1, 3))\n        p2 = numpy.broadcast_to(p2, 3)\n        # components perpendicular to v2\n        p2_perp = perp(p2, v2)\n        p1_perp = perp(p1_align, v2)\n        # now rotate along v2\n        theta = numpy.arctan2(numpy.dot(v2, numpy.cross(p1_perp, p2_perp)), numpy.dot(p1_perp, p2_perp))\n        #theta = numpy.arctan2(numpy.linalg.norm(numpy.cross(p1_perp, p2_perp)), numpy.dot(p1_perp, p2_perp))\n        return aligned.rotate(v2, theta).round_near_zero()\n\n    def align_standard(self) -> LinearTransform3D:\n        \"\"\"\n        Align `self` so `v1` is in the x-axis and `v2` is in the xy-plane.\n\n        This is equivalent to a $QR$ decomposition which keeps only the\n        right-triangular matrix $R$.\n        \"\"\"\n        if self.det() <= 0:\n            raise ValueError(\"align_standard() requires a right-handed transformation\")\n\n        import scipy.linalg\n        _q, r = t.cast(t.Tuple[numpy.ndarray, numpy.ndarray], scipy.linalg.qr(self.inner))\n        # qr unique up to the sign of the digonal\n        # we choose the case where r is positive definite\n        r = r * numpy.sign(r.diagonal())[:, None]\n        assert numpy.linalg.det(r) > 0  # check our work\n        return LinearTransform3D(r).round_near_zero()\n\n    def _orthogonal_axes(self, max_denom: int = 1000) -> NDArray[numpy.int_]:\n        \"\"\"\n        Given a linear transformation A, compute an optimal linear\n        combination of basis vectors to form an orthogonal basis.\n\n        More formally, returns a small integer matrix M such that A@M is normal.\n        \"\"\"\n        import scipy.linalg\n\n        inv = self.inverse().inner\n        r, _q = scipy.linalg.rq(inv)\n        # rq unique up to the sign of the digonal\n        r = r * numpy.sign(r.diagonal())\n\n        int_r = numpy.array([reduce_vec(v, max_denom) for v in r.T]).T\n        return int_r\n\n    @t.overload\n    @classmethod\n    def scale(cls, x: VecLike, /) -> LinearTransform3D:\n        ...\n\n    @t.overload\n    @classmethod\n    def scale(cls, x: Num = 1., y: Num = 1., z: Num = 1., *,\n              all: Num = 1.) -> LinearTransform3D:\n        ...\n\n    @opt_classmethod\n    def scale(self, x: t.Union[Num, VecLike] = 1., y: Num = 1., z: Num = 1., *,\n              all: Num = 1.) -> LinearTransform3D:\n        \"\"\"\n        Create or append a scaling transformation.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        if isinstance(x, t.Sized):\n            v = numpy.broadcast_to(x, 3)\n            if y != 1. or z != 1.:\n                raise ValueError(\"scale() must be passed a sequence or three numbers.\")\n        else:\n            v = numpy.array([x, y, z])\n\n        a = numpy.zeros((3, 3), dtype=self.inner.dtype)\n        a[numpy.diag_indices(3)] = all * v\n        return LinearTransform3D(a @ self.inner)\n\n    def conjugate(self, transform: Transform3DT) -> Transform3DT:  # type: ignore (spurious)\n        \"\"\"\n        Apply `transform` in the coordinate frame of `self`.\n\n        Equivalent to an (inverse) conjugation in group theory, or $T^{-1} A T$\n        \"\"\"\n        return self.inverse() @ self.compose(transform)\n\n    def compose(self, other: Transform3DT) -> Transform3DT:  # type: ignore (spurious)\n        \"\"\"Compose this transformation with another.\"\"\"\n        if isinstance(other, LinearTransform3D):\n            return other.__class__(other.inner @ self.inner)\n        if isinstance(other, AffineTransform3D):\n            return AffineTransform3D.from_linear(self).compose(other)\n        if not isinstance(other, Transform3D):\n            raise TypeError(f\"Expected a Transform3D, got {type(other)}\")\n        elif hasattr(other, '_rcompose'):\n            return other._rcompose(self)  # type: ignore\n        raise NotImplementedError()\n\n    @t.overload\n    def transform(self, points: BBox3D) -> BBox3D:\n        ...\n\n    @t.overload\n    def transform(self, points: ArrayLike) -> NDArray[numpy.floating]:\n        ...\n\n    def transform(self, points: Pts3DLike) -> t.Union[BBox3D, NDArray[numpy.floating]]:\n        \"\"\"Transform points according to the given transformation.\"\"\"\n        if isinstance(points, BBox3D):\n            return points.from_pts(self.transform(points.corners()))\n\n        pts: NDArray[numpy.floating] = numpy.atleast_1d(points).astype(self.inner.dtype)\n        if pts.shape[-1] != 3:\n            raise ValueError(f\"{self.__class__} works on 3d points only.\")\n\n        # carefully handle inf and nan\n        isnan = numpy.bitwise_or.reduce(numpy.isnan(pts), axis=-1)\n\n        with numpy.errstate(invalid='ignore'):\n            out = pts @ self.inner.T\n            isinf = numpy.isnan(out[..., 0]) & ~isnan\n\n            prod = numpy.multiply(self.inner, pts[isinf, None, :])\n\n        # inf * 0 = 0\n        prod[numpy.isnan(prod)] = 0.\n        out[isinf] = numpy.sum(prod, axis=-1)\n\n        return out\n\n    @t.overload\n    def __matmul__(self, other: Transform3DT) -> Transform3DT:\n        ...\n\n    @t.overload\n    def __matmul__(self, other: BBox3D) -> BBox3D:\n        ...\n\n    @t.overload\n    def __matmul__(self, other: ArrayLike) -> NDArray[numpy.floating]:\n        ...\n\n    def __matmul__(self, other: t.Union[Transform3DT, ArrayLike, BBox3D]) -> t.Union[Transform3DT, NDArray[numpy.floating], BBox3D]:\n        \"\"\"Compose this transformation, or apply it to a given set of points.\"\"\"\n        if isinstance(other, Transform3D):\n            return other.compose(self)\n        return self.transform(other)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.inner","title":"inner instance-attribute","text":"
    inner = broadcast_to(array, (3, 3))\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.make","title":"make staticmethod","text":"
    make(data: IntoTransform3D) -> Transform3D\n

    Make a transformation from a function or numpy array (3x3 or 4x4).

    Source code in atomlib/transform.py
    @staticmethod\ndef make(data: IntoTransform3D) -> Transform3D:\n    \"\"\"Make a transformation from a function or numpy array (3x3 or 4x4).\"\"\"\n    if isinstance(data, Transform3D):\n        return data\n    if not isinstance(data, numpy.ndarray) and hasattr(data, '__call__'):\n        return FuncTransform3D(data)\n    data = numpy.array(data)\n    if data.shape == (3, 3):\n        return LinearTransform3D(data)\n    if data.shape == (4, 4):\n        return AffineTransform3D(data)\n    raise ValueError(f\"Transform3D of invalid shape {data.shape}\")\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.transform_vec","title":"transform_vec","text":"
    transform_vec(vecs: ArrayLike) -> NDArray[floating]\n
    Source code in atomlib/transform.py
    def transform_vec(self, vecs: ArrayLike) -> NDArray[numpy.floating]:\n    return self.to_linear().transform(vecs)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.round_near_zero","title":"round_near_zero","text":"
    round_near_zero() -> Affine3DSelf\n

    Round near-zero matrix elements in self.

    Source code in atomlib/transform.py
    def round_near_zero(self: Affine3DSelf) -> Affine3DSelf:\n    \"\"\"Round near-zero matrix elements in self.\"\"\"\n    return type(self)(\n        numpy.where(numpy.abs(self.inner) < 1e-15, 0., self.inner)\n    )\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.from_linear","title":"from_linear staticmethod","text":"
    from_linear(linear: LinearTransform3D) -> AffineTransform3D\n

    Make an affine transformation from a linear transformation.

    Source code in atomlib/transform.py
    @staticmethod\ndef from_linear(linear: LinearTransform3D) -> AffineTransform3D:\n    \"\"\"Make an affine transformation from a linear transformation.\"\"\"\n    dtype = linear.inner.dtype\n    return AffineTransform3D(numpy.block([\n        [linear.inner, numpy.zeros((3, 1), dtype=dtype)],\n        [numpy.zeros((1, 3), dtype=dtype), numpy.ones((), dtype=dtype)]\n    ]))  # type: ignore\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.to_translation","title":"to_translation","text":"
    to_translation() -> AffineTransform3D\n

    Extract the translation component of self, and return it.

    Source code in atomlib/transform.py
    def to_translation(self) -> AffineTransform3D:\n    \"\"\"Extract the translation component of `self`, and return it.\"\"\"\n    return AffineTransform3D.translate(self.translation())\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.translate","title":"translate","text":"
    translate(\n    x: Union[Num, VecLike] = 0.0, y: Num = 0.0, z: Num = 0.0\n) -> AffineTransform3D\n

    Create or append an affine translation.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef translate(self, x: t.Union[Num, VecLike] = 0., y: Num = 0., z: Num = 0.) -> AffineTransform3D:\n    \"\"\"\n    Create or append an affine translation.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    if isinstance(x, t.Sized) and len(x) > 1:\n        try:\n            (x, y, z) = to_vec3(x)\n        except ValueError:\n            raise ValueError(\"translate() must be called with a sequence or three numbers.\")\n\n    if isinstance(self, LinearTransform3D):\n        self = AffineTransform3D.from_linear(self)\n\n    a = self.inner.copy()\n    a[:3, -1] += [x, y, z]\n    return AffineTransform3D(a)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.translation","title":"translation","text":"
    translation()\n

    Extract the translation component of self, and return it as a vector.

    Source code in atomlib/transform.py
    def translation(self):\n    \"\"\"Extract the translation component of `self`, and return it as a vector.\"\"\"\n    return numpy.zeros(3, dtype=self.inner.dtype)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.identity","title":"identity staticmethod","text":"
    identity() -> LinearTransform3D\n

    Return an identity transformation.

    Source code in atomlib/transform.py
    @staticmethod\ndef identity() -> LinearTransform3D:\n    \"\"\"Return an identity transformation.\"\"\"\n    return LinearTransform3D()\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.det","title":"det","text":"
    det() -> float\n

    Return the determinant of an affine transformation.

    Source code in atomlib/transform.py
    def det(self) -> float:\n    \"\"\"Return the determinant of an affine transformation.\"\"\"\n    return numpy.linalg.det(self.inner)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.inverse","title":"inverse","text":"
    inverse() -> LinearTransform3D\n

    Return the inverse of an affine transformation.

    Source code in atomlib/transform.py
    def inverse(self) -> LinearTransform3D:\n    \"\"\"Return the inverse of an affine transformation.\"\"\"\n    return LinearTransform3D(numpy.linalg.inv(self.inner))\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.to_linear","title":"to_linear","text":"
    to_linear() -> LinearTransform3D\n

    Return the linear part of an affine transformation.

    Source code in atomlib/transform.py
    def to_linear(self) -> LinearTransform3D:\n    \"\"\"Return the linear part of an affine transformation.\"\"\"\n    return self\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.is_diagonal","title":"is_diagonal","text":"
    is_diagonal(tol: float = 1e-10) -> bool\n

    Return whether this transformation is diagonal (i.e. axis-aligned scaling only).

    Source code in atomlib/transform.py
    def is_diagonal(self, tol: float = 1e-10) -> bool:\n    \"\"\"\n    Return whether this transformation is diagonal (i.e. axis-aligned scaling only).\n    \"\"\"\n    d = self.inner.shape[0]\n    p, q = self.inner.strides\n    offdiag = numpy.lib.stride_tricks.as_strided(self.inner[:, 1:], (d-1, d), (p+q, q))\n    return bool((numpy.abs(offdiag) < tol).all())\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.is_normal","title":"is_normal","text":"
    is_normal(rtol: float = 1e-05, atol: float = 1e-08) -> bool\n

    Returns True if self is a normal matrix.

    Source code in atomlib/transform.py
    def is_normal(self, rtol: float = 1e-5, atol: float = 1e-8) -> bool:\n    \"\"\"Returns `True` if `self` is a normal matrix.\"\"\"\n    return bool(numpy.allclose(\n        self.inner.T @ self.inner, self.inner @ self.inner.T,\n        rtol=rtol, atol=atol\n    ))\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.is_orthogonal","title":"is_orthogonal","text":"
    is_orthogonal(tol: float = 1e-08) -> bool\n

    Returns True if self is an orthogonal matrix (i.e. a pure rotation or roto-reflection).

    Source code in atomlib/transform.py
    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n    \"\"\"\n    Returns `True` if `self` is an orthogonal matrix (i.e. a pure rotation or roto-reflection).\n    \"\"\"\n    return numpy.allclose(self.inner @ self.inner.T, numpy.eye(3), atol=tol)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.is_scaled_orthogonal","title":"is_scaled_orthogonal","text":"
    is_scaled_orthogonal(tol: float = 1e-08) -> bool\n

    Returns True if self is a scaled orthogonal matrix (composed of orthogonal basis vectors, i.e. a scaling + a rotation or roto-reflection)

    Source code in atomlib/transform.py
    def is_scaled_orthogonal(self, tol: float = 1e-8) -> bool:\n    \"\"\"\n    Returns `True` if `self` is a scaled orthogonal matrix (composed of orthogonal\n    basis vectors, i.e. a scaling + a rotation or roto-reflection)\n    \"\"\"\n    return is_diagonal(self.inner @ self.inner.T, tol=tol)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.mirror","title":"mirror","text":"
    mirror(\n    a: Union[Num, VecLike],\n    b: Optional[Num] = None,\n    c: Optional[Num] = None,\n) -> LinearTransform3D\n

    Create or append a mirror transformation across the given plane.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef mirror(self, a: t.Union[Num, VecLike],\n           b: t.Optional[Num] = None,\n           c: t.Optional[Num] = None) -> LinearTransform3D:\n    \"\"\"\n    Create or append a mirror transformation across the given plane.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    if isinstance(a, t.Sized):\n        v = numpy.array(numpy.broadcast_to(a, 3), dtype=numpy.float64)\n        if b is not None or c is not None:\n            raise ValueError(\"mirror() must be passed a sequence or three numbers.\")\n    else:\n        v = numpy.array([a, b, c], dtype=numpy.float64)\n    v /= numpy.linalg.norm(v)\n    mirror = numpy.eye(3) - 2 * numpy.outer(v, v)\n    return LinearTransform3D(mirror @ self.inner)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.strain","title":"strain","text":"
    strain(\n    strain: float,\n    v: VecLike = (0, 0, 1),\n    poisson: float = 0.0,\n) -> LinearTransform3D\n

    Apply a strain of strain in direction v, assuming an elastically isotropic material.

    Strain is applied relative to the origin.

    With poisson=0 (default), a uniaxial strain is applied. With poisson=-1, hydrostatic strain is applied. Otherwise, a uniaxial stress is applied for a material with Poisson ratio poisson, which results in shrinkage perpendicular to the direction strain is applied.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef strain(self, strain: float, v: VecLike = (0, 0, 1), poisson: float = 0.) -> LinearTransform3D:\n    \"\"\"\n    Apply a strain of ``strain`` in direction ``v``, assuming an elastically isotropic material.\n\n    Strain is applied relative to the origin.\n\n    With ``poisson=0`` (default), a uniaxial strain is applied.\n    With ``poisson=-1``, hydrostatic strain is applied.\n    Otherwise, a uniaxial stress is applied for a material with Poisson ratio `poisson`,\n    which results in shrinkage perpendicular to the direction strain is applied.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    shrink = (1 + strain) ** -poisson\n    return self.compose(LinearTransform3D.align(v).conjugate(\n                        LinearTransform3D.scale([shrink, shrink, 1. + strain])))\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.rotate","title":"rotate","text":"
    rotate(v: VecLike, theta: Num) -> LinearTransform3D\n

    Create or append a rotation transformation of theta radians CCW around the given vector v.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef rotate(self, v: VecLike, theta: Num) -> LinearTransform3D:\n    \"\"\"\n    Create or append a rotation transformation of `theta`\n    radians CCW around the given vector `v`.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    theta = float(theta)\n    v = numpy.array(numpy.broadcast_to(v, (3,)), dtype=numpy.float64)\n    l = numpy.linalg.norm(v)\n    if numpy.isclose(l, 0.):\n        if numpy.isclose(theta, 0.):\n            # null rotation\n            return self\n        raise ValueError(\"rotate() about the zero vector is undefined.\")\n    v /= l\n\n    # Rodrigues rotation formula\n    w = numpy.array([[  0., -v[2],  v[1]],\n                     [ v[2],   0., -v[0]],\n                     [-v[1], v[0],   0.]], dtype=numpy.float64)\n    # I + sin(t) W + (1 - cos(t)) W^2 = I + sin(t) W + 2*sin^2(t/2) W^2\n    a = numpy.eye(3) + numpy.sin(theta) * w + 2 * (numpy.sin(theta / 2)**2) * w @ w\n    return LinearTransform3D(a @ self.inner)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.rotate_euler","title":"rotate_euler","text":"
    rotate_euler(\n    x: Num = 0.0, y: Num = 0.0, z: Num = 0.0\n) -> LinearTransform3D\n

    Create or append a Euler rotation transformation. Rotation is performed on the x axis first, then y axis and z axis. Values are specified in radians.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef rotate_euler(self, x: Num = 0., y: Num = 0., z: Num = 0.) -> LinearTransform3D:\n    \"\"\"\n    Create or append a Euler rotation transformation.\n    Rotation is performed on the x axis first, then y axis and z axis.\n    Values are specified in radians.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    angles = numpy.array([x, y, z], dtype=numpy.float64)\n    c, s = numpy.cos(angles), numpy.sin(angles)\n    a = numpy.array([\n        [c[1]*c[2], s[0]*s[1]*c[2] - c[0]*s[2], c[0]*s[1]*c[2] + s[0]*s[2]],\n        [c[1]*s[2], s[0]*s[1]*s[2] + c[0]*c[2], c[0]*s[1]*s[2] - s[0]*c[2]],\n        [-s[1],     s[0]*c[1],                  c[0]*c[1]],\n    ], dtype=numpy.float64)\n    return LinearTransform3D(a @ self.inner)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.align","title":"align","text":"
    align(\n    v1: VecLike, horz: Optional[VecLike] = None\n) -> LinearTransform3D\n

    Create a transformation which transforms v1 to align with [0, 0, 1]. If horz is specified, it will be aligned in the direction of [1, 0, 0].

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef align(self, v1: VecLike, horz: t.Optional[VecLike] = None) -> LinearTransform3D:\n    \"\"\"\n    Create a transformation which transforms `v1` to align with [0, 0, 1].\n    If `horz` is specified, it will be aligned in the direction of [1, 0, 0].\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    v1 = numpy.broadcast_to(v1, 3)\n    v1 = v1 / numpy.linalg.norm(v1)\n    if horz is None:\n        if numpy.isclose(v1[0], 1.):\n            # zone is [1., 0., 0.], choose a different direction\n            horz = numpy.array([0., 1., 0.])\n        else:\n            horz = numpy.array([1., 0., 0.])\n    else:\n        horz = numpy.broadcast_to(horz, 3)\n\n    return self.align_to(v1, [0., 0., 1.], horz, [1., 0., 0.])\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.align_to","title":"align_to","text":"
    align_to(\n    v1: VecLike,\n    v2: VecLike,\n    p1: Optional[VecLike] = None,\n    p2: Optional[VecLike] = None,\n) -> LinearTransform3D\n

    Create a transformation which transforms v1 to align with v2. If specified, additionally ensure that p1 aligns with p2 in the plane of v2.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef align_to(self, v1: VecLike, v2: VecLike,\n             p1: t.Optional[VecLike] = None, p2: t.Optional[VecLike] = None) -> LinearTransform3D:\n    \"\"\"\n    Create a transformation which transforms `v1` to align with `v2`.\n    If specified, additionally ensure that `p1` aligns with `p2` in the plane of `v2`.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    v1 = numpy.broadcast_to(v1, 3)\n    v1 = v1 / numpy.linalg.norm(v1)\n    v2 = numpy.broadcast_to(v2, 3)\n    v2 = v2 / numpy.linalg.norm(v2)\n\n    v3 = numpy.cross(v1, v2)\n    # rotate along v1 x v2 (geodesic rotation)\n    theta = numpy.arctan2(numpy.linalg.norm(v3), numpy.dot(v1, v2))\n    if numpy.isclose(numpy.linalg.norm(v3), 0.):\n        # any non-v1/v2 vector works. We choose the unit vector with largest cross product\n        v3 = numpy.zeros_like(v3)\n        v3[numpy.argmin(numpy.abs(v1))] = 1.\n\n    aligned = self.rotate(v3, theta)\n\n    if p1 is None and p2 is None:\n        return aligned.round_near_zero()\n    if p1 is None:\n        raise ValueError(\"If `p2` is specified, `p1` must also be specified.\")\n    if p2 is None:\n        raise ValueError(\"If `p1` is specified, `p2` must also be specified.\")\n\n    p1_align = aligned.transform(numpy.broadcast_to(p1, 3))\n    p2 = numpy.broadcast_to(p2, 3)\n    # components perpendicular to v2\n    p2_perp = perp(p2, v2)\n    p1_perp = perp(p1_align, v2)\n    # now rotate along v2\n    theta = numpy.arctan2(numpy.dot(v2, numpy.cross(p1_perp, p2_perp)), numpy.dot(p1_perp, p2_perp))\n    #theta = numpy.arctan2(numpy.linalg.norm(numpy.cross(p1_perp, p2_perp)), numpy.dot(p1_perp, p2_perp))\n    return aligned.rotate(v2, theta).round_near_zero()\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.align_standard","title":"align_standard","text":"
    align_standard() -> LinearTransform3D\n

    Align self so v1 is in the x-axis and v2 is in the xy-plane.

    This is equivalent to a \\(QR\\) decomposition which keeps only the right-triangular matrix \\(R\\).

    Source code in atomlib/transform.py
    def align_standard(self) -> LinearTransform3D:\n    \"\"\"\n    Align `self` so `v1` is in the x-axis and `v2` is in the xy-plane.\n\n    This is equivalent to a $QR$ decomposition which keeps only the\n    right-triangular matrix $R$.\n    \"\"\"\n    if self.det() <= 0:\n        raise ValueError(\"align_standard() requires a right-handed transformation\")\n\n    import scipy.linalg\n    _q, r = t.cast(t.Tuple[numpy.ndarray, numpy.ndarray], scipy.linalg.qr(self.inner))\n    # qr unique up to the sign of the digonal\n    # we choose the case where r is positive definite\n    r = r * numpy.sign(r.diagonal())[:, None]\n    assert numpy.linalg.det(r) > 0  # check our work\n    return LinearTransform3D(r).round_near_zero()\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.scale","title":"scale","text":"
    scale(\n    x: Union[Num, VecLike] = 1.0,\n    y: Num = 1.0,\n    z: Num = 1.0,\n    *,\n    all: Num = 1.0\n) -> LinearTransform3D\n

    Create or append a scaling transformation.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef scale(self, x: t.Union[Num, VecLike] = 1., y: Num = 1., z: Num = 1., *,\n          all: Num = 1.) -> LinearTransform3D:\n    \"\"\"\n    Create or append a scaling transformation.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    if isinstance(x, t.Sized):\n        v = numpy.broadcast_to(x, 3)\n        if y != 1. or z != 1.:\n            raise ValueError(\"scale() must be passed a sequence or three numbers.\")\n    else:\n        v = numpy.array([x, y, z])\n\n    a = numpy.zeros((3, 3), dtype=self.inner.dtype)\n    a[numpy.diag_indices(3)] = all * v\n    return LinearTransform3D(a @ self.inner)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.conjugate","title":"conjugate","text":"
    conjugate(transform: Transform3DT) -> Transform3DT\n

    Apply transform in the coordinate frame of self.

    Equivalent to an (inverse) conjugation in group theory, or \\(T^{-1} A T\\)

    Source code in atomlib/transform.py
    def conjugate(self, transform: Transform3DT) -> Transform3DT:  # type: ignore (spurious)\n    \"\"\"\n    Apply `transform` in the coordinate frame of `self`.\n\n    Equivalent to an (inverse) conjugation in group theory, or $T^{-1} A T$\n    \"\"\"\n    return self.inverse() @ self.compose(transform)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.compose","title":"compose","text":"
    compose(other: Transform3DT) -> Transform3DT\n

    Compose this transformation with another.

    Source code in atomlib/transform.py
    def compose(self, other: Transform3DT) -> Transform3DT:  # type: ignore (spurious)\n    \"\"\"Compose this transformation with another.\"\"\"\n    if isinstance(other, LinearTransform3D):\n        return other.__class__(other.inner @ self.inner)\n    if isinstance(other, AffineTransform3D):\n        return AffineTransform3D.from_linear(self).compose(other)\n    if not isinstance(other, Transform3D):\n        raise TypeError(f\"Expected a Transform3D, got {type(other)}\")\n    elif hasattr(other, '_rcompose'):\n        return other._rcompose(self)  # type: ignore\n    raise NotImplementedError()\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.transform","title":"transform","text":"
    transform(\n    points: Pts3DLike,\n) -> Union[BBox3D, NDArray[floating]]\n

    Transform points according to the given transformation.

    Source code in atomlib/transform.py
    def transform(self, points: Pts3DLike) -> t.Union[BBox3D, NDArray[numpy.floating]]:\n    \"\"\"Transform points according to the given transformation.\"\"\"\n    if isinstance(points, BBox3D):\n        return points.from_pts(self.transform(points.corners()))\n\n    pts: NDArray[numpy.floating] = numpy.atleast_1d(points).astype(self.inner.dtype)\n    if pts.shape[-1] != 3:\n        raise ValueError(f\"{self.__class__} works on 3d points only.\")\n\n    # carefully handle inf and nan\n    isnan = numpy.bitwise_or.reduce(numpy.isnan(pts), axis=-1)\n\n    with numpy.errstate(invalid='ignore'):\n        out = pts @ self.inner.T\n        isinf = numpy.isnan(out[..., 0]) & ~isnan\n\n        prod = numpy.multiply(self.inner, pts[isinf, None, :])\n\n    # inf * 0 = 0\n    prod[numpy.isnan(prod)] = 0.\n    out[isinf] = numpy.sum(prod, axis=-1)\n\n    return out\n
    "},{"location":"api/types/","title":"atomlib.types","text":""},{"location":"api/types/#atomlib.types.Vec3","title":"Vec3 module-attribute","text":"
    Vec3: TypeAlias = NDArray[floating[Any]]\n

    3D float vector, of shape (3,).

    "},{"location":"api/types/#atomlib.types.VecLike","title":"VecLike module-attribute","text":"
    VecLike: TypeAlias = ArrayLike\n

    3d vector-like

    "},{"location":"api/types/#atomlib.types.Pts3DLike","title":"Pts3DLike module-attribute","text":"
    Pts3DLike: TypeAlias = Union['BBox3D', ArrayLike]\n

    Sequence of 3d points-like

    "},{"location":"api/types/#atomlib.types.Num","title":"Num module-attribute","text":"
    Num: TypeAlias = Union[float, int]\n

    Scalar numeric type

    "},{"location":"api/types/#atomlib.types.ElemLike","title":"ElemLike module-attribute","text":"
    ElemLike: TypeAlias = Union[str, int]\n

    Element-like

    "},{"location":"api/types/#atomlib.types.ElemsLike","title":"ElemsLike module-attribute","text":"
    ElemsLike: TypeAlias = Union[\n    str,\n    int,\n    Sequence[Union[ElemLike, Tuple[ElemLike, float]]],\n]\n
    "},{"location":"api/types/#atomlib.types.ScalarT","title":"ScalarT module-attribute","text":"
    ScalarT = TypeVar('ScalarT', bound=generic)\n

    numpy.generic-bound type variable

    "},{"location":"api/types/#atomlib.types.to_vec3","title":"to_vec3","text":"
    to_vec3(\n    v: VecLike, dtype: Optional[Type[generic]] = None\n) -> NDArray[generic]\n

    Broadcast and coerce v to a Vec3 of type dtype.

    Source code in atomlib/types.py
    def to_vec3(v: VecLike, dtype: t.Optional[t.Type[numpy.generic]] = None) -> NDArray[numpy.generic]:\n    \"\"\"\n    Broadcast and coerce `v` to a [`Vec3`][atomlib.types.Vec3] of type `dtype`.\n    \"\"\"\n\n    try:\n        v = numpy.broadcast_to(v, (3,)).astype(dtype or numpy.float64)\n    except (ValueError, TypeError):\n        raise TypeError(\"Expected a vector of 3 elements.\") from None\n    return v\n
    "},{"location":"api/util/","title":"atomlib.util","text":""},{"location":"api/util/#atomlib.util.FileOrPath","title":"FileOrPath module-attribute","text":"
    FileOrPath: TypeAlias = Union[str, Path, TextIOBase, TextIO]\n

    Open text file or path to a file. Use with open_file.

    "},{"location":"api/util/#atomlib.util.BinaryFileOrPath","title":"BinaryFileOrPath module-attribute","text":"
    BinaryFileOrPath: TypeAlias = Union[\n    str, Path, TextIO, BinaryIO, IOBase\n]\n

    Open binary file or path to a file. Use with open_file_binary.

    "},{"location":"api/util/#atomlib.util.opt_classmethod","title":"opt_classmethod","text":"

    Bases: classmethod, Generic[T, P, U_co]

    Decorates a method that may be called either on an instance or the class. If called on the class, a default instance will be constructed before calling the wrapped function.

    Source code in atomlib/util.py
    class opt_classmethod(classmethod, t.Generic[T, P, U_co]):\n    \"\"\"\n    Decorates a method that may be called either on an instance or the class.\n    If called on the class, a default instance will be constructed before\n    calling the wrapped function.\n    \"\"\"\n\n    __func__: t.Callable[Concatenate[T, P], U_co]  # type: ignore\n    def __init__(self, f: t.Callable[Concatenate[T, P], U_co]):\n        super().__init__(f)  # type: ignore\n\n    def __get__(self, obj: t.Optional[T], ty: t.Optional[t.Type[T]] = None) -> t.Callable[P, U_co]:  # type: ignore\n        if obj is None:\n            if ty is None:\n                raise RuntimeError()  # pragma: no cover\n            obj = ty()\n        return t.cast(\n            t.Callable[P, U_co],\n            super().__get__(obj, obj)  # type: ignore\n        )\n
    "},{"location":"api/util/#atomlib.util.CheckedJoinError","title":"CheckedJoinError","text":"

    Bases: Exception

    Source code in atomlib/util.py
    class CheckedJoinError(Exception):\n    def __init__(self, missing_keys: t.Sequence[t.Any]):\n        self.missing_keys: t.Tuple[t.Any, ...] = tuple(missing_keys)\n        super().__init__()\n\n    def __str__(self) -> str:\n        return f\"Missing match for key(s): '{', '.join(map(repr, self.missing_keys))}'\"\n
    "},{"location":"api/util/#atomlib.util.CheckedJoinError.missing_keys","title":"missing_keys instance-attribute","text":"
    missing_keys: Tuple[Any, ...] = tuple(missing_keys)\n
    "},{"location":"api/util/#atomlib.util.map_some","title":"map_some","text":"
    map_some(\n    f: Callable[[T], U], val: Optional[T]\n) -> Optional[U]\n

    Map f over val if not None.

    Source code in atomlib/util.py
    def map_some(f: t.Callable[[T], U], val: t.Optional[T]) -> t.Optional[U]:\n    \"\"\"\n    Map `f` over `val` if not `None`.\n    \"\"\"\n    return None if val is None else f(val)\n
    "},{"location":"api/util/#atomlib.util.open_file","title":"open_file","text":"
    open_file(\n    f: FileOrPath,\n    mode: Union[Literal[\"r\"], Literal[\"w\"]] = \"r\",\n    newline: Optional[str] = None,\n    encoding: Optional[str] = \"utf-8\",\n) -> AbstractContextManager[TextIOBase]\n

    Open the given file for text I/O.

    If given a path-like, opens it with the specified settings. Otherwise, make an effort to reconfigure the encoding, and check that it is readable/writable as specified.

    Source code in atomlib/util.py
    def open_file(f: FileOrPath,\n              mode: t.Union[t.Literal['r'], t.Literal['w']] = 'r',\n              newline: t.Optional[str] = None,\n              encoding: t.Optional[str] = 'utf-8') -> AbstractContextManager[TextIOBase]:\n    \"\"\"\n    Open the given file for text I/O.\n\n    If given a path-like, opens it with the specified settings.\n    Otherwise, make an effort to reconfigure the encoding, and\n    check that it is readable/writable as specified.\n    \"\"\"\n    if not isinstance(f, (IOBase, t.BinaryIO, t.TextIO)):\n        return open(f, mode, newline=newline, encoding=encoding)\n\n    if isinstance(f, TextIOWrapper):\n        f.reconfigure(newline=newline, encoding=encoding)\n    elif isinstance(f, t.TextIO):\n        f = TextIOWrapper(f.buffer, newline=newline, encoding=encoding)\n    elif isinstance(f, (BufferedIOBase, t.BinaryIO)):\n        f = TextIOWrapper(t.cast(t.BinaryIO, f), newline=newline, encoding=encoding)\n\n    _validate_file(f, mode)\n    return nullcontext(f)  # don't close a f we didn't open\n
    "},{"location":"api/util/#atomlib.util.open_file_binary","title":"open_file_binary","text":"
    open_file_binary(\n    f: BinaryFileOrPath,\n    mode: Union[Literal[\"r\"], Literal[\"w\"]] = \"r\",\n) -> AbstractContextManager[IOBase]\n

    Open the given file for binary I/O.

    If given a path-like, opens it with the specified settings. If given text I/O, reconfigure to binary. Make sure stream is readable/writable, as specified.

    Source code in atomlib/util.py
    def open_file_binary(f: BinaryFileOrPath,\n                     mode: t.Union[t.Literal['r'], t.Literal['w']] = 'r') -> AbstractContextManager[IOBase]:\n    \"\"\"\n    Open the given file for binary I/O.\n\n    If given a path-like, opens it with the specified settings. If given text I/O,\n    reconfigure to binary. Make sure stream is readable/writable, as specified.\n    \"\"\"\n    if not isinstance(f, (IOBase, t.BinaryIO, t.TextIO)):\n        return t.cast(IOBase, open(f, mode + 'b'))\n\n    if isinstance(f, (TextIOWrapper, t.TextIO)):\n        try:\n            f = f.buffer\n        except AttributeError:\n            raise ValueError(\"Error: Couldn't get raw buffer from text file.\")\n    elif isinstance(f, StringIO):\n        if mode == 'w':\n            raise ValueError(\"Can't write binary stream to StringIO.\")\n        return BytesIO(f.getvalue().encode('utf-8'))\n    elif isinstance(f, TextIOBase):\n        raise ValueError(f\"Error: Couldn't get binary stream from text stream of type '{type(f)}'.\")\n\n    _validate_file(f, mode)\n    return nullcontext(t.cast(IOBase, f))  # don't close a file we didn't open\n
    "},{"location":"api/util/#atomlib.util.localtime","title":"localtime","text":"
    localtime() -> datetime\n

    Return the current time in a timezone-aware datetime object.

    Source code in atomlib/util.py
    def localtime() -> datetime.datetime:\n    \"\"\"Return the current time in a timezone-aware [datetime][datetime.datetime] object.\"\"\"\n    ltime = time.localtime()\n    tz = datetime.timezone(datetime.timedelta(seconds=ltime.tm_gmtoff), ltime.tm_zone)\n    return datetime.datetime.now(tz)\n
    "},{"location":"api/util/#atomlib.util.proc_seed","title":"proc_seed","text":"
    proc_seed(\n    seed: Optional[object], entropy: object\n) -> Optional[NDArray[uint32]]\n

    Process a random seed, which can be any object (or None for a random seed). Return it in a form which can be passed to numpy.random.default_rng.

    Uses a SHA-256 sum under the hood.

    entropy should be a routine-specific object, to ensure that separate random routines called using the same seed return uncorrelated results.

    Source code in atomlib/util.py
    def proc_seed(seed: t.Optional[object], entropy: object) -> t.Optional[NDArray[numpy.uint32]]:\n    \"\"\"\n    Process a random seed, which can be any object (or `None` for a random seed).\n    Return it in a form which can be passed to [numpy.random.default_rng][].\n\n    Uses a SHA-256 sum under the hood.\n\n    `entropy` should be a routine-specific object, to ensure that separate random\n    routines called using the same seed return uncorrelated results.\n    \"\"\"\n    if seed is None:\n        return None\n    # hash our seed and our extra entropy\n    state = sha256()\n    state.update(str(seed).encode('utf-8'))\n    state.update(json.dumps(entropy).encode('utf-8'))\n    return numpy.frombuffer(state.digest(), dtype=numpy.uint32)\n
    "},{"location":"api/util/#atomlib.util.checked_left_join","title":"checked_left_join","text":"
    checked_left_join(\n    lhs: DataFrame,\n    rhs: DataFrame,\n    on: Optional[str] = None,\n    *,\n    left_on: Optional[str] = None,\n    right_on: Optional[str] = None\n) -> DataFrame\n
    Source code in atomlib/util.py
    def checked_left_join(lhs: polars.DataFrame, rhs: polars.DataFrame, on: t.Optional[str] = None, *,\n                      left_on: t.Optional[str] = None, right_on: t.Optional[str] = None) -> polars.DataFrame:\n    df = lhs.join(rhs, how='inner', on=on, left_on=left_on, right_on=right_on, validate='m:1')\n\n    if len(df) < len(lhs):\n        missing_rows = lhs.join(rhs, how='anti', on=on, left_on=left_on, right_on=right_on)\n        col = t.cast(str, left_on or on)\n        missing = missing_rows.select(polars.col(col).unique()).to_series()\n        raise CheckedJoinError(tuple(missing))\n\n    return df\n
    "},{"location":"api/vec/","title":"atomlib.vec","text":"

    Helper functions for spatial vectors.

    "},{"location":"api/vec/#atomlib.vec.WindingRule","title":"WindingRule module-attribute","text":"
    WindingRule = Literal[\n    \"nonzero\", \"evenodd\", \"positive\", \"negative\"\n]\n
    "},{"location":"api/vec/#atomlib.vec.to_vec3","title":"to_vec3","text":"
    to_vec3(\n    v: VecLike, dtype: Optional[Type[generic]] = None\n) -> NDArray[generic]\n

    Broadcast and coerce v to a Vec3 of type dtype.

    Source code in atomlib/types.py
    def to_vec3(v: VecLike, dtype: t.Optional[t.Type[numpy.generic]] = None) -> NDArray[numpy.generic]:\n    \"\"\"\n    Broadcast and coerce `v` to a [`Vec3`][atomlib.types.Vec3] of type `dtype`.\n    \"\"\"\n\n    try:\n        v = numpy.broadcast_to(v, (3,)).astype(dtype or numpy.float64)\n    except (ValueError, TypeError):\n        raise TypeError(\"Expected a vector of 3 elements.\") from None\n    return v\n
    "},{"location":"api/vec/#atomlib.vec.dot","title":"dot","text":"
    dot(\n    v1: ArrayLike,\n    v2: ArrayLike,\n    axis: int = -1,\n    keepdims: bool = True,\n) -> NDArray[floating]\n

    Take the dot product between two vectors, along axis axis.

    Source code in atomlib/vec.py
    def dot(v1: ArrayLike, v2: ArrayLike, axis: int = -1, keepdims: bool = True) -> NDArray[numpy.floating]:\n    \"\"\"\n    Take the dot product between two vectors, along axis `axis`.\n    \"\"\"\n    return numpy.add.reduce(numpy.atleast_1d(v1) * numpy.atleast_1d(v2), axis=axis, keepdims=keepdims)\n
    "},{"location":"api/vec/#atomlib.vec.norm","title":"norm","text":"
    norm(v: ArrayLike) -> floating\n

    Return the norm of the vector v.

    Source code in atomlib/vec.py
    def norm(v: ArrayLike) -> numpy.floating:\n    \"\"\"\n    Return the norm of the vector `v`.\n    \"\"\"\n    return numpy.linalg.norm(v)\n
    "},{"location":"api/vec/#atomlib.vec.perp","title":"perp","text":"
    perp(v1: ArrayLike, v2: ArrayLike) -> NDArray[floating]\n

    Return the component of v1 perpendicular to v2.

    Source code in atomlib/vec.py
    def perp(v1: ArrayLike, v2: ArrayLike) -> NDArray[numpy.floating]:\n    \"\"\"Return the component of `v1` perpendicular to `v2`.\"\"\"\n    v1 = numpy.atleast_1d(v1)\n    v2 = numpy.atleast_1d(v2)\n    v2 /= norm(v2)\n    return v1 - v2 * dot(v1, v2)\n
    "},{"location":"api/vec/#atomlib.vec.para","title":"para","text":"
    para(v1: ArrayLike, v2: ArrayLike) -> NDArray[floating]\n

    Return the component of v1 parallel to v2.

    Source code in atomlib/vec.py
    def para(v1: ArrayLike, v2: ArrayLike) -> NDArray[numpy.floating]:\n    \"\"\"Return the component of `v1` parallel to `v2`.\"\"\"\n    v1 = numpy.atleast_1d(v1)\n    v2 = numpy.atleast_1d(v2)\n    v2 /= norm(v2)\n    return v2 * dot(v1, v2)\n
    "},{"location":"api/vec/#atomlib.vec.is_diagonal","title":"is_diagonal","text":"
    is_diagonal(matrix: ndarray, tol: float = 1e-10) -> bool\n

    Return if matrix is diagonal, to tolerance tol.

    Source code in atomlib/vec.py
    def is_diagonal(matrix: numpy.ndarray, tol: float = 1e-10) -> bool:\n    \"\"\"\n    Return if `matrix` is diagonal, to tolerance `tol`.\n    \"\"\"\n    d = matrix.shape[0]\n    assert matrix.shape == (d, d)\n    p, q = matrix.strides\n    offdiag = numpy.lib.stride_tricks.as_strided(matrix[:, 1:], (d-1, d), (p+q, q))\n    return bool((numpy.abs(offdiag) < tol).all())\n
    "},{"location":"api/vec/#atomlib.vec.split_arr","title":"split_arr","text":"
    split_arr(\n    a: NDArray[ScalarT], axis: int = 0\n) -> Iterator[NDArray[ScalarT]]\n

    Split the array along the axis axis.

    Source code in atomlib/vec.py
    def split_arr(a: NDArray[ScalarT], axis: int = 0) -> t.Iterator[NDArray[ScalarT]]:\n    \"\"\"\n    Split the array along the axis `axis`.\n    \"\"\"\n    return (numpy.squeeze(sub_a, axis) for sub_a in numpy.split(a, a.shape[axis], axis))\n
    "},{"location":"api/vec/#atomlib.vec.polygon_solid_angle","title":"polygon_solid_angle","text":"
    polygon_solid_angle(\n    poly: ArrayLike,\n    pts: Optional[ArrayLike] = None,\n    winding: Optional[ArrayLike] = None,\n) -> NDArray[float64]\n

    Return the signed solid angle of the polygon poly in the xy plane, as viewed from pts.

    PARAMETER DESCRIPTION poly

    Polygon(s) to compute the angle of, array of shape (..., N, 2)

    TYPE: ArrayLike

    pts

    Point(s) to view the polygons from, array of shape (..., 3)

    TYPE: Optional[ArrayLike] DEFAULT: None

    RETURNS DESCRIPTION NDArray[float64]

    A ndarray of shape broadcast(poly.shape[:-2], pts.shape[:-1]), containing signed solid angles.

    Source code in atomlib/vec.py
    def polygon_solid_angle(poly: ArrayLike, pts: t.Optional[ArrayLike] = None,\n                        winding: t.Optional[ArrayLike] = None) -> NDArray[numpy.float64]:\n    \"\"\"\n    Return the signed solid angle of the polygon `poly` in the xy plane, as viewed from `pts`.\n\n    Args:\n      poly: Polygon(s) to compute the angle of, array of shape `(..., N, 2)`\n      pts: Point(s) to view the polygons from, array of shape `(..., 3)`\n\n    Returns:\n      A ndarray of shape `broadcast(poly.shape[:-2], pts.shape[:-1])`, containing signed solid angles.\n    \"\"\"\n    poly = numpy.atleast_2d(poly).astype(numpy.float64)\n    pts = (numpy.array([0., 0., 0.]) if pts is None else numpy.atleast_1d(pts)).astype(numpy.float64)\n\n    if poly.shape[-1] == 3:\n        raise ValueError(\"Only 2d polygons are supported.\")\n    if poly.shape[-1] != 2:\n        raise ValueError(\"`poly` must be a list of 2d points.\")\n    if winding is None:\n        # calculate winding\n        winding = polygon_winding(poly)\n    else:\n        winding = numpy.asarray(winding, dtype=int)\n    # extend to 3d\n    poly = numpy.concatenate((poly, numpy.zeros_like(poly, shape=(*poly.shape[:-1], 1))), axis=-1)\n\n    if pts.shape[-1] != 3:\n        raise ValueError(\"`pts` must be a list of 3d points.\")\n\n    poly = poly - pts[..., None, :]\n    # normalize polygon points to unit sphere\n    numpy.divide(poly, numpy.linalg.norm(poly, axis=-1, keepdims=True), out=poly)\n\n    def _dot(v1: NDArray[numpy.float64], v2: NDArray[numpy.float64]) -> NDArray[numpy.float64]:\n        return numpy.add.reduce(v1 * v2, axis=-1)\n\n    # next and previous points in polygon\n    poly_n = numpy.roll(poly, -1, axis=-2)\n    poly_p = numpy.roll(poly, 1, axis=-2)\n\n    # spherical angle is 2*pi - sum(atan2(-|v1v2v3|, v1 dot v2 * v2 dot v3 - v1 dot v3))\n    angles = numpy.arctan2(_dot(poly_p, numpy.cross(poly, poly_n)), _dot(poly_p, poly) * _dot(poly, poly_n) - _dot(poly_p, poly_n))\n    angle = numpy.sum(angles, axis=-1)\n\n    # when winding is nonzero, we have to offset the calculated angle by the angle created by winding.\n    numpy.mod(angle, 4*numpy.pi*winding, out=angle, where=(winding != 0))\n    return angle - 2*numpy.pi*winding\n
    "},{"location":"api/vec/#atomlib.vec.polygon_winding","title":"polygon_winding","text":"
    polygon_winding(\n    poly: ArrayLike, pt: Optional[ArrayLike] = None\n) -> NDArray[int64]\n

    Return the winding number of the given 2d polygon poly around the point pt. If pt is not specified, return the polygon's total winding number (turning number).

    Vectorized. CCW winding is defined as positive.

    Source code in atomlib/vec.py
    def polygon_winding(poly: ArrayLike, pt: t.Optional[ArrayLike] = None) -> NDArray[numpy.int64]:\n    \"\"\"\n    Return the winding number of the given 2d polygon ``poly`` around the point ``pt``.\n    If ``pt`` is not specified, return the polygon's total winding number (turning number).\n\n    Vectorized. CCW winding is defined as positive.\n    \"\"\"\n    poly = numpy.atleast_2d(poly)\n    if poly.dtype == object:\n        raise ValueError(\"Ragged arrays not supported.\")\n    poly = poly.astype(numpy.float64)\n\n    if pt is None:\n        # return polygon's total winding number (turning number)\n        poly_next = numpy.roll(poly, -1, axis=-2)\n        # equivalent to the turning number of velocity vectors (difference vectors)\n        poly = poly_next - poly\n        # about the origin\n        pt = numpy.array([0., 0.])\n\n        # remove points at the origin (duplicate points)\n        zero_pts = (numpy.isclose(poly[..., 0], 0., atol=1e-10) & \n                    numpy.isclose(poly[..., 1], 0., atol=1e-10))\n        poly = poly[~zero_pts]\n\n    pt = numpy.atleast_1d(pt)[..., None, :].astype(numpy.float64)\n\n    # shift the polygon's origin to `pt`.\n    poly = poly - pt\n    poly_next = numpy.roll(poly, -1, axis=-2)\n    (x, y) = split_arr(poly, axis=-1)\n    (xn, yn) = split_arr(poly_next, axis=-1)\n\n    # |p1 cross (p2 - p1)| -> (p2 - p1) to right or left of origin\n    x_pos = x*(yn - y) - y*(xn - x)  # type: ignore\n    # count up crossings and down crossings\n    up_crossing = (y <= 0) & (yn > 0) & (x_pos > 0)\n    down_crossing = (y > 0) & (yn <= 0) & (x_pos < 0)\n\n    # reduce and return\n    return (numpy.sum(up_crossing, axis=-1) - numpy.sum(down_crossing, axis=-1)).astype(numpy.int64)\n
    "},{"location":"api/vec/#atomlib.vec.in_polygon","title":"in_polygon","text":"
    in_polygon(\n    poly: ndarray,\n    pt: Optional[ndarray] = None,\n    *,\n    rule: WindingRule = \"evenodd\"\n) -> Union[\n    NDArray[bool_], Callable[[ndarray], NDArray[bool_]]\n]\n

    Return whether pt is in poly, under the given winding rule. In the one-argument form, return a closure which tests poly for the given point.

    Source code in atomlib/vec.py
    def in_polygon(poly: numpy.ndarray, pt: t.Optional[numpy.ndarray] = None, *,\n               rule: WindingRule = 'evenodd') -> t.Union[NDArray[numpy.bool_], t.Callable[[numpy.ndarray], NDArray[numpy.bool_]]]:\n    \"\"\"\n    Return whether `pt` is in `poly`, under the given winding rule.\n    In the one-argument form, return a closure which tests `poly` for the given point.\n    \"\"\"\n    if pt is None:\n        return lambda pt: in_polygon(poly, pt, rule=rule)\n    winding = polygon_winding(poly, pt)\n\n    rule = t.cast(WindingRule, rule.lower())\n    if rule == 'nonzero':\n        return winding.astype(numpy.bool_)\n    elif rule == 'evenodd':\n        return (winding & 1) > 0\n    elif rule == 'positive':\n        return winding > 0\n    elif rule == 'negative':\n        return winding < 0\n    raise ValueError(f\"Unknown winding rule '{rule}'. Expected one of \"\n                     \"'nonzero', 'evenodd', 'positive', or 'negative'.\")\n
    "},{"location":"api/vec/#atomlib.vec.reduce_vec","title":"reduce_vec","text":"
    reduce_vec(\n    arr: ArrayLike, max_denom: int = 10000\n) -> NDArray[int64]\n

    Reduce a crystallographic vector (int or float) to lowest common terms. Example: reduce_vec([3, 3, 3]) = [1, 1, 1] reduce_vec([0.25, 0.25, 0.25]) = [1, 1, 1]

    Source code in atomlib/vec.py
    def reduce_vec(arr: ArrayLike, max_denom: int = 10000) -> NDArray[numpy.int64]:\n    \"\"\"\n    Reduce a crystallographic vector (int or float) to lowest common terms.\n    Example: reduce_vec([3, 3, 3]) = [1, 1, 1]\n    reduce_vec([0.25, 0.25, 0.25]) = [1, 1, 1]\n    \"\"\"\n    a = numpy.atleast_1d(arr)\n    if not numpy.issubdtype(a.dtype, numpy.floating):\n        return a // numpy.gcd.reduce(a, axis=-1, keepdims=True)\n\n    a = a / numpy.max(numpy.abs(a))\n\n    n = numpy.empty(shape=a.shape, dtype=numpy.int64)\n    d = numpy.empty(shape=a.shape, dtype=numpy.int64)\n    with numpy.nditer([a, n, d], ['refs_ok'], [['readonly'], ['writeonly'], ['writeonly']]) as it:  # type: ignore\n        for (v, n_, d_) in it:\n            (n_[()], d_[()]) = Fraction(float(v)).limit_denominator(max_denom).as_integer_ratio()\n\n    # reduce to common denominator\n    factors = numpy.lcm.reduce(d, axis=-1, keepdims=True) // d\n    n *= factors\n    # and then reduce numerators\n    return n // numpy.gcd.reduce(n, axis=-1, keepdims=True)\n
    "},{"location":"api/vec/#atomlib.vec.miller_4_to_3_vec","title":"miller_4_to_3_vec","text":"
    miller_4_to_3_vec(\n    a: NDArray[number],\n    reduce: bool = True,\n    max_denom: int = 10000,\n) -> NDArray[number]\n

    Convert a vector in 4-axis Miller-Bravais notation to 3-axis Miller notation.

    Source code in atomlib/vec.py
    def miller_4_to_3_vec(a: NDArray[numpy.number], reduce: bool = True, max_denom: int = 10000) -> NDArray[numpy.number]:\n    \"\"\"Convert a vector in 4-axis Miller-Bravais notation to 3-axis Miller notation.\"\"\"\n    a = numpy.atleast_1d(a)\n    assert a.shape[-1] == 4\n    U, V, T, W = numpy.split(a, 4, axis=-1)\n    assert numpy.allclose(-T, U + V, equal_nan=True)\n    out = numpy.concatenate((2*U + V, 2*V + U, W), axis=-1)\n    return reduce_vec(out, max_denom) if reduce else out\n
    "},{"location":"api/vec/#atomlib.vec.miller_3_to_4_vec","title":"miller_3_to_4_vec","text":"
    miller_3_to_4_vec(\n    a: NDArray[number],\n    reduce: bool = True,\n    max_denom: int = 10000,\n) -> NDArray[number]\n

    Convert a vector in 3-axis Miller notation to 4-axis Miller-Bravais notation.

    Source code in atomlib/vec.py
    def miller_3_to_4_vec(a: NDArray[numpy.number], reduce: bool = True, max_denom: int = 10000) -> NDArray[numpy.number]:\n    \"\"\"Convert a vector in 3-axis Miller notation to 4-axis Miller-Bravais notation.\"\"\"\n    a = numpy.atleast_1d(a)\n    assert a.shape[-1] == 3\n    u, v, w = numpy.split(a, 3, axis=-1)\n    U = 2*u - v\n    V = 2*v - u\n    W = 3*w\n    out = numpy.concatenate((U, V, -(U + V), W), axis=-1)\n    return reduce_vec(out, max_denom) if reduce else out\n
    "},{"location":"api/vec/#atomlib.vec.miller_4_to_3_plane","title":"miller_4_to_3_plane","text":"
    miller_4_to_3_plane(\n    a: NDArray[number],\n    reduce: bool = True,\n    max_denom: int = 10000,\n) -> NDArray[number]\n

    Convert a plane in 4-axis Miller-Bravais notation to 3-axis Miller notation.

    Source code in atomlib/vec.py
    def miller_4_to_3_plane(a: NDArray[numpy.number], reduce: bool = True, max_denom: int = 10000) -> NDArray[numpy.number]:\n    \"\"\"Convert a plane in 4-axis Miller-Bravais notation to 3-axis Miller notation.\"\"\"\n    a = numpy.atleast_1d(a)\n    assert a.shape[-1] == 4\n    h, k, i, l = numpy.split(a, 4, axis=-1)  # noqa: E741\n    assert numpy.allclose(-i, h + k, equal_nan=True)\n    out = numpy.concatenate((h, k, l), axis=-1)\n    return reduce_vec(out, max_denom) if reduce else out\n
    "},{"location":"api/vec/#atomlib.vec.miller_3_to_4_plane","title":"miller_3_to_4_plane","text":"
    miller_3_to_4_plane(\n    a: NDArray[number],\n    reduce: bool = True,\n    max_denom: int = 10000,\n) -> NDArray[number]\n

    Convert a plane in 3-axis Miller notation to 4-axis Miller-Bravais notation.

    Source code in atomlib/vec.py
    def miller_3_to_4_plane(a: NDArray[numpy.number], reduce: bool = True, max_denom: int = 10000) -> NDArray[numpy.number]:\n    \"\"\"Convert a plane in 3-axis Miller notation to 4-axis Miller-Bravais notation.\"\"\"\n    a = numpy.atleast_1d(a)\n    assert a.shape[-1] == 3\n    h, k, l = numpy.split(a, 3, axis=-1)  # noqa: E741\n    out = numpy.concatenate((h, k, -(h + k), l), axis=-1)\n    return reduce_vec(out, max_denom) if reduce else out\n
    "},{"location":"api/visualize/","title":"atomlib.visualize","text":"

    Visualization of atomic structures. Useful for debugging.

    "},{"location":"api/visualize/#atomlib.visualize.BackendName","title":"BackendName module-attribute","text":"
    BackendName: TypeAlias = Literal['mpl', 'ase']\n
    "},{"location":"api/visualize/#atomlib.visualize.AtomStyle","title":"AtomStyle module-attribute","text":"
    AtomStyle: TypeAlias = Literal[\n    \"spacefill\", \"ballstick\", \"small\"\n]\n
    "},{"location":"api/visualize/#atomlib.visualize.AtomImage","title":"AtomImage","text":"

    Bases: ABC

    Source code in atomlib/visualize/__init__.py
    class AtomImage(ABC):\n    @abstractmethod\n    def save(self, f: FileOrPath):\n        ...\n
    "},{"location":"api/visualize/#atomlib.visualize.AtomImage.save","title":"save abstractmethod","text":"
    save(f: FileOrPath)\n
    Source code in atomlib/visualize/__init__.py
    @abstractmethod\ndef save(self, f: FileOrPath):\n    ...\n
    "},{"location":"api/visualize/#atomlib.visualize.AtomImageMpl","title":"AtomImageMpl","text":"

    Bases: Figure, AtomImage

    Source code in atomlib/visualize/__init__.py
    class AtomImageMpl(Figure, AtomImage):\n    def __new__(cls, fig: Figure):\n        fig.__class__ = cls\n        return fig\n\n    def __init__(self, fig: Figure):\n        ...\n\n    def save(self, f: FileOrPath):\n        return self.savefig(f)  # type: ignore\n
    "},{"location":"api/visualize/#atomlib.visualize.AtomImageMpl.save","title":"save","text":"
    save(f: FileOrPath)\n
    Source code in atomlib/visualize/__init__.py
    def save(self, f: FileOrPath):\n    return self.savefig(f)  # type: ignore\n
    "},{"location":"api/visualize/#atomlib.visualize.show_atoms_3d","title":"show_atoms_3d","text":"
    show_atoms_3d(\n    atoms: HasAtoms,\n    *,\n    zone: Optional[VecLike] = None,\n    plane: Optional[VecLike] = None,\n    backend: BackendName = \"mpl\",\n    style: AtomStyle = \"small\",\n    **kwargs: Any\n) -> AtomImage\n

    Show atoms on a 3D plot, using backend backend (defaults to matplotlib).

    Source code in atomlib/visualize/__init__.py
    def show_atoms_3d(atoms: HasAtoms, *,\n                  zone: t.Optional[VecLike] = None,\n                  plane: t.Optional[VecLike] = None,\n                  backend: BackendName = 'mpl',\n                  style: AtomStyle = 'small', **kwargs: t.Any) -> AtomImage:\n    \"\"\"\n    Show `atoms` on a 3D plot, using backend `backend` (defaults to matplotlib).\n    \"\"\"\n\n    backend = t.cast(BackendName, backend.lower())\n    if backend == 'mpl':\n        return show_atoms_mpl_3d(atoms, zone=zone, plane=plane, style=style, **kwargs)\n    elif backend == 'ase':\n        raise NotImplementedError()\n\n    raise ValueError(f\"Unknown backend '{backend}'\")\n
    "},{"location":"api/visualize/#atomlib.visualize.show_atoms_2d","title":"show_atoms_2d","text":"
    show_atoms_2d(\n    atoms: HasAtoms,\n    *,\n    zone: Optional[VecLike] = None,\n    plane: Optional[VecLike] = None,\n    horz: Optional[VecLike] = None,\n    backend: BackendName = \"mpl\",\n    style: AtomStyle = \"small\",\n    **kwargs: Any\n) -> AtomImage\n

    Show atoms on a 2D plot, using backend backend (defaults to matplotlib).

    Source code in atomlib/visualize/__init__.py
    def show_atoms_2d(atoms: HasAtoms, *,\n                  zone: t.Optional[VecLike] = None,\n                  plane: t.Optional[VecLike] = None,\n                  horz: t.Optional[VecLike] = None,\n                  backend: BackendName = 'mpl',\n                  style: AtomStyle = 'small', **kwargs: t.Any) -> AtomImage:\n    \"\"\"\n    Show `atoms` on a 2D plot, using backend `backend` (defaults to matplotlib).\n    \"\"\"\n\n    backend = t.cast(BackendName, backend.lower())\n    if backend == 'mpl':\n        return show_atoms_mpl_2d(atoms, zone=zone, plane=plane, horz=horz, style=style, **kwargs)\n    elif backend == 'ase':\n        raise NotImplementedError()\n\n    raise ValueError(f\"Unknown backend '{backend}'\")\n
    "},{"location":"api/visualize/#atomlib.visualize.get_elem_color","title":"get_elem_color","text":"
    get_elem_color(elem: int) -> List[int]\n

    Get the color to use for element elem.

    Source code in atomlib/visualize/__init__.py
    def get_elem_color(elem: int) -> t.List[int]:\n    \"\"\"Get the color to use for element `elem`.\"\"\"\n    # grey fallback\n    return _ELEM_MAP.get(elem, [80, 80, 80])\n
    "},{"location":"api/visualize/#atomlib.visualize.get_zone","title":"get_zone","text":"
    get_zone(\n    atoms: HasAtoms,\n    zone: Optional[VecLike] = None,\n    plane: Optional[VecLike] = None,\n    default: Optional[VecLike] = None,\n) -> NDArray[float64]\n

    Get the zone axis corresponding to the arguments zone, plane, and default.

    Source code in atomlib/visualize/__init__.py
    def get_zone(atoms: HasAtoms, zone: t.Optional[VecLike] = None,\n             plane: t.Optional[VecLike] = None,\n             default: t.Optional[VecLike] = None) -> NDArray[numpy.float64]:\n    \"\"\"\n    Get the zone axis corresponding to the arguments `zone`, `plane`, and `default`.\n    \"\"\"\n\n    if zone is not None and plane is not None:\n        raise ValueError(\"'zone' and 'plane' can't both be specified.\")\n    if plane is not None:\n        if isinstance(atoms, AtomCell) and not atoms.is_orthogonal():\n            # convert plane into zone\n            raise NotImplementedError()\n        zone = plane\n    if zone is not None:\n        return numpy.broadcast_to(zone, 3).astype(numpy.float64)\n    if default is not None:\n        return numpy.broadcast_to(default, 3).astype(numpy.float64)\n    return numpy.array([0., 0., 1.], dtype=numpy.float64)\n
    "},{"location":"api/visualize/#atomlib.visualize.get_plot_radii","title":"get_plot_radii","text":"
    get_plot_radii(\n    atoms: HasAtoms,\n    min_r: Optional[float] = 1.0,\n    style: AtomStyle = \"small\",\n) -> NDArray[float64]\n

    Get the radii to use for each atom in atoms.

    Source code in atomlib/visualize/__init__.py
    def get_plot_radii(atoms: HasAtoms, min_r: t.Optional[float] = 1.0, style: AtomStyle = 'small') -> NDArray[numpy.float64]:\n    \"\"\"Get the radii to use for each atom in `atoms`.\"\"\"\n    radii = get_radius(atoms['elem']).to_numpy()\n    if style == 'small':\n        radii = radii * 0.6\n    elif style == 'ballstick':\n        radii = radii * 0.5\n    elif style == 'spacefill':\n        radii = radii * 1.0\n    else:\n        raise ValueError(f\"Unknown atom style '{style}'. Expected 'spacefill', 'ballstick', or 'small'.\")\n    if min_r is not None:\n        return numpy.maximum(min_r, radii)\n    return radii\n
    "},{"location":"api/visualize/#atomlib.visualize.get_azim_elev","title":"get_azim_elev","text":"
    get_azim_elev(zone: VecLike) -> Tuple[float, float]\n

    Get the azimuth and elevation corresponding to the zone zone.

    Source code in atomlib/visualize/__init__.py
    def get_azim_elev(zone: VecLike) -> t.Tuple[float, float]:\n    \"\"\"Get the azimuth and elevation corresponding to the zone `zone`.\"\"\"\n    (a, b, c) = -to_vec3(zone)  # look down zone\n    # todo: aren't these just arctan2s?\n    return (numpy.angle(a + b*1.j, deg=True), numpy.angle(numpy.sqrt(a**2 + b**2) + c*1.j, deg=True))  # type: ignore\n
    "},{"location":"api/visualize/#atomlib.visualize.show_atoms_mpl_3d","title":"show_atoms_mpl_3d","text":"
    show_atoms_mpl_3d(\n    atoms: HasAtoms,\n    *,\n    fig: Optional[Figure] = None,\n    zone: Optional[VecLike] = None,\n    plane: Optional[VecLike] = None,\n    min_r: Optional[float] = 1.0,\n    style: AtomStyle = \"small\"\n) -> AtomImageMpl\n

    Show atoms on a 3D plot using matplotlib.

    Source code in atomlib/visualize/__init__.py
    def show_atoms_mpl_3d(atoms: HasAtoms, *, fig: t.Optional[Figure] = None,\n                      zone: t.Optional[VecLike] = None,\n                      plane: t.Optional[VecLike] = None,\n                      min_r: t.Optional[float] = 1.0,\n                      style: AtomStyle = 'small') -> AtomImageMpl:\n    \"\"\"Show `atoms` on a 3D plot using matplotlib.\"\"\"\n\n    fig = AtomImageMpl(fig or pyplot.figure())  # type: ignore\n\n    zone = get_zone(atoms, zone, plane, [1., 2., 4.])\n    (azim, elev) = get_azim_elev(zone)\n\n    rect = (0., 0., 1., 1.)\n    ax: Axes3D = fig.add_axes(rect, axes_class=Axes3D, proj_type='ortho', azim=azim, elev=elev)  # type: ignore\n    ax.grid(False)\n\n    bbox = atoms.bbox().pad(0.2)\n    ax.set_xlim3d(bbox.x)  # type: ignore\n    ax.set_ylim3d(bbox.y)  # type: ignore\n    ax.set_zlim3d(bbox.z)  # type: ignore\n    ax.set_box_aspect(bbox.size)\n\n    ax.set_xlabel('X')\n    ax.set_ylabel('Y')\n    ax.set_zlabel('Z')\n\n    frame = atoms.get_atoms('local')\n    #radii = get_plot_radii(atoms, min_r=min_r, style=style)\n    coords = frame.coords()\n    elem_colors = numpy.array(list(map(get_elem_color, frame['elem']))) / 255.\n    s = 100\n\n    if isinstance(atoms, HasCell):\n        # plot cell corners\n        corners = atoms.corners('global')\n        faces = [\n            numpy.array([\n                corners[(val*2**axis + v1*2**((axis+1) % 3) + v2*2**((axis+2) % 3))]\n                for (v1, v2) in [(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]\n            ])\n            for axis in (0, 1, 2)\n            for val in (0, 1)\n        ]\n        for face in faces:\n            ax.plot3D(*split_arr(face, axis=-1), '.-k', alpha=1, markersize=8)\n\n    ax.scatter(coords[:, 0], coords[:, 1], coords[:, 2], c=elem_colors, alpha=1, s=s)  # type: ignore\n\n    return t.cast(AtomImageMpl, fig)\n
    "},{"location":"api/visualize/#atomlib.visualize.show_atoms_mpl_2d","title":"show_atoms_mpl_2d","text":"
    show_atoms_mpl_2d(\n    atoms: HasAtoms,\n    *,\n    fig: Optional[Figure] = None,\n    zone: Optional[VecLike] = None,\n    plane: Optional[VecLike] = None,\n    horz: Optional[VecLike] = None,\n    min_r: Optional[float] = 1.0,\n    style: AtomStyle = \"small\"\n) -> AtomImageMpl\n

    Show atoms on a 2D plot using matplotlib.

    Source code in atomlib/visualize/__init__.py
    def show_atoms_mpl_2d(atoms: HasAtoms, *, fig: t.Optional[Figure] = None,\n                      zone: t.Optional[VecLike] = None,\n                      plane: t.Optional[VecLike] = None,\n                      horz: t.Optional[VecLike] = None,\n                      min_r: t.Optional[float] = 1.0,\n                      style: AtomStyle = 'small') -> AtomImageMpl:\n    \"\"\"Show `atoms` on a 2D plot using matplotlib.\"\"\"\n\n    zone = get_zone(atoms, zone, plane, [0., 0., 1.])\n    fig = AtomImageMpl(fig or pyplot.figure())  # type: ignore\n\n    rect = (0.05, 0.05, 0.95, 0.95)\n    ax: Axes = fig.add_axes(rect)\n    ax.set_aspect('equal')\n\n    frame = atoms.get_atoms('local')\n    coords = frame.coords()\n    elem_colors = numpy.array(list(map(get_elem_color, frame['elem']))) / 255.\n    radii = get_plot_radii(frame, min_r=min_r, style=style)\n\n    # look down zone\n    if horz is None:\n        transform = LinearTransform3D.align_to(zone, [0, 0, -1])\n    else:\n        transform = LinearTransform3D.align_to(zone, [0, 0, -1], horz, [1, 0, 0])\n    bbox_2d = transform @ atoms.bbox()\n    coords = transform @ coords\n    # sort by z-order\n    sort = numpy.argsort(coords[..., 2])\n    coords = coords[sort]\n    elem_colors = elem_colors[sort]\n    radii = radii[sort]\n\n    ax.set_xbound(*bbox_2d.x)\n    ax.set_ybound(*bbox_2d.y)\n\n    # old plotting method\n    # ax.scatter(coords[:, 0], coords[:, 1], c=elem_colors, alpha=1., s=s)\n\n    ax.add_collection(EllipseCollection(\n        radii, radii, numpy.zeros_like(radii), units='xy', facecolors=elem_colors, ec='black',\n        offsets=coords[..., :2], offset_transform=ax.transData,\n    ))  # type: ignore (bad api typing)\n\n    return t.cast(AtomImageMpl, fig)\n
    "},{"location":"api/io/","title":"atomlib.io","text":""},{"location":"api/io/#atomlib.io.FileType","title":"FileType module-attribute","text":"
    FileType: TypeAlias = Literal[\n    \"cif\", \"xyz\", \"xsf\", \"cfg\", \"lmp\", \"mslice\", \"qe\"\n]\n
    "},{"location":"api/io/#atomlib.io.ReadFunc","title":"ReadFunc module-attribute","text":"
    ReadFunc = Callable[[FileOrPath], HasAtoms]\n
    "},{"location":"api/io/#atomlib.io.WriteFunc","title":"WriteFunc module-attribute","text":"
    WriteFunc = Callable[[HasAtoms, FileOrPath], None]\n
    "},{"location":"api/io/#atomlib.io.CIF","title":"CIF dataclass","text":"Source code in atomlib/io/cif.py
    @dataclass\nclass CIF:\n    data_blocks: t.Tuple[CIFDataBlock, ...]\n\n    def __post_init__(self):\n        # ensure that all data_blocks after the first have a name\n        for data_block in self.data_blocks[1:]:\n            if data_block.name is None:\n                data_block.name = \"\"\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> CIF:\n        return CIF(tuple(CIFDataBlock.from_file(file)))\n\n    def __len__(self) -> int:\n        return self.data_blocks.__len__()\n\n    def get_block(self, block: t.Union[int, str]) -> CIFDataBlock:\n        try:\n            if isinstance(block, int):\n                return self.data_blocks[block]\n            return next(b for b in self.data_blocks if b.name == block)\n        except (IndexError, StopIteration):\n            raise ValueError(f\"Couldn't find block '{block}' in CIF file. File has {len(self)} blocks.\")\n\n    def write(self, file: FileOrPath):\n        with open_file(file, 'w') as f:\n            print(\"# generated by atomlib\", file=f, end=None)\n            for data_block in self.data_blocks:\n                print(file=f)\n                data_block._write(f)\n
    "},{"location":"api/io/#atomlib.io.CIF.data_blocks","title":"data_blocks instance-attribute","text":"
    data_blocks: Tuple[CIFDataBlock, ...]\n
    "},{"location":"api/io/#atomlib.io.CIF.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> CIF\n
    Source code in atomlib/io/cif.py
    @staticmethod\ndef from_file(file: FileOrPath) -> CIF:\n    return CIF(tuple(CIFDataBlock.from_file(file)))\n
    "},{"location":"api/io/#atomlib.io.CIF.get_block","title":"get_block","text":"
    get_block(block: Union[int, str]) -> CIFDataBlock\n
    Source code in atomlib/io/cif.py
    def get_block(self, block: t.Union[int, str]) -> CIFDataBlock:\n    try:\n        if isinstance(block, int):\n            return self.data_blocks[block]\n        return next(b for b in self.data_blocks if b.name == block)\n    except (IndexError, StopIteration):\n        raise ValueError(f\"Couldn't find block '{block}' in CIF file. File has {len(self)} blocks.\")\n
    "},{"location":"api/io/#atomlib.io.CIF.write","title":"write","text":"
    write(file: FileOrPath)\n
    Source code in atomlib/io/cif.py
    def write(self, file: FileOrPath):\n    with open_file(file, 'w') as f:\n        print(\"# generated by atomlib\", file=f, end=None)\n        for data_block in self.data_blocks:\n            print(file=f)\n            data_block._write(f)\n
    "},{"location":"api/io/#atomlib.io.XYZ","title":"XYZ dataclass","text":"Source code in atomlib/io/xyz.py
    @dataclass\nclass XYZ:\n    atoms: polars.DataFrame\n    comment: t.Optional[str] = None\n    params: t.Dict[str, str] = field(default_factory=dict)\n\n    @staticmethod\n    def from_atoms(atoms: HasAtoms) -> XYZ:\n        params = {}\n        if isinstance(atoms, HasAtomCell):\n            coords = atoms.get_cell().to_ortho().to_linear().inner.ravel()\n            lattice_str = \" \".join((f\"{c:.8f}\" for c in coords))\n            params['Lattice'] = lattice_str\n\n            pbc_str = \" \".join(str(int(v)) for v in atoms.get_cell().pbc)\n            params['pbc'] = pbc_str\n\n        return XYZ(\n            atoms.get_atoms('local')._get_frame(),\n            params=params\n        )\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> XYZ:\n        logging.info(f\"Loading XYZ {file.name if hasattr(file, 'name') else file!r}...\")  # type: ignore\n\n        with open_file(file, 'r') as f:\n            try:\n                # TODO be more gracious about whitespace here\n                length = int(f.readline())\n            except ValueError:\n                raise ValueError(\"Error parsing XYZ file: Invalid length\") from None\n            except IOError as e:\n                raise IOError(f\"Error parsing XYZ file: {e}\") from None\n\n            comment = f.readline().rstrip('\\n')\n            # TODO handle if there's not a gap here\n\n            try:\n                params = ExtXYZParser(comment).parse()\n            except ValueError:\n                params = None\n\n            schema = _get_columns_from_params(params)\n\n            df = parse_whitespace_separated(f, schema, start_line=1)\n\n            # map atomic numbers -> symbols (on columns which are Int8)\n            df = df.with_columns(\n                get_sym(df.select(polars.col('symbol').cast(polars.Int8, strict=False)).to_series())\n                    .fill_null(df['symbol']).alias('symbol')\n            )\n            # ensure all symbols are recognizable (this will raise ValueError if not)\n            get_elem(df['symbol'])\n\n            if length < len(df):\n                warnings.warn(f\"Warning: truncating structure of length {len(df)} \"\n                            f\"to match declared length of {length}\")\n                df = df[:length]\n            elif length > len(df):\n                warnings.warn(f\"Warning: structure length {len(df)} doesn't match \"\n                            f\"declared length {length}.\\nData could be corrupted.\")\n\n            try:\n                params = ExtXYZParser(comment).parse()\n                return XYZ(df, comment, params)\n            except ValueError:\n                pass\n\n            return XYZ(df, comment)\n\n    def write(self, file: FileOrPath, fmt: XYZFormat = 'exyz'):\n        with open_file(file, 'w', newline='\\r\\n') as f:\n\n            f.write(f\"{len(self.atoms)}\\n\")\n            if len(self.params) > 0 and fmt == 'exyz':\n                f.write(\" \".join(_param_strings(self.params)))\n            else:\n                f.write(self.comment or \"\")\n            f.write(\"\\n\")\n\n            # not my best work\n            col_space = (3, 12, 12, 12)\n            f.writelines(\n                \"\".join(\n                    f\"{val:< {space}.8f}\" if isinstance(val, float) else f\"{val:<{space}}\" for (val, space) in zip(_flatten(row), col_space)\n                ).strip() + '\\n' for row in self.atoms.select(('symbol', 'coords')).rows()\n            )\n\n    def cell_matrix(self) -> t.Optional[NDArray[numpy.float64]]:\n        if (s := self.params.get('Lattice')) is None:\n            return None\n\n        try:\n            items = list(map(float, s.split()))\n            if not len(items) == 9:\n                raise ValueError(\"Invalid length\")\n            return numpy.array(items, dtype=numpy.float64).reshape((3, 3)).T\n        except ValueError:\n            warnings.warn(f\"Warning: Invalid format for key 'Lattice=\\\"{s}\\\"'\")\n        return None\n\n    def pbc(self) -> t.Optional[NDArray[numpy.bool_]]:\n        if (s := self.params.get('pbc')) is None:\n            return None\n\n        val_map = {'0': False, 'f': False, '1': True, 't': True}\n        try:\n            items = [val_map[v.lower()] for v in s.split()]\n            if not len(items) == 3:\n                raise ValueError(\"Invalid length\")\n            return numpy.array(items, dtype=numpy.bool_)\n        except ValueError:\n            warnings.warn(f\"Warning: Invalid format for key 'pbc=\\\"{s}\\\"'\")\n        return None\n
    "},{"location":"api/io/#atomlib.io.XYZ.atoms","title":"atoms instance-attribute","text":"
    atoms: DataFrame\n
    "},{"location":"api/io/#atomlib.io.XYZ.comment","title":"comment class-attribute instance-attribute","text":"
    comment: Optional[str] = None\n
    "},{"location":"api/io/#atomlib.io.XYZ.params","title":"params class-attribute instance-attribute","text":"
    params: Dict[str, str] = field(default_factory=dict)\n
    "},{"location":"api/io/#atomlib.io.XYZ.from_atoms","title":"from_atoms staticmethod","text":"
    from_atoms(atoms: HasAtoms) -> XYZ\n
    Source code in atomlib/io/xyz.py
    @staticmethod\ndef from_atoms(atoms: HasAtoms) -> XYZ:\n    params = {}\n    if isinstance(atoms, HasAtomCell):\n        coords = atoms.get_cell().to_ortho().to_linear().inner.ravel()\n        lattice_str = \" \".join((f\"{c:.8f}\" for c in coords))\n        params['Lattice'] = lattice_str\n\n        pbc_str = \" \".join(str(int(v)) for v in atoms.get_cell().pbc)\n        params['pbc'] = pbc_str\n\n    return XYZ(\n        atoms.get_atoms('local')._get_frame(),\n        params=params\n    )\n
    "},{"location":"api/io/#atomlib.io.XYZ.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> XYZ\n
    Source code in atomlib/io/xyz.py
    @staticmethod\ndef from_file(file: FileOrPath) -> XYZ:\n    logging.info(f\"Loading XYZ {file.name if hasattr(file, 'name') else file!r}...\")  # type: ignore\n\n    with open_file(file, 'r') as f:\n        try:\n            # TODO be more gracious about whitespace here\n            length = int(f.readline())\n        except ValueError:\n            raise ValueError(\"Error parsing XYZ file: Invalid length\") from None\n        except IOError as e:\n            raise IOError(f\"Error parsing XYZ file: {e}\") from None\n\n        comment = f.readline().rstrip('\\n')\n        # TODO handle if there's not a gap here\n\n        try:\n            params = ExtXYZParser(comment).parse()\n        except ValueError:\n            params = None\n\n        schema = _get_columns_from_params(params)\n\n        df = parse_whitespace_separated(f, schema, start_line=1)\n\n        # map atomic numbers -> symbols (on columns which are Int8)\n        df = df.with_columns(\n            get_sym(df.select(polars.col('symbol').cast(polars.Int8, strict=False)).to_series())\n                .fill_null(df['symbol']).alias('symbol')\n        )\n        # ensure all symbols are recognizable (this will raise ValueError if not)\n        get_elem(df['symbol'])\n\n        if length < len(df):\n            warnings.warn(f\"Warning: truncating structure of length {len(df)} \"\n                        f\"to match declared length of {length}\")\n            df = df[:length]\n        elif length > len(df):\n            warnings.warn(f\"Warning: structure length {len(df)} doesn't match \"\n                        f\"declared length {length}.\\nData could be corrupted.\")\n\n        try:\n            params = ExtXYZParser(comment).parse()\n            return XYZ(df, comment, params)\n        except ValueError:\n            pass\n\n        return XYZ(df, comment)\n
    "},{"location":"api/io/#atomlib.io.XYZ.write","title":"write","text":"
    write(file: FileOrPath, fmt: XYZFormat = 'exyz')\n
    Source code in atomlib/io/xyz.py
    def write(self, file: FileOrPath, fmt: XYZFormat = 'exyz'):\n    with open_file(file, 'w', newline='\\r\\n') as f:\n\n        f.write(f\"{len(self.atoms)}\\n\")\n        if len(self.params) > 0 and fmt == 'exyz':\n            f.write(\" \".join(_param_strings(self.params)))\n        else:\n            f.write(self.comment or \"\")\n        f.write(\"\\n\")\n\n        # not my best work\n        col_space = (3, 12, 12, 12)\n        f.writelines(\n            \"\".join(\n                f\"{val:< {space}.8f}\" if isinstance(val, float) else f\"{val:<{space}}\" for (val, space) in zip(_flatten(row), col_space)\n            ).strip() + '\\n' for row in self.atoms.select(('symbol', 'coords')).rows()\n        )\n
    "},{"location":"api/io/#atomlib.io.XYZ.cell_matrix","title":"cell_matrix","text":"
    cell_matrix() -> Optional[NDArray[float64]]\n
    Source code in atomlib/io/xyz.py
    def cell_matrix(self) -> t.Optional[NDArray[numpy.float64]]:\n    if (s := self.params.get('Lattice')) is None:\n        return None\n\n    try:\n        items = list(map(float, s.split()))\n        if not len(items) == 9:\n            raise ValueError(\"Invalid length\")\n        return numpy.array(items, dtype=numpy.float64).reshape((3, 3)).T\n    except ValueError:\n        warnings.warn(f\"Warning: Invalid format for key 'Lattice=\\\"{s}\\\"'\")\n    return None\n
    "},{"location":"api/io/#atomlib.io.XYZ.pbc","title":"pbc","text":"
    pbc() -> Optional[NDArray[bool_]]\n
    Source code in atomlib/io/xyz.py
    def pbc(self) -> t.Optional[NDArray[numpy.bool_]]:\n    if (s := self.params.get('pbc')) is None:\n        return None\n\n    val_map = {'0': False, 'f': False, '1': True, 't': True}\n    try:\n        items = [val_map[v.lower()] for v in s.split()]\n        if not len(items) == 3:\n            raise ValueError(\"Invalid length\")\n        return numpy.array(items, dtype=numpy.bool_)\n    except ValueError:\n        warnings.warn(f\"Warning: Invalid format for key 'pbc=\\\"{s}\\\"'\")\n    return None\n
    "},{"location":"api/io/#atomlib.io.XSF","title":"XSF dataclass","text":"Source code in atomlib/io/xsf.py
    @dataclass\nclass XSF:\n    periodicity: Periodicity = 'crystal'\n    primitive_cell: t.Optional[LinearTransform3D] = None\n    conventional_cell: t.Optional[LinearTransform3D] = None\n\n    prim_coords: t.Optional[polars.DataFrame] = None\n    conv_coords: t.Optional[polars.DataFrame] = None\n    atoms: t.Optional[polars.DataFrame] = None\n\n    def get_atoms(self) -> polars.DataFrame:\n        if self.prim_coords is not None:\n            return self.prim_coords\n        if self.atoms is not None:\n            return self.atoms\n        if self.conv_coords is not None:\n            raise NotImplementedError()  # TODO untransform conv_coords by conventional_cell?\n        raise ValueError(\"No coordinates specified in XSF file.\")\n\n    def get_pbc(self) -> NDArray[numpy.bool_]:\n        return _periodicity_to_pbc(self.periodicity)\n\n    @staticmethod\n    def from_cell(cell: HasAtomCell) -> XSF:\n        ortho = cell.get_transform('local', 'cell_box').to_linear()\n        return XSF(\n            primitive_cell=ortho,\n            conventional_cell=ortho,\n            prim_coords=cell.get_atoms('linear').inner,\n            periodicity=_pbc_to_periodicity(cell.pbc)\n        )\n\n    @staticmethod\n    def from_atoms(atoms: HasAtoms) -> XSF:\n        return XSF(\n            periodicity='molecule',\n            atoms=atoms.get_atoms('local').inner\n        )\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> XSF:\n        logging.info(f\"Loading XSF {file.name if hasattr(file, 'name') else file!r}...\")  # type: ignore\n        with open_file(file) as f:\n            return XSFParser(f).parse()\n\n    def __post_init__(self):\n        if self.prim_coords is None and self.conv_coords is None and self.atoms is None:\n            raise ValueError(\"Error: No coordinates are specified (atoms, primitive, or conventional).\")\n\n        if self.prim_coords is not None and self.conv_coords is not None:\n            logging.warning(\"Warning: Both 'primcoord' and 'convcoord' are specified. 'convcoord' will be ignored.\")\n        elif self.conv_coords is not None and self.conventional_cell is None:\n            raise ValueError(\"If 'convcoord' is specified, 'convvec' must be specified as well.\")\n\n        if self.periodicity == 'molecule':\n            if self.atoms is None:\n                raise ValueError(\"'atoms' must be specified for molecules.\")\n\n    def write(self, path: FileOrPath):\n        with open_file(path, 'w') as f:\n            print(self.periodicity.upper(), file=f)\n            if self.primitive_cell is not None:\n                print('PRIMVEC', file=f)\n                self._write_cell(f, self.primitive_cell)\n            if self.conventional_cell is not None:\n                print('CONVVEC', file=f)\n                self._write_cell(f, self.conventional_cell)\n            print(file=f)\n\n            if self.prim_coords is not None:\n                print(\"PRIMCOORD\", file=f)\n                print(f\"{len(self.prim_coords)} 1\", file=f)\n                self._write_coords(f, self.prim_coords)\n            if self.conv_coords is not None:\n                print(\"CONVCOORD\", file=f)\n                print(f\"{len(self.conv_coords)} 1\", file=f)\n                self._write_coords(f, self.conv_coords)\n            if self.atoms is not None:\n                print(\"ATOMS\", file=f)\n                self._write_coords(f, self.atoms)\n\n    def _write_cell(self, f: TextIOBase, cell: LinearTransform3D):\n        for row in cell.inner.T:\n            for val in row:\n                f.write(f\"{val:12.7f}\")\n            f.write('\\n')\n\n    def _write_coords(self, f: TextIOBase, coords: polars.DataFrame):\n        for (elem, [x, y, z]) in coords.select(['elem', 'coords']).rows():\n            print(f\"{elem:2d} {x:11.6f} {y:11.6f} {z:11.6f}\", file=f)\n        print(file=f)\n
    "},{"location":"api/io/#atomlib.io.XSF.periodicity","title":"periodicity class-attribute instance-attribute","text":"
    periodicity: Periodicity = 'crystal'\n
    "},{"location":"api/io/#atomlib.io.XSF.primitive_cell","title":"primitive_cell class-attribute instance-attribute","text":"
    primitive_cell: Optional[LinearTransform3D] = None\n
    "},{"location":"api/io/#atomlib.io.XSF.conventional_cell","title":"conventional_cell class-attribute instance-attribute","text":"
    conventional_cell: Optional[LinearTransform3D] = None\n
    "},{"location":"api/io/#atomlib.io.XSF.prim_coords","title":"prim_coords class-attribute instance-attribute","text":"
    prim_coords: Optional[DataFrame] = None\n
    "},{"location":"api/io/#atomlib.io.XSF.conv_coords","title":"conv_coords class-attribute instance-attribute","text":"
    conv_coords: Optional[DataFrame] = None\n
    "},{"location":"api/io/#atomlib.io.XSF.atoms","title":"atoms class-attribute instance-attribute","text":"
    atoms: Optional[DataFrame] = None\n
    "},{"location":"api/io/#atomlib.io.XSF.get_atoms","title":"get_atoms","text":"
    get_atoms() -> DataFrame\n
    Source code in atomlib/io/xsf.py
    def get_atoms(self) -> polars.DataFrame:\n    if self.prim_coords is not None:\n        return self.prim_coords\n    if self.atoms is not None:\n        return self.atoms\n    if self.conv_coords is not None:\n        raise NotImplementedError()  # TODO untransform conv_coords by conventional_cell?\n    raise ValueError(\"No coordinates specified in XSF file.\")\n
    "},{"location":"api/io/#atomlib.io.XSF.get_pbc","title":"get_pbc","text":"
    get_pbc() -> NDArray[bool_]\n
    Source code in atomlib/io/xsf.py
    def get_pbc(self) -> NDArray[numpy.bool_]:\n    return _periodicity_to_pbc(self.periodicity)\n
    "},{"location":"api/io/#atomlib.io.XSF.from_cell","title":"from_cell staticmethod","text":"
    from_cell(cell: HasAtomCell) -> XSF\n
    Source code in atomlib/io/xsf.py
    @staticmethod\ndef from_cell(cell: HasAtomCell) -> XSF:\n    ortho = cell.get_transform('local', 'cell_box').to_linear()\n    return XSF(\n        primitive_cell=ortho,\n        conventional_cell=ortho,\n        prim_coords=cell.get_atoms('linear').inner,\n        periodicity=_pbc_to_periodicity(cell.pbc)\n    )\n
    "},{"location":"api/io/#atomlib.io.XSF.from_atoms","title":"from_atoms staticmethod","text":"
    from_atoms(atoms: HasAtoms) -> XSF\n
    Source code in atomlib/io/xsf.py
    @staticmethod\ndef from_atoms(atoms: HasAtoms) -> XSF:\n    return XSF(\n        periodicity='molecule',\n        atoms=atoms.get_atoms('local').inner\n    )\n
    "},{"location":"api/io/#atomlib.io.XSF.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> XSF\n
    Source code in atomlib/io/xsf.py
    @staticmethod\ndef from_file(file: FileOrPath) -> XSF:\n    logging.info(f\"Loading XSF {file.name if hasattr(file, 'name') else file!r}...\")  # type: ignore\n    with open_file(file) as f:\n        return XSFParser(f).parse()\n
    "},{"location":"api/io/#atomlib.io.XSF.write","title":"write","text":"
    write(path: FileOrPath)\n
    Source code in atomlib/io/xsf.py
    def write(self, path: FileOrPath):\n    with open_file(path, 'w') as f:\n        print(self.periodicity.upper(), file=f)\n        if self.primitive_cell is not None:\n            print('PRIMVEC', file=f)\n            self._write_cell(f, self.primitive_cell)\n        if self.conventional_cell is not None:\n            print('CONVVEC', file=f)\n            self._write_cell(f, self.conventional_cell)\n        print(file=f)\n\n        if self.prim_coords is not None:\n            print(\"PRIMCOORD\", file=f)\n            print(f\"{len(self.prim_coords)} 1\", file=f)\n            self._write_coords(f, self.prim_coords)\n        if self.conv_coords is not None:\n            print(\"CONVCOORD\", file=f)\n            print(f\"{len(self.conv_coords)} 1\", file=f)\n            self._write_coords(f, self.conv_coords)\n        if self.atoms is not None:\n            print(\"ATOMS\", file=f)\n            self._write_coords(f, self.atoms)\n
    "},{"location":"api/io/#atomlib.io.CFG","title":"CFG dataclass","text":"Source code in atomlib/io/cfg.py
    @dataclass\nclass CFG:\n    atoms: polars.DataFrame\n\n    cell: LinearTransform3D\n    transform: t.Optional[LinearTransform3D] = None\n    eta: t.Optional[LinearTransform3D] = None\n\n    length_scale: t.Optional[float] = None\n    length_unit: t.Optional[str] = None\n    rate_scale: t.Optional[float] = None\n    rate_unit: t.Optional[str] = None\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> CFG:\n        with open_file(file, 'r') as f:\n            return CFGParser(f).parse()\n\n    @staticmethod\n    def from_atoms(atoms: HasAtoms) -> CFG:\n        if isinstance(atoms, HasAtomCell):\n            cell = atoms.get_transform('cell_box').inverse().to_linear()\n            atoms = atoms.get_atoms('cell_box')\n        else:\n            cell = LinearTransform3D.identity()\n\n        # ensure we have masses and velocities\n        atoms = atoms.with_mass().with_velocity()\n        return CFG(atoms._get_frame(), cell, length_scale=1.0, length_unit=\"Angstrom\")\n\n    def write(self, file: FileOrPath):\n        with open_file(file, 'w', newline='\\r\\n') as f:\n            f.write(f\"Number of particles = {len(self.atoms)}\\n\")\n\n            if self.length_scale is not None:\n                unit = f\" [{self.length_unit}]\" if self.length_unit is not None else \"\"\n                f.write(f\"\\nA = {self.length_scale:.8}{unit}\\n\\n\")\n\n            cell = self.cell.inner\n            for (i, j) in product(range(3), repeat=2):\n                f.write(f\"H0({i+1},{j+1}) = {cell[j,i]:.8} A\\n\")\n\n            if self.transform is not None:\n                f.write(\"\\n\")\n                transform = self.transform.inner\n                for (i, j) in product(range(3), repeat=2):\n                    f.write(f\"Transform({i+1},{j+1}) = {transform[j,i]:.8}\\n\")\n\n            if self.eta is not None:\n                f.write(\"\\n\")\n                eta = self.eta.inner\n                for i in range(3):\n                    for j in range(i, 3):\n                        f.write(f\"eta({i+1},{j+1}) = {eta[j,i]:.8}\\n\")\n\n            if self.rate_scale is not None:\n                unit = f\" [{self.rate_unit}]\" if self.rate_unit is not None else \"\"\n                f.write(f\"\\nR = {self.rate_scale:.8}{unit}\\n\")\n\n            f.write(\"\\n\")\n            for row in self.atoms.select(('mass', 'symbol', 'coords', 'velocity')).rows():\n                (mass, sym, (x, y, z), (v_x, v_y, v_z)) = row\n                f.write(f\"{mass:.4f} {sym:>2} {x:.8} {y:.8} {z:.8} {v_x:.8} {v_y:.8} {v_z:.8}\\n\")\n
    "},{"location":"api/io/#atomlib.io.CFG.atoms","title":"atoms instance-attribute","text":"
    atoms: DataFrame\n
    "},{"location":"api/io/#atomlib.io.CFG.cell","title":"cell instance-attribute","text":"
    cell: LinearTransform3D\n
    "},{"location":"api/io/#atomlib.io.CFG.transform","title":"transform class-attribute instance-attribute","text":"
    transform: Optional[LinearTransform3D] = None\n
    "},{"location":"api/io/#atomlib.io.CFG.eta","title":"eta class-attribute instance-attribute","text":"
    eta: Optional[LinearTransform3D] = None\n
    "},{"location":"api/io/#atomlib.io.CFG.length_scale","title":"length_scale class-attribute instance-attribute","text":"
    length_scale: Optional[float] = None\n
    "},{"location":"api/io/#atomlib.io.CFG.length_unit","title":"length_unit class-attribute instance-attribute","text":"
    length_unit: Optional[str] = None\n
    "},{"location":"api/io/#atomlib.io.CFG.rate_scale","title":"rate_scale class-attribute instance-attribute","text":"
    rate_scale: Optional[float] = None\n
    "},{"location":"api/io/#atomlib.io.CFG.rate_unit","title":"rate_unit class-attribute instance-attribute","text":"
    rate_unit: Optional[str] = None\n
    "},{"location":"api/io/#atomlib.io.CFG.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> CFG\n
    Source code in atomlib/io/cfg.py
    @staticmethod\ndef from_file(file: FileOrPath) -> CFG:\n    with open_file(file, 'r') as f:\n        return CFGParser(f).parse()\n
    "},{"location":"api/io/#atomlib.io.CFG.from_atoms","title":"from_atoms staticmethod","text":"
    from_atoms(atoms: HasAtoms) -> CFG\n
    Source code in atomlib/io/cfg.py
    @staticmethod\ndef from_atoms(atoms: HasAtoms) -> CFG:\n    if isinstance(atoms, HasAtomCell):\n        cell = atoms.get_transform('cell_box').inverse().to_linear()\n        atoms = atoms.get_atoms('cell_box')\n    else:\n        cell = LinearTransform3D.identity()\n\n    # ensure we have masses and velocities\n    atoms = atoms.with_mass().with_velocity()\n    return CFG(atoms._get_frame(), cell, length_scale=1.0, length_unit=\"Angstrom\")\n
    "},{"location":"api/io/#atomlib.io.CFG.write","title":"write","text":"
    write(file: FileOrPath)\n
    Source code in atomlib/io/cfg.py
    def write(self, file: FileOrPath):\n    with open_file(file, 'w', newline='\\r\\n') as f:\n        f.write(f\"Number of particles = {len(self.atoms)}\\n\")\n\n        if self.length_scale is not None:\n            unit = f\" [{self.length_unit}]\" if self.length_unit is not None else \"\"\n            f.write(f\"\\nA = {self.length_scale:.8}{unit}\\n\\n\")\n\n        cell = self.cell.inner\n        for (i, j) in product(range(3), repeat=2):\n            f.write(f\"H0({i+1},{j+1}) = {cell[j,i]:.8} A\\n\")\n\n        if self.transform is not None:\n            f.write(\"\\n\")\n            transform = self.transform.inner\n            for (i, j) in product(range(3), repeat=2):\n                f.write(f\"Transform({i+1},{j+1}) = {transform[j,i]:.8}\\n\")\n\n        if self.eta is not None:\n            f.write(\"\\n\")\n            eta = self.eta.inner\n            for i in range(3):\n                for j in range(i, 3):\n                    f.write(f\"eta({i+1},{j+1}) = {eta[j,i]:.8}\\n\")\n\n        if self.rate_scale is not None:\n            unit = f\" [{self.rate_unit}]\" if self.rate_unit is not None else \"\"\n            f.write(f\"\\nR = {self.rate_scale:.8}{unit}\\n\")\n\n        f.write(\"\\n\")\n        for row in self.atoms.select(('mass', 'symbol', 'coords', 'velocity')).rows():\n            (mass, sym, (x, y, z), (v_x, v_y, v_z)) = row\n            f.write(f\"{mass:.4f} {sym:>2} {x:.8} {y:.8} {z:.8} {v_x:.8} {v_y:.8} {v_z:.8}\\n\")\n
    "},{"location":"api/io/#atomlib.io.LMP","title":"LMP dataclass","text":"Source code in atomlib/io/lmp.py
    @dataclass\nclass LMP:\n    comment: t.Optional[str]\n    headers: t.Dict[str, t.Any]\n    sections: t.Tuple[LMPSection, ...]\n\n    def get_cell(self) -> Cell:\n        dims = numpy.array([\n            self.headers.get(f\"{c}lo {c}hi\", (-0.5, 0.5))\n            for c in \"xyz\"\n        ])\n        origin = dims[:, 0]\n        tilts = self.headers.get(\"xy xz yz\", (0., 0., 0.))\n\n        ortho = numpy.diag(dims[:, 1] - dims[:, 0])\n        (ortho[0, 1], ortho[0, 2], ortho[1, 2]) = tilts\n\n        return Cell.from_ortho(LinearTransform3D(ortho).translate(origin))\n\n    def get_atoms(self, type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> AtomCell:\n        if type_map is not None:\n            try:\n                type_map_df = polars.DataFrame({\n                    'type': polars.Series(type_map.keys(), dtype=polars.Int32),\n                    'elem': polars.Series(list(map(get_elem, type_map.values())), dtype=polars.UInt8),\n                    'symbol': polars.Series([get_sym(v) if isinstance(v, int) else v for v in type_map.values()], dtype=polars.Utf8),\n                })\n            except ValueError as e:\n                raise ValueError(\"Invalid type map\") from e\n        else:\n            type_map_df = None\n\n        cell = self.get_cell()\n\n        def _apply_type_labels(df: polars.DataFrame, section_name: str, labels: t.Optional[polars.DataFrame] = None) -> polars.DataFrame:\n            if labels is not None:\n                #df = df.with_columns(polars.col('type').replace(d, default=polars.col('type').cast(polars.Int32, strict=False), return_dtype=polars.Int32))\n                df = df.with_columns(polars.col('type').replace_strict(labels['symbol'], labels['type'], default=polars.col('type').cast(polars.Int32, strict=False), return_dtype=polars.Int32))\n                if df['type'].is_null().any():\n                    raise ValueError(f\"While parsing section {section_name}: Unknown atom label or invalid atom type\")\n            try:\n                return df.with_columns(polars.col('type').cast(polars.Int32))\n            except polars.ComputeError:\n                raise ValueError(f\"While parsing section {section_name}: Invalid atom type(s)\")\n\n        atoms: t.Optional[polars.DataFrame] = None\n        labels: t.Optional[polars.DataFrame] = None\n        masses: t.Optional[polars.DataFrame] = None\n        velocities = None\n\n        for section in self.sections:\n            start_line = section.start_line + 1\n\n            if section.name == 'Atoms':\n                if section.style not in (None, 'atomic'):\n                    # TODO support other styles\n                    raise ValueError(f\"Only 'atomic' atom_style is supported, instead got '{section.style}'\")\n\n                atoms = parse_whitespace_separated(section.body, {\n                    'i': polars.Int64, 'type': polars.Utf8,\n                    'coords': polars.Array(polars.Float64, 3),\n                }, start_line=start_line)\n                atoms = _apply_type_labels(atoms, 'Atoms', labels)\n            elif section.name == 'Atom Type Labels':\n                labels = parse_whitespace_separated(section.body, {'type': polars.Int32, 'symbol': polars.Utf8}, start_line=start_line)\n            elif section.name == 'Masses':\n                masses = parse_whitespace_separated(section.body, {'type': polars.Utf8, 'mass': polars.Float64}, start_line=start_line)\n                masses = _apply_type_labels(masses, 'Masses', labels)\n            elif section.name == 'Velocities':\n                velocities = parse_whitespace_separated(section.body, {\n                    'i': polars.Int64, 'velocity': polars.Array(polars.Float64, 3),\n                }, start_line=start_line)\n\n        # now all 'type's should be in Int32\n\n        if atoms is None:\n            if self.headers['atoms'] > 0:\n                raise ValueError(\"Missing required section 'Atoms'\")\n            return AtomCell(Atoms.empty(), cell=cell, frame='local')\n\n        # next we need to assign element symbols\n        # first, if type_map is specified, use that:\n        #if type_map_elem is not None and type_map_sym is not None:\n        if type_map_df is not None:\n            try:\n                atoms = checked_left_join(atoms, type_map_df, on='type')\n            except CheckedJoinError as e:\n                raise ValueError(f\"Missing type_map specification for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n        elif labels is not None:\n            try:\n                labels = labels.with_columns(get_elem(labels['symbol']))\n            except ValueError as e:\n                raise ValueError(\"Failed to auto-detect elements from type labels. Please pass 'type_map' explicitly\") from e\n            try:\n                atoms = checked_left_join(atoms, labels, 'type')\n            except CheckedJoinError as e:\n                raise ValueError(f\"Missing labels for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n        # otherwise we have no way\n        else:\n            raise ValueError(\"Failed to auto-detect elements from type labels. Please pass 'type_map' explicitly\")\n\n        if velocities is not None:\n            # join velocities\n            try:\n                # TODO use join_asof here?\n                atoms = checked_left_join(atoms, velocities, 'i')\n            except CheckedJoinError as e:\n                raise ValueError(f\"Missing velocities for {len(e.missing_keys)}/{len(atoms)} atoms\")\n\n        if masses is not None:\n            # join masses\n            try:\n                atoms = checked_left_join(atoms, masses, 'type')\n            except CheckedJoinError as e:\n                raise ValueError(f\"Missing masses for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n\n        return AtomCell(atoms, cell=cell, frame='local')\n\n    @staticmethod\n    def from_atoms(atoms: HasAtoms) -> LMP:\n        if isinstance(atoms, HasAtomCell):\n            # we're basically converting everything to the ortho frame, but including the affine shift\n\n            # transform affine shift into ortho frame\n            origin = atoms.get_transform('ortho', 'local').to_linear().round_near_zero() \\\n                .transform(atoms.get_cell().affine.translation())\n\n            # get the orthogonalization transform only, without affine\n            ortho = atoms.get_transform('ortho', 'cell_box').to_linear().round_near_zero().inner\n\n            # get atoms in ortho frame, and then add the affine shift\n            frame = atoms.get_atoms('ortho').transform_atoms(AffineTransform3D.translate(origin)) \\\n                .round_near_zero().with_type()\n        else:\n            bbox = atoms.bbox_atoms()\n            ortho = numpy.diag(bbox.size)\n            origin = bbox.min\n\n            frame = atoms.get_atoms('local').with_type()\n\n        types = frame.unique(subset='type')\n        types = types.with_mass().sort('type')\n\n        now = localtime()\n        comment = f\"# Generated by atomlib on {now.isoformat(' ', 'seconds')}\"\n\n        headers = {}\n        sections = []\n\n        headers['atoms'] = len(frame)\n        headers['atom types'] = len(types)\n\n        for (s, low, diff) in zip(('x', 'y', 'z'), origin, ortho.diagonal()):\n            headers[f\"{s}lo {s}hi\"] = (low, low + diff)\n\n        headers['xy xz yz'] = (ortho[0, 1], ortho[0, 2], ortho[1, 2])\n\n        body = [\n            f\" {ty:8} {sym:>4}\\n\"\n            for (ty, sym) in types.select('type', 'symbol').rows()\n        ]\n        sections.append(LMPSection(\"Atom Type Labels\", tuple(body)))\n\n        if 'mass' in types:\n            body = [\n                f\" {ty:8} {mass:14.7f}  # {sym}\\n\"\n                for (ty, sym, mass) in types.select(('type', 'symbol', 'mass')).rows()\n            ]\n            sections.append(LMPSection(\"Masses\", tuple(body)))\n\n        body = [\n            f\" {i+1:8} {ty:4} {x:14.7f} {y:14.7f} {z:14.7f}\\n\"\n            for (i, (ty, (x, y, z))) in enumerate(frame.select(('type', 'coords')).rows())\n        ]\n        sections.append(LMPSection(\"Atoms\", tuple(body), 'atomic'))\n\n        if (velocities := frame.velocities()) is not None:\n            body = [\n                f\" {i+1:8} {v_x:14.7f} {v_y:14.7f} {v_z:14.7f}\\n\"\n                for (i, (v_x, v_y, v_z)) in enumerate(velocities)\n            ]\n            sections.append(LMPSection(\"Velocities\", tuple(body)))\n\n        return LMP(comment, headers, tuple(sections))\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> LMP:\n        with open_file(file, 'r') as f:\n            return LMPReader(f).parse()\n\n    def write(self, file: FileOrPath):\n        with open_file(file, 'w') as f:\n            print((self.comment or \"\") + '\\n', file=f)\n\n            # print headers\n            for (name, val) in self.headers.items():\n                val = _HEADER_FMT.get(name, lambda s: f\"{s:8}\")(val)\n                print(f\" {val} {name}\", file=f)\n\n            # print sections\n            for section in self.sections:\n                line = section.name\n                if section.style is not None:\n                    line += f'  # {section.style}'\n                print(f\"\\n{line}\\n\", file=f)\n\n                f.writelines(section.body)\n
    "},{"location":"api/io/#atomlib.io.LMP.comment","title":"comment instance-attribute","text":"
    comment: Optional[str]\n
    "},{"location":"api/io/#atomlib.io.LMP.headers","title":"headers instance-attribute","text":"
    headers: Dict[str, Any]\n
    "},{"location":"api/io/#atomlib.io.LMP.sections","title":"sections instance-attribute","text":"
    sections: Tuple[LMPSection, ...]\n
    "},{"location":"api/io/#atomlib.io.LMP.get_cell","title":"get_cell","text":"
    get_cell() -> Cell\n
    Source code in atomlib/io/lmp.py
    def get_cell(self) -> Cell:\n    dims = numpy.array([\n        self.headers.get(f\"{c}lo {c}hi\", (-0.5, 0.5))\n        for c in \"xyz\"\n    ])\n    origin = dims[:, 0]\n    tilts = self.headers.get(\"xy xz yz\", (0., 0., 0.))\n\n    ortho = numpy.diag(dims[:, 1] - dims[:, 0])\n    (ortho[0, 1], ortho[0, 2], ortho[1, 2]) = tilts\n\n    return Cell.from_ortho(LinearTransform3D(ortho).translate(origin))\n
    "},{"location":"api/io/#atomlib.io.LMP.get_atoms","title":"get_atoms","text":"
    get_atoms(\n    type_map: Optional[Dict[int, Union[str, int]]] = None\n) -> AtomCell\n
    Source code in atomlib/io/lmp.py
    def get_atoms(self, type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> AtomCell:\n    if type_map is not None:\n        try:\n            type_map_df = polars.DataFrame({\n                'type': polars.Series(type_map.keys(), dtype=polars.Int32),\n                'elem': polars.Series(list(map(get_elem, type_map.values())), dtype=polars.UInt8),\n                'symbol': polars.Series([get_sym(v) if isinstance(v, int) else v for v in type_map.values()], dtype=polars.Utf8),\n            })\n        except ValueError as e:\n            raise ValueError(\"Invalid type map\") from e\n    else:\n        type_map_df = None\n\n    cell = self.get_cell()\n\n    def _apply_type_labels(df: polars.DataFrame, section_name: str, labels: t.Optional[polars.DataFrame] = None) -> polars.DataFrame:\n        if labels is not None:\n            #df = df.with_columns(polars.col('type').replace(d, default=polars.col('type').cast(polars.Int32, strict=False), return_dtype=polars.Int32))\n            df = df.with_columns(polars.col('type').replace_strict(labels['symbol'], labels['type'], default=polars.col('type').cast(polars.Int32, strict=False), return_dtype=polars.Int32))\n            if df['type'].is_null().any():\n                raise ValueError(f\"While parsing section {section_name}: Unknown atom label or invalid atom type\")\n        try:\n            return df.with_columns(polars.col('type').cast(polars.Int32))\n        except polars.ComputeError:\n            raise ValueError(f\"While parsing section {section_name}: Invalid atom type(s)\")\n\n    atoms: t.Optional[polars.DataFrame] = None\n    labels: t.Optional[polars.DataFrame] = None\n    masses: t.Optional[polars.DataFrame] = None\n    velocities = None\n\n    for section in self.sections:\n        start_line = section.start_line + 1\n\n        if section.name == 'Atoms':\n            if section.style not in (None, 'atomic'):\n                # TODO support other styles\n                raise ValueError(f\"Only 'atomic' atom_style is supported, instead got '{section.style}'\")\n\n            atoms = parse_whitespace_separated(section.body, {\n                'i': polars.Int64, 'type': polars.Utf8,\n                'coords': polars.Array(polars.Float64, 3),\n            }, start_line=start_line)\n            atoms = _apply_type_labels(atoms, 'Atoms', labels)\n        elif section.name == 'Atom Type Labels':\n            labels = parse_whitespace_separated(section.body, {'type': polars.Int32, 'symbol': polars.Utf8}, start_line=start_line)\n        elif section.name == 'Masses':\n            masses = parse_whitespace_separated(section.body, {'type': polars.Utf8, 'mass': polars.Float64}, start_line=start_line)\n            masses = _apply_type_labels(masses, 'Masses', labels)\n        elif section.name == 'Velocities':\n            velocities = parse_whitespace_separated(section.body, {\n                'i': polars.Int64, 'velocity': polars.Array(polars.Float64, 3),\n            }, start_line=start_line)\n\n    # now all 'type's should be in Int32\n\n    if atoms is None:\n        if self.headers['atoms'] > 0:\n            raise ValueError(\"Missing required section 'Atoms'\")\n        return AtomCell(Atoms.empty(), cell=cell, frame='local')\n\n    # next we need to assign element symbols\n    # first, if type_map is specified, use that:\n    #if type_map_elem is not None and type_map_sym is not None:\n    if type_map_df is not None:\n        try:\n            atoms = checked_left_join(atoms, type_map_df, on='type')\n        except CheckedJoinError as e:\n            raise ValueError(f\"Missing type_map specification for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n    elif labels is not None:\n        try:\n            labels = labels.with_columns(get_elem(labels['symbol']))\n        except ValueError as e:\n            raise ValueError(\"Failed to auto-detect elements from type labels. Please pass 'type_map' explicitly\") from e\n        try:\n            atoms = checked_left_join(atoms, labels, 'type')\n        except CheckedJoinError as e:\n            raise ValueError(f\"Missing labels for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n    # otherwise we have no way\n    else:\n        raise ValueError(\"Failed to auto-detect elements from type labels. Please pass 'type_map' explicitly\")\n\n    if velocities is not None:\n        # join velocities\n        try:\n            # TODO use join_asof here?\n            atoms = checked_left_join(atoms, velocities, 'i')\n        except CheckedJoinError as e:\n            raise ValueError(f\"Missing velocities for {len(e.missing_keys)}/{len(atoms)} atoms\")\n\n    if masses is not None:\n        # join masses\n        try:\n            atoms = checked_left_join(atoms, masses, 'type')\n        except CheckedJoinError as e:\n            raise ValueError(f\"Missing masses for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n\n    return AtomCell(atoms, cell=cell, frame='local')\n
    "},{"location":"api/io/#atomlib.io.LMP.from_atoms","title":"from_atoms staticmethod","text":"
    from_atoms(atoms: HasAtoms) -> LMP\n
    Source code in atomlib/io/lmp.py
    @staticmethod\ndef from_atoms(atoms: HasAtoms) -> LMP:\n    if isinstance(atoms, HasAtomCell):\n        # we're basically converting everything to the ortho frame, but including the affine shift\n\n        # transform affine shift into ortho frame\n        origin = atoms.get_transform('ortho', 'local').to_linear().round_near_zero() \\\n            .transform(atoms.get_cell().affine.translation())\n\n        # get the orthogonalization transform only, without affine\n        ortho = atoms.get_transform('ortho', 'cell_box').to_linear().round_near_zero().inner\n\n        # get atoms in ortho frame, and then add the affine shift\n        frame = atoms.get_atoms('ortho').transform_atoms(AffineTransform3D.translate(origin)) \\\n            .round_near_zero().with_type()\n    else:\n        bbox = atoms.bbox_atoms()\n        ortho = numpy.diag(bbox.size)\n        origin = bbox.min\n\n        frame = atoms.get_atoms('local').with_type()\n\n    types = frame.unique(subset='type')\n    types = types.with_mass().sort('type')\n\n    now = localtime()\n    comment = f\"# Generated by atomlib on {now.isoformat(' ', 'seconds')}\"\n\n    headers = {}\n    sections = []\n\n    headers['atoms'] = len(frame)\n    headers['atom types'] = len(types)\n\n    for (s, low, diff) in zip(('x', 'y', 'z'), origin, ortho.diagonal()):\n        headers[f\"{s}lo {s}hi\"] = (low, low + diff)\n\n    headers['xy xz yz'] = (ortho[0, 1], ortho[0, 2], ortho[1, 2])\n\n    body = [\n        f\" {ty:8} {sym:>4}\\n\"\n        for (ty, sym) in types.select('type', 'symbol').rows()\n    ]\n    sections.append(LMPSection(\"Atom Type Labels\", tuple(body)))\n\n    if 'mass' in types:\n        body = [\n            f\" {ty:8} {mass:14.7f}  # {sym}\\n\"\n            for (ty, sym, mass) in types.select(('type', 'symbol', 'mass')).rows()\n        ]\n        sections.append(LMPSection(\"Masses\", tuple(body)))\n\n    body = [\n        f\" {i+1:8} {ty:4} {x:14.7f} {y:14.7f} {z:14.7f}\\n\"\n        for (i, (ty, (x, y, z))) in enumerate(frame.select(('type', 'coords')).rows())\n    ]\n    sections.append(LMPSection(\"Atoms\", tuple(body), 'atomic'))\n\n    if (velocities := frame.velocities()) is not None:\n        body = [\n            f\" {i+1:8} {v_x:14.7f} {v_y:14.7f} {v_z:14.7f}\\n\"\n            for (i, (v_x, v_y, v_z)) in enumerate(velocities)\n        ]\n        sections.append(LMPSection(\"Velocities\", tuple(body)))\n\n    return LMP(comment, headers, tuple(sections))\n
    "},{"location":"api/io/#atomlib.io.LMP.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> LMP\n
    Source code in atomlib/io/lmp.py
    @staticmethod\ndef from_file(file: FileOrPath) -> LMP:\n    with open_file(file, 'r') as f:\n        return LMPReader(f).parse()\n
    "},{"location":"api/io/#atomlib.io.LMP.write","title":"write","text":"
    write(file: FileOrPath)\n
    Source code in atomlib/io/lmp.py
    def write(self, file: FileOrPath):\n    with open_file(file, 'w') as f:\n        print((self.comment or \"\") + '\\n', file=f)\n\n        # print headers\n        for (name, val) in self.headers.items():\n            val = _HEADER_FMT.get(name, lambda s: f\"{s:8}\")(val)\n            print(f\" {val} {name}\", file=f)\n\n        # print sections\n        for section in self.sections:\n            line = section.name\n            if section.style is not None:\n                line += f'  # {section.style}'\n            print(f\"\\n{line}\\n\", file=f)\n\n            f.writelines(section.body)\n
    "},{"location":"api/io/#atomlib.io.write_mslice","title":"write_mslice","text":"
    write_mslice(\n    cell: HasAtomCell,\n    f: BinaryFileOrPath,\n    template: Optional[MSliceFile] = None,\n    *,\n    slice_thickness: Optional[float] = None,\n    scan_points: Optional[ArrayLike] = None,\n    scan_extent: Optional[ArrayLike] = None,\n    noise_sigma: Optional[float] = None,\n    conv_angle: Optional[float] = None,\n    energy: Optional[float] = None,\n    defocus: Optional[float] = None,\n    tilt: Optional[Tuple[float, float]] = None,\n    tds: Optional[bool] = None,\n    n_cells: Optional[ArrayLike] = None\n)\n

    Write a structure to an mslice file. The structure must be orthogonal and aligned with the local coordinate system. It should be periodic in X and Y.

    template may be a file, path, or ElementTree containing an existing mslice file. Its structure will be modified to make the final output. If not specified, a default template will be used.

    Additional options modify simulation properties. If an option is not specified, the template's properties are used.

    Source code in atomlib/io/mslice.py
    def write_mslice(cell: HasAtomCell, f: BinaryFileOrPath, template: t.Optional[MSliceFile] = None, *,\n                 slice_thickness: t.Optional[float] = None,  # angstrom\n                 scan_points: t.Optional[ArrayLike] = None,\n                 scan_extent: t.Optional[ArrayLike] = None,\n                 noise_sigma: t.Optional[float] = None,  # angstrom\n                 conv_angle: t.Optional[float] = None,  # mrad\n                 energy: t.Optional[float] = None,  # keV\n                 defocus: t.Optional[float] = None,  # angstrom\n                 tilt: t.Optional[t.Tuple[float, float]] = None,  # (mrad, mrad)\n                 tds: t.Optional[bool] = None,\n                 n_cells: t.Optional[ArrayLike] = None):\n    \"\"\"\n    Write a structure to an mslice file. The structure must be orthogonal and aligned\n    with the local coordinate system. It should be periodic in X and Y.\n\n    ``template`` may be a file, path, or ElementTree containing an existing mslice file.\n    Its structure will be modified to make the final output. If not specified, a default\n    template will be used.\n\n    Additional options modify simulation properties. If an option is not specified, the\n    template's properties are used.\n    \"\"\"\n    #if not issubclass(type(cell), HasAtomCell):\n    #    raise TypeError(\"mslice format requires an AtomCell.\")\n\n    if not cell.is_orthogonal_in_local():\n        raise ValueError(\"mslice requires an orthogonal AtomCell.\")\n\n    if not numpy.all(cell.pbc[:2]):\n        warn(\"AtomCell may not be periodic\", UserWarning, stacklevel=2)\n\n    box_size = cell._box_size_in_local()\n\n    # get atoms in local frame (which we verified aligns with the cell's axes)\n    # then scale into fractional coordinates\n    atoms = cell.get_atoms('linear') \\\n        .transform(AffineTransform3D.scale(1/box_size)) \\\n        .with_wobble().with_occupancy()\n\n    out: ElementTree\n    if template is None:\n        out = default_template()\n    elif not isinstance(template, ElementTree):\n        with open_file(template, 'r') as temp:\n            out = et.parse(temp, None)\n    else:\n        out = deepcopy(template)\n\n    # TODO clean up this code\n    db: t.Optional[Element] = out.getroot() if out.getroot().tag == 'database' else out.find(\"./database\", None)\n    if db is None:\n        raise ValueError(\"Couldn't find 'database' tag in template.\")\n\n    struct = db.find(\".//object[@type='STRUCTURE']\", None)\n    if struct is None:\n        raise ValueError(\"Couldn't find STRUCTURE object in template.\")\n\n    params = db.find(\".//object[@type='SIMPARAMETERS']\", None)\n    if params is None:\n        raise ValueError(\"Couldn't find SIMPARAMETERS object in template.\")\n\n    microscope = db.find(\".//object[@type='MICROSCOPE']\", None)\n    if microscope is None:\n        raise ValueError(\"Couldn't find MICROSCOPE object in template.\")\n\n    scan = db.find(\".//object[@type='SCAN']\", None)\n    aberrations = db.findall(\".//object[@type='ABERRATION']\", None)\n\n    def set_attr(struct: Element, name: str, type: str, val: str):\n        node = t.cast(t.Optional[Element], struct.find(f\".//attribute[@name='{name}']\", None))\n        if node is None:\n            node = t.cast(Element, et.Element('attribute', dict(name=name, type=type), None))\n            struct.append(node)\n        else:\n            node.attrib['type'] = type\n        node.text = val  # type: ignore\n\n    def parse_xml_object(obj: Element) -> t.Dict[str, t.Any]:\n        \"\"\"Parse the attributes of a passed XML object.\"\"\"\n        params = {}\n        for attr in obj.iterchildren(None):\n            if attr.tag == 'attribute':\n                params[attr.attrib['name']] = convert_xml_value(attr.text, attr.attrib['type'])\n            elif attr.tag == 'relationship':\n                # todo give this a better API\n                if 'idrefs' in attr.attrib:\n                    params[f\"{attr.attrib['name']}ID\"] = attr.attrib['idrefs']\n        return params\n\n    # TODO how to store atoms in unexploded form\n    (n_a, n_b, n_c) = map(str, (1, 1, 1) if n_cells is None else numpy.asarray(n_cells).astype(int))\n    set_attr(struct, 'repeata', 'int16', n_a)\n    set_attr(struct, 'repeatb', 'int16', n_b)\n    set_attr(struct, 'repeatc', 'int16', n_c)\n\n    (a, b, c) = map(lambda v: f\"{v:.8g}\", box_size)\n    set_attr(struct, 'aparam', 'float', a)\n    set_attr(struct, 'bparam', 'float', b)\n    set_attr(struct, 'cparam', 'float', c)\n\n    if tilt is not None:\n        (tiltx, tilty) = tilt\n        set_attr(struct, 'tiltx', 'float', f\"{tiltx:.4g}\")\n        set_attr(struct, 'tilty', 'float', f\"{tilty:.4g}\")\n\n    if slice_thickness is not None:\n        set_attr(params, 'slicethickness', 'float', f\"{float(slice_thickness):.8g}\")\n    if tds is not None:\n        set_attr(params, 'includetds', 'bool', str(int(bool(tds))))\n    if conv_angle is not None:\n        set_attr(microscope, 'aperture', 'float', f\"{float(conv_angle):.8g}\")\n    if energy is not None:\n        set_attr(microscope, 'kv', 'float', f\"{float(energy):.8g}\")\n    if noise_sigma is not None:\n        if scan is None:\n            raise ValueError(\"New scan specification required for 'noise_sigma'.\")\n        set_attr(scan, 'noise_sigma', 'float', f\"{float(noise_sigma):.8g}\")\n\n    if defocus is not None:\n        for aberration in aberrations:\n            obj = parse_xml_object(aberration)\n            if obj['n'] == 1 and obj['m'] == 0:\n                set_attr(aberration, 'cnma', 'float', f\"{float(defocus):.8g}\")  # A, + is over\n                set_attr(aberration, 'cnmb', 'float', \"0.0\")\n                break\n        else:\n            raise ValueError(\"Couldn't find defocus aberration to modify.\")\n\n    if scan_points is not None:\n        (nx, ny) = numpy.broadcast_to(scan_points, 2,).astype(int)\n        if scan is not None:\n            set_attr(scan, 'nx', 'int16', str(nx))\n            set_attr(scan, 'ny', 'int16', str(ny))\n        else:\n            set_attr(params, 'numscanx', 'int16', str(nx))\n            set_attr(params, 'numscany', 'int16', str(ny))\n\n    if scan_extent is not None:\n        scan_extent = numpy.asarray(scan_extent, dtype=float)\n        try:\n            if scan_extent.ndim < 2:\n                if not scan_extent.shape == (4,):\n                    scan_extent = numpy.broadcast_to(scan_extent, (2,))\n                    scan_extent = numpy.stack(((0., 0.), scan_extent), axis=-1)\n            else:\n                scan_extent = numpy.broadcast_to(scan_extent, (2, 2))\n        except ValueError as e:\n            raise ValueError(f\"Invalid scan_extent '{scan_extent}'. Expected an array of shape (2,), (4,), or (2, 2).\") from e\n\n        if scan is not None:\n            names = ('x_i', 'x_f', 'y_i', 'y_f')\n            elem = scan\n        else:\n            names = ('intx', 'finx', 'inty', 'finy')\n            elem = params\n\n        for (name, val) in zip(names, scan_extent.ravel()):\n            set_attr(elem, name, 'float', f\"{float(val):.8g}\")\n\n    # remove existing atoms\n    for elem in db.findall(\"./object[@type='STRUCTUREATOM']\", None):\n        db.remove(elem)\n\n    # <u^2> -> 1d sigma\n    atoms = atoms.with_wobble((polars.col('wobble') / 3.).sqrt())\n    rows = atoms.select(('elem', 'coords', 'wobble', 'frac_occupancy')).rows()\n    for (i, (elem, (x, y, z), wobble, frac_occupancy)) in enumerate(rows):\n        e = _atom_elem(i, elem, x, y, z, wobble, frac_occupancy)\n        db.append(e)\n\n    et.indent(db, space=\"    \", level=0)  # type: ignore\n\n    with open_file_binary(f, 'w') as f:\n        doctype = b\"\"\"<!DOCTYPE database SYSTEM \"file:///System/Library/DTDs/CoreData.dtd\">\\n\"\"\"\n        out.write(f, encoding='UTF-8', xml_declaration=True, standalone=True, doctype=doctype)  # type: ignore\n        f.write(b'\\n')\n
    "},{"location":"api/io/#atomlib.io.write_qe","title":"write_qe","text":"
    write_qe(\n    atomcell: HasAtomCell,\n    f: FileOrPath,\n    pseudo: Optional[Mapping[str, str]] = None,\n)\n

    Write a structure to a Quantum Espresso pw.x file.

    PARAMETER DESCRIPTION atomcell

    Structure to write

    TYPE: HasAtomCell

    f

    File or path to write to

    TYPE: FileOrPath

    pseudo

    Mapping from atom symbol

    TYPE: Optional[Mapping[str, str]] DEFAULT: None

    Source code in atomlib/io/qe.py
    def write_qe(atomcell: HasAtomCell, f: FileOrPath, pseudo: t.Optional[t.Mapping[str, str]] = None):\n    \"\"\"\n    Write a structure to a Quantum Espresso pw.x file.\n\n    Args:\n      atomcell: Structure to write\n      f: File or path to write to\n      pseudo: Mapping from atom symbol\n    \"\"\"\n    if not isinstance(atomcell, HasAtomCell):\n        raise TypeError(\"'qe' format requires an AtomCell.\")\n\n    atoms = atomcell.wrap().get_atoms('cell_box').with_mass()\n\n    types = atoms.select(('symbol', 'mass')).unique(subset='symbol').sort('mass')\n    if pseudo is not None:\n        types = types.with_columns(_get_symbol_mapping(types, pseudo, ty=polars.Utf8).alias('pot'))\n    else:\n        types = types.with_columns((polars.col('symbol') + polars.lit('.UPF')).alias('pot'))\n        #types = types.with_columns(polars.col('symbol').apply(lambda sym: f\"{sym}.UPF\").alias('pot'))\n\n    with open_file(f, 'w') as f:\n        print(f\"\"\"\\\n&SYSTEM\n  ibrav=0,\n  nat={len(atoms)},\n  ntyp={len(types)}\n/\"\"\", file=f)\n\n        ortho = atomcell.get_transform('local', 'cell_box').to_linear().inner\n        print(\"\\nCELL_PARAMETERS angstrom\", file=f)\n        for row in ortho.T:\n            print(f\"  {row[0]:12.8f} {row[1]:12.8f} {row[2]:12.8f}\", file=f)\n\n        print(\"\\nATOMIC_SPECIES\", file=f)\n        for (symbol, mass, pot) in types.select(('symbol', 'mass', 'pot')).rows():\n            print(f\"{symbol:>4} {mass:10.3f}  {pot}\", file=f)\n\n        print(\"\\nATOMIC_POSITIONS crystal\", file=f)\n        for (symbol, (x, y, z)) in atoms.select(('symbol', 'coords')).rows():\n            print(f\"{symbol:>4} {x:.8f} {y:.8f} {z:.8f}\", file=f)\n\n        print(file=f)  # allows for easy concatenation\n
    "},{"location":"api/io/#atomlib.io.read_cif","title":"read_cif","text":"
    read_cif(\n    f: Union[FileOrPath, CIF, CIFDataBlock],\n    block: Union[int, str, None] = None,\n) -> HasAtoms\n

    Read a structure from a CIF file.

    If block is specified, read data from the given block of the CIF file (index or name).

    Source code in atomlib/io/__init__.py
    def read_cif(f: t.Union[FileOrPath, CIF, CIFDataBlock], block: t.Union[int, str, None] = None) -> HasAtoms:\n    \"\"\"\n    Read a structure from a CIF file.\n\n    If `block` is specified, read data from the given block of the CIF file (index or name).\n    \"\"\"\n\n    if isinstance(f, (CIF, CIFDataBlock)):\n        cif = f\n    else:\n        cif = CIF.from_file(f)\n\n    if isinstance(cif, CIF):\n        if len(cif) == 0:\n            raise ValueError(\"No data present in CIF file.\")\n        if block is None:\n            if len(cif) > 1:\n                logging.warning(\"Multiple blocks present in CIF file. Defaulting to reading first block.\")\n            cif = cif.data_blocks[0]\n        else:\n            cif = cif.get_block(block)\n\n    logging.debug(\"cif data: %r\", cif.data_dict)\n\n    # TODO: support atom_site_Cartn_[xyz]\n    df = cif.stack_tags('atom_site_fract_x', 'atom_site_fract_y', 'atom_site_fract_z',\n                        'atom_site_type_symbol', 'atom_site_label', 'atom_site_occupancy',\n                        'atom_site_U_iso_or_equiv', 'atom_site_B_iso_or_equiv',\n                        rename=('x', 'y', 'z', 'symbol', 'label', 'frac_occupancy', 'wobble', 'wobble_B'),\n                        required=(True, True, True, False, False, False, False, False))\n    if 'wobble_B' in df.columns:\n        if 'wobble' in df.columns:\n            raise ValueError(\"CIF file specifies both 'atom_site_U_iso_or_equiv' and 'atom_site_B_iso_or_equiv'\")\n        df = df.rename({'wobble_B': 'wobble'}) \\\n            .with_columns(polars.col('wobble') * (3./8. / numpy.pi**2))\n    if 'symbol' not in df.columns:\n        if 'label' not in df.columns:\n            raise ValueError(\"Tag 'atom_site_type_symbol' or 'atom_site_label' missing from CIF file\")\n        # infer symbol from label, insert at beginning\n        df = df.insert_column(0, get_sym(get_elem(df['label'])))\n    atoms = Atoms(df)\n\n    # parse and apply symmetry\n    sym_atoms = []\n    for sym in cif.get_symmetry():\n        sym_atoms.append(atoms.transform(sym))\n\n    #s = '\\n'.join(map(str, sym_atoms))\n    #print(f\"sym_atoms:\\n{s}\")\n    #print(f\"atoms: {atoms!s}\")\n\n    if len(sym_atoms) > 0:\n        atoms = Atoms.concat(sym_atoms)._wrap().deduplicate()\n\n    if (cell_size := cif.cell_size()) is not None:\n        cell_size = to_vec3(cell_size)\n        if (cell_angle := cif.cell_angle()) is not None:\n            # degrees to radians\n            cell_angle = to_vec3(cell_angle) * numpy.pi/180.\n        return AtomCell.from_unit_cell(atoms, cell_size, cell_angle, frame='cell_frac')\n    return Atoms(atoms)\n
    "},{"location":"api/io/#atomlib.io.write_cif","title":"write_cif","text":"
    write_cif(\n    atoms: Union[HasAtoms, CIF, CIFDataBlock], f: FileOrPath\n)\n

    Write a structure to an XSF file.

    Source code in atomlib/io/__init__.py
    def write_cif(atoms: t.Union[HasAtoms, CIF, CIFDataBlock], f: FileOrPath):\n    \"\"\"Write a structure to an XSF file.\"\"\"\n    if isinstance(atoms, (CIF, CIFDataBlock)):\n        cif = atoms\n    elif isinstance(atoms, AtomCell):\n        cif = CIF((CIFDataBlock.from_atomcell(atoms),))\n    else:\n        cif = CIF((CIFDataBlock.from_atoms(atoms),))\n\n    cif.write(f)\n
    "},{"location":"api/io/#atomlib.io.read_xyz","title":"read_xyz","text":"
    read_xyz(f: Union[FileOrPath, XYZ]) -> HasAtoms\n

    Read a structure from an XYZ file.

    Source code in atomlib/io/__init__.py
    def read_xyz(f: t.Union[FileOrPath, XYZ]) -> HasAtoms:\n    \"\"\"Read a structure from an XYZ file.\"\"\"\n    if isinstance(f, XYZ):\n        xyz = f\n    else:\n        xyz = XYZ.from_file(f)\n\n    atoms = Atoms(xyz.atoms)\n\n    if (cell_matrix := xyz.cell_matrix()) is not None:\n        cell = Cell.from_ortho(LinearTransform3D(cell_matrix), pbc=xyz.pbc())\n        return AtomCell(atoms, cell, frame='local')\n    return Atoms(atoms)\n
    "},{"location":"api/io/#atomlib.io.write_xyz","title":"write_xyz","text":"
    write_xyz(\n    atoms: Union[HasAtoms, XYZ],\n    f: FileOrPath,\n    fmt: XYZFormat = \"exyz\",\n)\n
    Source code in atomlib/io/__init__.py
    def write_xyz(atoms: t.Union[HasAtoms, XYZ], f: FileOrPath, fmt: XYZFormat = 'exyz'):\n    if not isinstance(atoms, XYZ):\n        atoms = XYZ.from_atoms(atoms)\n    atoms.write(f, fmt)\n
    "},{"location":"api/io/#atomlib.io.read_xsf","title":"read_xsf","text":"
    read_xsf(f: Union[FileOrPath, XSF]) -> HasAtoms\n

    Read a structure from a XSF file.

    Source code in atomlib/io/__init__.py
    def read_xsf(f: t.Union[FileOrPath, XSF]) -> HasAtoms:\n    \"\"\"Read a structure from a XSF file.\"\"\"\n    if isinstance(f, XSF):\n        xsf = f\n    else:\n        xsf = XSF.from_file(f)\n\n    atoms = xsf.get_atoms()\n    atoms = atoms.with_columns(get_sym(atoms['elem']))\n\n    if (primitive_cell := xsf.primitive_cell) is not None:\n        cell = Cell.from_ortho(primitive_cell, pbc=xsf.get_pbc())\n        return AtomCell(atoms, cell, frame='local')\n    return Atoms(atoms)\n
    "},{"location":"api/io/#atomlib.io.write_xsf","title":"write_xsf","text":"
    write_xsf(atoms: Union[HasAtoms, XSF], f: FileOrPath)\n

    Write a structure to an XSF file.

    Source code in atomlib/io/__init__.py
    def write_xsf(atoms: t.Union[HasAtoms, XSF], f: FileOrPath):\n    \"\"\"Write a structure to an XSF file.\"\"\"\n    if isinstance(atoms, XSF):\n        xsf = atoms\n    elif isinstance(atoms, AtomCell):\n        xsf = XSF.from_cell(atoms)\n    else:\n        xsf = XSF.from_atoms(atoms)\n\n    xsf.write(f)\n
    "},{"location":"api/io/#atomlib.io.read_cfg","title":"read_cfg","text":"
    read_cfg(f: Union[FileOrPath, CFG]) -> AtomCell\n

    Read a structure from an AtomEye CFG file.

    Source code in atomlib/io/__init__.py
    def read_cfg(f: t.Union[FileOrPath, CFG]) -> AtomCell:\n    \"\"\"Read a structure from an AtomEye CFG file.\"\"\"\n    if isinstance(f, CFG):\n        cfg = f\n    else:\n        cfg = CFG.from_file(f)\n\n    ortho = cfg.cell\n    if cfg.transform is not None:\n        ortho = cfg.transform @ ortho\n\n    if cfg.length_scale is not None:\n        ortho = ortho.scale(all=cfg.length_scale)\n\n    if cfg.eta is not None:\n        m = numpy.eye(3) + 2. * cfg.eta.inner\n        # matrix sqrt using eigenvals, eigenvecs\n        eigenvals, eigenvecs = numpy.linalg.eigh(m)\n        sqrtm = (eigenvecs * numpy.sqrt(eigenvals)) @ eigenvecs.T\n        ortho = LinearTransform3D(sqrtm) @ ortho\n\n    frame = Atoms(cfg.atoms).transform(ortho, transform_velocities=True)\n    return AtomCell.from_ortho(frame, ortho)\n
    "},{"location":"api/io/#atomlib.io.write_cfg","title":"write_cfg","text":"
    write_cfg(atoms: Union[HasAtoms, CFG], f: FileOrPath)\n

    Write a structure to an AtomEye CFG file.

    Source code in atomlib/io/__init__.py
    def write_cfg(atoms: t.Union[HasAtoms, CFG], f: FileOrPath):\n    \"\"\"Write a structure to an AtomEye CFG file.\"\"\"\n    if not isinstance(atoms, CFG):\n        atoms = CFG.from_atoms(atoms)\n    atoms.write(f)\n
    "},{"location":"api/io/#atomlib.io.read_lmp","title":"read_lmp","text":"
    read_lmp(\n    f: Union[FileOrPath, LMP],\n    type_map: Optional[Dict[int, Union[str, int]]] = None,\n) -> AtomCell\n

    Read a structure from a LAAMPS data file.

    Source code in atomlib/io/__init__.py
    def read_lmp(f: t.Union[FileOrPath, LMP], type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> AtomCell:\n    \"\"\"Read a structure from a LAAMPS data file.\"\"\"\n    if isinstance(f, LMP):\n        lmp = f\n    else:\n        lmp = LMP.from_file(f)\n\n    return lmp.get_atoms(type_map=type_map)\n
    "},{"location":"api/io/#atomlib.io.write_lmp","title":"write_lmp","text":"
    write_lmp(atoms: Union[HasAtoms, LMP], f: FileOrPath)\n

    Write a structure to a LAAMPS data file.

    Source code in atomlib/io/__init__.py
    def write_lmp(atoms: t.Union[HasAtoms, LMP], f: FileOrPath):\n    \"\"\"Write a structure to a LAAMPS data file.\"\"\"\n    if not isinstance(atoms, LMP):\n        atoms = LMP.from_atoms(atoms)\n    atoms.write(f)\n
    "},{"location":"api/io/#atomlib.io.read","title":"read","text":"
    read(\n    path: FileOrPath, ty: Optional[FileType] = None\n) -> HasAtoms\n

    Read a structure from a file.

    Currently, supported file types are 'cif', 'xyz', and 'xsf'. If no ty is specified, it is inferred from the file's extension.

    Source code in atomlib/io/__init__.py
    def read(path: FileOrPath, ty: t.Optional[FileType] = None) -> HasAtoms:\n    \"\"\"\n    Read a structure from a file.\n\n    Currently, supported file types are 'cif', 'xyz', and 'xsf'.\n    If no `ty` is specified, it is inferred from the file's extension.\n    \"\"\"\n    if ty is None:\n        if isinstance(path, (t.IO, IOBase)):\n            try:\n                name = path.name  # type: ignore\n                if name is None:\n                    raise AttributeError()\n                ext = Path(name).suffix\n            except AttributeError:\n                raise TypeError(\"read() must be passed a file-type when reading an already-open file.\") from None\n        else:\n            name = Path(path).name\n            ext = Path(path).suffix\n\n        if len(ext) == 0:\n            raise ValueError(f\"Can't infer extension for file '{name}'\")\n\n        return read(path, t.cast(FileType, ext))\n\n    ty_strip = str(ty).lstrip('.').lower()\n    try:\n        read_fn = _READ_TABLE[t.cast(FileType, ty_strip)]\n    except KeyError:\n        raise ValueError(f\"Unknown file type '{ty}'\") from None\n    if read_fn is None:\n        raise ValueError(f\"Reading is not supported for file type '{ty_strip}'\")\n    return read_fn(path)\n
    "},{"location":"api/io/#atomlib.io.write","title":"write","text":"
    write(\n    atoms: HasAtoms,\n    path: FileOrPath,\n    ty: Optional[FileType] = None,\n)\n

    Write this structure to a file.

    A file type may be specified using ty. If no ty is specified, it is inferred from the path's extension.

    Source code in atomlib/io/__init__.py
    def write(atoms: HasAtoms, path: FileOrPath, ty: t.Optional[FileType] = None):\n    \"\"\"\n    Write this structure to a file.\n\n    A file type may be specified using `ty`.\n    If no `ty` is specified, it is inferred from the path's extension.\n    \"\"\"\n\n    if ty is None:\n        if isinstance(path, (t.IO, IOBase)):\n            try:\n                name = path.name  # type: ignore\n                if name is None:\n                    raise AttributeError()\n                ext = Path(name).suffix\n            except AttributeError:\n                raise TypeError(\"write() must be passed a file-type when reading an already-open file.\") from None\n        else:\n            name = Path(path).name\n            ext = Path(path).suffix\n\n        if len(ext) == 0:\n            raise ValueError(f\"Can't infer extension for file '{name}'\")\n\n        return write(atoms, path, t.cast(FileType, ext))\n\n    ty_strip = str(ty).lstrip('.').lower()\n    try:\n        write_fn = _WRITE_TABLE[t.cast(FileType, ty_strip)]\n    except KeyError:\n        raise ValueError(f\"Unknown file type '{ty}'\") from None\n    if write_fn is None:\n        raise ValueError(f\"Writing is not supported for file type '{ty_strip}'\")\n\n    return write_fn(atoms, path)\n
    "},{"location":"api/io/cfg/","title":"atomlib.io.cfg","text":"

    IO for AtomEye's CFG file format, as described here.

    "},{"location":"api/io/cfg/#atomlib.io.cfg.TAGS","title":"TAGS module-attribute","text":"
    TAGS: FrozenSet[str] = frozenset(\n    map(\n        lambda s: lower(), (\"Number of particles\", \"A\", \"R\")\n    )\n)\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.ARRAY_TAGS","title":"ARRAY_TAGS module-attribute","text":"
    ARRAY_TAGS: FrozenSet[str] = frozenset(\n    map(lambda s: lower(), (\"H0\", \"Transform\", \"eta\"))\n)\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG","title":"CFG dataclass","text":"Source code in atomlib/io/cfg.py
    @dataclass\nclass CFG:\n    atoms: polars.DataFrame\n\n    cell: LinearTransform3D\n    transform: t.Optional[LinearTransform3D] = None\n    eta: t.Optional[LinearTransform3D] = None\n\n    length_scale: t.Optional[float] = None\n    length_unit: t.Optional[str] = None\n    rate_scale: t.Optional[float] = None\n    rate_unit: t.Optional[str] = None\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> CFG:\n        with open_file(file, 'r') as f:\n            return CFGParser(f).parse()\n\n    @staticmethod\n    def from_atoms(atoms: HasAtoms) -> CFG:\n        if isinstance(atoms, HasAtomCell):\n            cell = atoms.get_transform('cell_box').inverse().to_linear()\n            atoms = atoms.get_atoms('cell_box')\n        else:\n            cell = LinearTransform3D.identity()\n\n        # ensure we have masses and velocities\n        atoms = atoms.with_mass().with_velocity()\n        return CFG(atoms._get_frame(), cell, length_scale=1.0, length_unit=\"Angstrom\")\n\n    def write(self, file: FileOrPath):\n        with open_file(file, 'w', newline='\\r\\n') as f:\n            f.write(f\"Number of particles = {len(self.atoms)}\\n\")\n\n            if self.length_scale is not None:\n                unit = f\" [{self.length_unit}]\" if self.length_unit is not None else \"\"\n                f.write(f\"\\nA = {self.length_scale:.8}{unit}\\n\\n\")\n\n            cell = self.cell.inner\n            for (i, j) in product(range(3), repeat=2):\n                f.write(f\"H0({i+1},{j+1}) = {cell[j,i]:.8} A\\n\")\n\n            if self.transform is not None:\n                f.write(\"\\n\")\n                transform = self.transform.inner\n                for (i, j) in product(range(3), repeat=2):\n                    f.write(f\"Transform({i+1},{j+1}) = {transform[j,i]:.8}\\n\")\n\n            if self.eta is not None:\n                f.write(\"\\n\")\n                eta = self.eta.inner\n                for i in range(3):\n                    for j in range(i, 3):\n                        f.write(f\"eta({i+1},{j+1}) = {eta[j,i]:.8}\\n\")\n\n            if self.rate_scale is not None:\n                unit = f\" [{self.rate_unit}]\" if self.rate_unit is not None else \"\"\n                f.write(f\"\\nR = {self.rate_scale:.8}{unit}\\n\")\n\n            f.write(\"\\n\")\n            for row in self.atoms.select(('mass', 'symbol', 'coords', 'velocity')).rows():\n                (mass, sym, (x, y, z), (v_x, v_y, v_z)) = row\n                f.write(f\"{mass:.4f} {sym:>2} {x:.8} {y:.8} {z:.8} {v_x:.8} {v_y:.8} {v_z:.8}\\n\")\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.atoms","title":"atoms instance-attribute","text":"
    atoms: DataFrame\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.cell","title":"cell instance-attribute","text":"
    cell: LinearTransform3D\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.transform","title":"transform class-attribute instance-attribute","text":"
    transform: Optional[LinearTransform3D] = None\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.eta","title":"eta class-attribute instance-attribute","text":"
    eta: Optional[LinearTransform3D] = None\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.length_scale","title":"length_scale class-attribute instance-attribute","text":"
    length_scale: Optional[float] = None\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.length_unit","title":"length_unit class-attribute instance-attribute","text":"
    length_unit: Optional[str] = None\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.rate_scale","title":"rate_scale class-attribute instance-attribute","text":"
    rate_scale: Optional[float] = None\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.rate_unit","title":"rate_unit class-attribute instance-attribute","text":"
    rate_unit: Optional[str] = None\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> CFG\n
    Source code in atomlib/io/cfg.py
    @staticmethod\ndef from_file(file: FileOrPath) -> CFG:\n    with open_file(file, 'r') as f:\n        return CFGParser(f).parse()\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.from_atoms","title":"from_atoms staticmethod","text":"
    from_atoms(atoms: HasAtoms) -> CFG\n
    Source code in atomlib/io/cfg.py
    @staticmethod\ndef from_atoms(atoms: HasAtoms) -> CFG:\n    if isinstance(atoms, HasAtomCell):\n        cell = atoms.get_transform('cell_box').inverse().to_linear()\n        atoms = atoms.get_atoms('cell_box')\n    else:\n        cell = LinearTransform3D.identity()\n\n    # ensure we have masses and velocities\n    atoms = atoms.with_mass().with_velocity()\n    return CFG(atoms._get_frame(), cell, length_scale=1.0, length_unit=\"Angstrom\")\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.write","title":"write","text":"
    write(file: FileOrPath)\n
    Source code in atomlib/io/cfg.py
    def write(self, file: FileOrPath):\n    with open_file(file, 'w', newline='\\r\\n') as f:\n        f.write(f\"Number of particles = {len(self.atoms)}\\n\")\n\n        if self.length_scale is not None:\n            unit = f\" [{self.length_unit}]\" if self.length_unit is not None else \"\"\n            f.write(f\"\\nA = {self.length_scale:.8}{unit}\\n\\n\")\n\n        cell = self.cell.inner\n        for (i, j) in product(range(3), repeat=2):\n            f.write(f\"H0({i+1},{j+1}) = {cell[j,i]:.8} A\\n\")\n\n        if self.transform is not None:\n            f.write(\"\\n\")\n            transform = self.transform.inner\n            for (i, j) in product(range(3), repeat=2):\n                f.write(f\"Transform({i+1},{j+1}) = {transform[j,i]:.8}\\n\")\n\n        if self.eta is not None:\n            f.write(\"\\n\")\n            eta = self.eta.inner\n            for i in range(3):\n                for j in range(i, 3):\n                    f.write(f\"eta({i+1},{j+1}) = {eta[j,i]:.8}\\n\")\n\n        if self.rate_scale is not None:\n            unit = f\" [{self.rate_unit}]\" if self.rate_unit is not None else \"\"\n            f.write(f\"\\nR = {self.rate_scale:.8}{unit}\\n\")\n\n        f.write(\"\\n\")\n        for row in self.atoms.select(('mass', 'symbol', 'coords', 'velocity')).rows():\n            (mass, sym, (x, y, z), (v_x, v_y, v_z)) = row\n            f.write(f\"{mass:.4f} {sym:>2} {x:.8} {y:.8} {z:.8} {v_x:.8} {v_y:.8} {v_z:.8}\\n\")\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFGParser","title":"CFGParser","text":"Source code in atomlib/io/cfg.py
    class CFGParser:\n    def __init__(self, f: TextIOBase):\n        self.buf = LineBuffer(f)\n\n    def parse(self) -> CFG:\n        (n, value_tags, array_tags) = self.parse_tags()\n        atoms = self.parse_atoms(n)\n\n        try:\n            cell = array_tags['h0']\n        except KeyError:\n            raise ValueError(\"CFG file missing required tag 'H0'\") from None\n\n        length = value_tags.get('a')\n        length_scale = map_some(lambda t: t[0], length)\n        length_unit = map_some(lambda t: t[1], length)\n\n        rate = value_tags.get('r')\n        rate_scale = map_some(lambda t: t[0], rate)\n        rate_unit = map_some(lambda t: t[1], rate)\n\n        return CFG(\n            atoms=atoms,\n            cell=LinearTransform3D(cell),\n            transform=map_some(LinearTransform3D, array_tags.get('transform')),\n            eta=map_some(LinearTransform3D, array_tags.get('eta')),\n            length_scale=length_scale,\n            length_unit=length_unit,\n            rate_scale=rate_scale,\n            rate_unit=rate_unit,\n        )\n\n    def parse_tags(self) -> t.Tuple[int, t.Dict[str, t.Tuple[float, t.Optional[str]]], t.Dict[str, numpy.ndarray]]:\n        first = True\n\n        # tag, (value, unit)\n        n: t.Optional[int] = None\n        value_tags: t.Dict[str, t.Tuple[float, t.Optional[str]]] = {}\n        array_tags: t.Dict[str, t.List[t.List[t.Optional[float]]]] = {}\n\n        while (line := self.buf.peek_line()) is not None:\n            line = line.strip()\n            if len(line) == 0 or line.startswith(\"#\"):\n                # skip comments and blank lines\n                self.buf.next_line()\n                continue\n\n            if first:\n                if not line.lower().startswith('number of particles'):\n                    raise ValueError(\"File does not start with Number of particles.\"\n                                    \" Is this an AtomEye CFG file?\")\n\n            try:\n                tag, value = line.split('=')\n            except ValueError:\n                try:\n                    float(line.split(' ', 1)[0])\n                    # started list of atoms\n                    break\n                except ValueError:\n                    raise ValueError(f\"Expected a tag-value pair at line {self.buf.line}: '{line}'\")\n\n            tag = tag.strip()\n            value = value.strip()\n            if first:\n                try:\n                    value = int(value)\n                except ValueError:\n                    raise ValueError(f\"Invalid # of elements '{value}' at line {self.buf.line}\") from None\n                n = value\n                first = False\n                self.buf.next_line()\n                continue\n\n            if tag.lower() in TAGS:\n                try:\n                    value_tags[tag.lower()] = self.parse_value_with_unit(value)\n                except ValueError:\n                    raise ValueError(f\"Invalid value '{value}' at line {self.buf.line}\") from None\n            elif (match := re.match(r'(.+)\\((\\d+),(\\d+)\\)', tag)):\n                try:\n                    (tag, i, j) = (match[1].lower(), int(match[2]), int(match[3]))\n                    if not (0 < i <= 3 and 0 < j <= 3):\n                        raise ValueError(f\"Invalid index ({i},{j}) for tag '{tag}' at line {self.buf.line}\")\n                    if tag not in array_tags:\n                        array_tags[tag] = [[None] * 3, [None] * 3, [None] * 3]\n                    try:\n                        val = self.parse_value_with_unit(value)[0]\n                        array_tags[tag][j-1][i-1] = val\n                        if tag == 'eta':\n                            array_tags[tag][i-1][j-1] = val\n                    except ValueError:\n                        raise ValueError(f\"Invalid value '{value}' at line {self.buf.line}\") from None\n                except ValueError:\n                    raise ValueError(f\"Invalid indexes in tag '{tag}' at line {self.buf.line}\") from None\n                if tag.lower() not in ARRAY_TAGS:\n                    raise ValueError(f\"Unknown array tag '{tag}'\")\n            elif tag.lower() in ARRAY_TAGS:\n                raise ValueError(f\"Missing indexes for tag '{tag}' at line {self.buf.line}\")\n            else:\n                raise ValueError(f\"Unknown tag '{tag}' at line {self.buf.line}\")\n\n            self.buf.next_line()\n\n        if n is None:\n            raise ValueError(\"Empty CFG file\")\n\n        ndarray_tags: t.Dict[str, numpy.ndarray] = {}\n\n        for (tag, value) in array_tags.items():\n            for i in range(3):\n                for j in range(3):\n                    if value[j][i] is None:\n                        raise ValueError(f\"Tag '{tag}' missing value for index ({i+1},{j+1})\")\n            ndarray_tags[tag] = numpy.array(value)\n\n        return (n, value_tags, ndarray_tags)\n\n    def parse_value_with_unit(self, value: str) -> t.Tuple[float, t.Optional[str]]:\n        segments = value.split(maxsplit=1)\n        if len(segments) == 1:\n            return (float(value), None)\n        value, unit = map(lambda s: s.strip(), segments)\n\n        if (match := re.match(r'\\[(.+)\\]', unit)):\n            unit = str(match[1])\n        else:\n            unit = unit.split(maxsplit=1)[0]\n\n        return (float(value), unit)\n\n    def parse_atoms(self, n: int) -> polars.DataFrame:\n        df = parse_whitespace_separated(self.buf, {\n            'mass': polars.Float64, 'symbol': polars.Utf8,\n            'coords': polars.Array(polars.Float64, 3),\n            'velocity': polars.Array(polars.Float64, 3),\n        })\n        df = df.with_columns(get_elem(df['symbol'])).select(\n            'elem', 'symbol', 'coords', 'velocity', 'mass'\n        )\n\n        if n != len(df):\n            raise ValueError(f\"# of atom rows doesn't match declared number ({len(df)} vs. {n})\")\n\n        return df\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFGParser.buf","title":"buf instance-attribute","text":"
    buf = LineBuffer(f)\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFGParser.parse","title":"parse","text":"
    parse() -> CFG\n
    Source code in atomlib/io/cfg.py
    def parse(self) -> CFG:\n    (n, value_tags, array_tags) = self.parse_tags()\n    atoms = self.parse_atoms(n)\n\n    try:\n        cell = array_tags['h0']\n    except KeyError:\n        raise ValueError(\"CFG file missing required tag 'H0'\") from None\n\n    length = value_tags.get('a')\n    length_scale = map_some(lambda t: t[0], length)\n    length_unit = map_some(lambda t: t[1], length)\n\n    rate = value_tags.get('r')\n    rate_scale = map_some(lambda t: t[0], rate)\n    rate_unit = map_some(lambda t: t[1], rate)\n\n    return CFG(\n        atoms=atoms,\n        cell=LinearTransform3D(cell),\n        transform=map_some(LinearTransform3D, array_tags.get('transform')),\n        eta=map_some(LinearTransform3D, array_tags.get('eta')),\n        length_scale=length_scale,\n        length_unit=length_unit,\n        rate_scale=rate_scale,\n        rate_unit=rate_unit,\n    )\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFGParser.parse_tags","title":"parse_tags","text":"
    parse_tags() -> Tuple[\n    int,\n    Dict[str, Tuple[float, Optional[str]]],\n    Dict[str, ndarray],\n]\n
    Source code in atomlib/io/cfg.py
    def parse_tags(self) -> t.Tuple[int, t.Dict[str, t.Tuple[float, t.Optional[str]]], t.Dict[str, numpy.ndarray]]:\n    first = True\n\n    # tag, (value, unit)\n    n: t.Optional[int] = None\n    value_tags: t.Dict[str, t.Tuple[float, t.Optional[str]]] = {}\n    array_tags: t.Dict[str, t.List[t.List[t.Optional[float]]]] = {}\n\n    while (line := self.buf.peek_line()) is not None:\n        line = line.strip()\n        if len(line) == 0 or line.startswith(\"#\"):\n            # skip comments and blank lines\n            self.buf.next_line()\n            continue\n\n        if first:\n            if not line.lower().startswith('number of particles'):\n                raise ValueError(\"File does not start with Number of particles.\"\n                                \" Is this an AtomEye CFG file?\")\n\n        try:\n            tag, value = line.split('=')\n        except ValueError:\n            try:\n                float(line.split(' ', 1)[0])\n                # started list of atoms\n                break\n            except ValueError:\n                raise ValueError(f\"Expected a tag-value pair at line {self.buf.line}: '{line}'\")\n\n        tag = tag.strip()\n        value = value.strip()\n        if first:\n            try:\n                value = int(value)\n            except ValueError:\n                raise ValueError(f\"Invalid # of elements '{value}' at line {self.buf.line}\") from None\n            n = value\n            first = False\n            self.buf.next_line()\n            continue\n\n        if tag.lower() in TAGS:\n            try:\n                value_tags[tag.lower()] = self.parse_value_with_unit(value)\n            except ValueError:\n                raise ValueError(f\"Invalid value '{value}' at line {self.buf.line}\") from None\n        elif (match := re.match(r'(.+)\\((\\d+),(\\d+)\\)', tag)):\n            try:\n                (tag, i, j) = (match[1].lower(), int(match[2]), int(match[3]))\n                if not (0 < i <= 3 and 0 < j <= 3):\n                    raise ValueError(f\"Invalid index ({i},{j}) for tag '{tag}' at line {self.buf.line}\")\n                if tag not in array_tags:\n                    array_tags[tag] = [[None] * 3, [None] * 3, [None] * 3]\n                try:\n                    val = self.parse_value_with_unit(value)[0]\n                    array_tags[tag][j-1][i-1] = val\n                    if tag == 'eta':\n                        array_tags[tag][i-1][j-1] = val\n                except ValueError:\n                    raise ValueError(f\"Invalid value '{value}' at line {self.buf.line}\") from None\n            except ValueError:\n                raise ValueError(f\"Invalid indexes in tag '{tag}' at line {self.buf.line}\") from None\n            if tag.lower() not in ARRAY_TAGS:\n                raise ValueError(f\"Unknown array tag '{tag}'\")\n        elif tag.lower() in ARRAY_TAGS:\n            raise ValueError(f\"Missing indexes for tag '{tag}' at line {self.buf.line}\")\n        else:\n            raise ValueError(f\"Unknown tag '{tag}' at line {self.buf.line}\")\n\n        self.buf.next_line()\n\n    if n is None:\n        raise ValueError(\"Empty CFG file\")\n\n    ndarray_tags: t.Dict[str, numpy.ndarray] = {}\n\n    for (tag, value) in array_tags.items():\n        for i in range(3):\n            for j in range(3):\n                if value[j][i] is None:\n                    raise ValueError(f\"Tag '{tag}' missing value for index ({i+1},{j+1})\")\n        ndarray_tags[tag] = numpy.array(value)\n\n    return (n, value_tags, ndarray_tags)\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFGParser.parse_value_with_unit","title":"parse_value_with_unit","text":"
    parse_value_with_unit(\n    value: str,\n) -> Tuple[float, Optional[str]]\n
    Source code in atomlib/io/cfg.py
    def parse_value_with_unit(self, value: str) -> t.Tuple[float, t.Optional[str]]:\n    segments = value.split(maxsplit=1)\n    if len(segments) == 1:\n        return (float(value), None)\n    value, unit = map(lambda s: s.strip(), segments)\n\n    if (match := re.match(r'\\[(.+)\\]', unit)):\n        unit = str(match[1])\n    else:\n        unit = unit.split(maxsplit=1)[0]\n\n    return (float(value), unit)\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFGParser.parse_atoms","title":"parse_atoms","text":"
    parse_atoms(n: int) -> DataFrame\n
    Source code in atomlib/io/cfg.py
    def parse_atoms(self, n: int) -> polars.DataFrame:\n    df = parse_whitespace_separated(self.buf, {\n        'mass': polars.Float64, 'symbol': polars.Utf8,\n        'coords': polars.Array(polars.Float64, 3),\n        'velocity': polars.Array(polars.Float64, 3),\n    })\n    df = df.with_columns(get_elem(df['symbol'])).select(\n        'elem', 'symbol', 'coords', 'velocity', 'mass'\n    )\n\n    if n != len(df):\n        raise ValueError(f\"# of atom rows doesn't match declared number ({len(df)} vs. {n})\")\n\n    return df\n
    "},{"location":"api/io/cif/","title":"atomlib.io.cif","text":"

    IO for the CIF1.1 file format, specified here.

    "},{"location":"api/io/cif/#atomlib.io.cif.Value","title":"Value module-attribute","text":"
    Value: TypeAlias = Union[int, float, str, None]\n
    "},{"location":"api/io/cif/#atomlib.io.cif.SYMMETRY_PARSER","title":"SYMMETRY_PARSER module-attribute","text":"
    SYMMETRY_PARSER: Parser[SymmetryVec, SymmetryVec] = Parser(\n    [\n        BinaryOrUnaryOp([\"-\"], sub, False, 5),\n        BinaryOrUnaryOp([\"+\"], add, False, 5),\n        BinaryOp([\"*\"], mul, 6),\n        BinaryOp([\"/\"], truediv, 6),\n    ],\n    parse,\n)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIF","title":"CIF dataclass","text":"Source code in atomlib/io/cif.py
    @dataclass\nclass CIF:\n    data_blocks: t.Tuple[CIFDataBlock, ...]\n\n    def __post_init__(self):\n        # ensure that all data_blocks after the first have a name\n        for data_block in self.data_blocks[1:]:\n            if data_block.name is None:\n                data_block.name = \"\"\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> CIF:\n        return CIF(tuple(CIFDataBlock.from_file(file)))\n\n    def __len__(self) -> int:\n        return self.data_blocks.__len__()\n\n    def get_block(self, block: t.Union[int, str]) -> CIFDataBlock:\n        try:\n            if isinstance(block, int):\n                return self.data_blocks[block]\n            return next(b for b in self.data_blocks if b.name == block)\n        except (IndexError, StopIteration):\n            raise ValueError(f\"Couldn't find block '{block}' in CIF file. File has {len(self)} blocks.\")\n\n    def write(self, file: FileOrPath):\n        with open_file(file, 'w') as f:\n            print(\"# generated by atomlib\", file=f, end=None)\n            for data_block in self.data_blocks:\n                print(file=f)\n                data_block._write(f)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIF.data_blocks","title":"data_blocks instance-attribute","text":"
    data_blocks: Tuple[CIFDataBlock, ...]\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIF.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> CIF\n
    Source code in atomlib/io/cif.py
    @staticmethod\ndef from_file(file: FileOrPath) -> CIF:\n    return CIF(tuple(CIFDataBlock.from_file(file)))\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIF.get_block","title":"get_block","text":"
    get_block(block: Union[int, str]) -> CIFDataBlock\n
    Source code in atomlib/io/cif.py
    def get_block(self, block: t.Union[int, str]) -> CIFDataBlock:\n    try:\n        if isinstance(block, int):\n            return self.data_blocks[block]\n        return next(b for b in self.data_blocks if b.name == block)\n    except (IndexError, StopIteration):\n        raise ValueError(f\"Couldn't find block '{block}' in CIF file. File has {len(self)} blocks.\")\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIF.write","title":"write","text":"
    write(file: FileOrPath)\n
    Source code in atomlib/io/cif.py
    def write(self, file: FileOrPath):\n    with open_file(file, 'w') as f:\n        print(\"# generated by atomlib\", file=f, end=None)\n        for data_block in self.data_blocks:\n            print(file=f)\n            data_block._write(f)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock","title":"CIFDataBlock dataclass","text":"Source code in atomlib/io/cif.py
    @dataclass\nclass CIFDataBlock:\n    name: t.Optional[str]  # None: no data_ block, empty string: unnamed \"data_\"\n\n    # data (including loops) in file order\n    data: t.Tuple[t.Union[t.Tuple[str, Value], CIFTable], ...]\n\n    # data flattened into a single dictionary. Created automatically from `data`\n    data_dict: t.Dict[str, t.Union[t.List[Value], Value]] = field(init=False)\n\n    def __post_init__(self):\n        # if we raise here, make sure the object state is fine\n        self.data_dict = None  # type: ignore\n\n        data_values = {}\n\n        def _iter_data_values():\n            for d in self.data:\n                if isinstance(d, CIFTable):\n                    yield from d.data.items()\n                else:\n                    yield d\n\n        for (k, v) in _iter_data_values():\n            if k in data_values:\n                raise ValueError(f\"Duplicate key {k}\")\n            data_values[k] = v\n\n        self.data_dict = data_values\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> t.Iterator[CIFDataBlock]:\n        with open_file(file) as f:\n            yield from CifReader(f).parse()\n\n    @staticmethod\n    def from_atoms(atoms: HasAtoms) -> CIFDataBlock:\n        data: t.List[t.Union[t.Tuple[str, Value], CIFTable]] = []\n\n        data.append(('audit_creation_method', 'Generated by atomlib'))\n\n        keys: t.Sequence[t.Tuple[str, t.Union[str, polars.Expr], t.Union[str, bool]]] = (\n            # col, expr, predicate (column or boolean)\n            ('atom_site_type_symbol', 'symbol', True),\n            ('atom_site_label', 'label', 'label'),\n            ('atom_site_occupancy', 'frac_occupancy', 'frac_occupancy'),\n            ('atom_site_Cartn_x', polars.col('coords').arr.get(0), True),\n            ('atom_site_Cartn_y', polars.col('coords').arr.get(1), True),\n            ('atom_site_Cartn_z', polars.col('coords').arr.get(2), True),\n            ('atom_site_U_iso_or_equiv', 'wobble', 'wobble'),\n        )\n        data.append(CIFTable({\n            key: atoms.select(expr).to_series().to_list() for (key, expr, pred) in keys\n            if (atoms.try_get_column(pred) is not None if isinstance(pred, str) else pred)\n        }))\n\n        return CIFDataBlock(\"\", tuple(data))\n\n    @staticmethod\n    def from_atomcell(atomcell: HasAtomCell) -> CIFDataBlock:\n        atoms = atomcell.get_atoms('cell_box')\n        ortho = atomcell.get_transform('local', 'cell_box').to_linear()\n        (cell_size, cell_angle) = ortho_to_cell(ortho)\n        cell_angle *= 180./numpy.pi  # convert to degrees\n\n        data: t.List[t.Union[t.Tuple[str, Value], CIFTable]] = []\n\n        data.append(('audit_creation_method', 'Generated by atomlib'))\n\n        # symmetry information\n        data.append(CIFTable({\n            'space_group_symop_id': [1],\n            'space_group_symop_operation_xyz': ['x,y,z'],\n        }))\n\n        # cell information\n        data.append(('cell_length_a', cell_size[0]))\n        data.append(('cell_length_b', cell_size[1]))\n        data.append(('cell_length_c', cell_size[2]))\n        data.append(('cell_angle_alpha', cell_angle[0]))\n        data.append(('cell_angle_beta', cell_angle[1]))\n        data.append(('cell_angle_gamma', cell_angle[2]))\n        data.append(('cell_volume', ortho.det()))\n\n        keys: t.Sequence[t.Tuple[str, t.Union[str, polars.Expr], t.Union[str, bool]]] = (\n            # col, expr, predicate (column or boolean)\n            ('atom_site_type_symbol', 'symbol', True),\n            ('atom_site_label', 'label', 'label'),\n            ('atom_site_occupancy', 'frac_occupancy', 'frac_occupancy'),\n            ('atom_site_fract_x', polars.col('coords').arr.get(0), True),\n            ('atom_site_fract_y', polars.col('coords').arr.get(1), True),\n            ('atom_site_fract_z', polars.col('coords').arr.get(2), True),\n            ('atom_site_U_iso_or_equiv', 'wobble', 'wobble'),\n        )\n        data.append(CIFTable({\n            key: atoms.select(expr).to_series().to_list() for (key, expr, pred) in keys\n            if (atoms.try_get_column(pred) is not None if isinstance(pred, str) else pred)\n        }))\n\n        return CIFDataBlock(\"\", tuple(data))\n\n    def write(self, file: FileOrPath):\n        with open_file(file, 'w') as f:\n            self._write(f)\n\n    def _write(self, f: TextIOBase):\n        if self.name is not None:\n            print(f\"data_{self.name}\\n\", file=f)\n\n        for data in self.data:\n            if isinstance(data, CIFTable):\n                data._write(f)\n            else:\n                (name, value) = data\n                val = _format_val(value).rstrip()\n                if val.startswith(';'):\n                    # multiline string\n                    print(f\"_{name}\\n{val}\", file=f)\n                else:\n                    print(f\"_{name: <28} {_format_val(value).rstrip()}\", file=f)\n\n    def stack_tags(self, *tags: str, dtype: t.Union[str, numpy.dtype, t.Iterable[t.Union[str, numpy.dtype]], None] = None,\n                   rename: t.Optional[t.Iterable[t.Optional[str]]] = None, required: t.Union[bool, t.Iterable[bool]] = True) -> polars.DataFrame:\n        dtypes: t.Iterable[t.Optional[numpy.dtype]]\n        if dtype is None:\n            dtypes = repeat(None)\n        elif isinstance(dtype, (numpy.dtype, str)):\n            dtypes = (numpy.dtype(dtype),) * len(tags)\n        else:\n            dtypes = tuple(map(lambda ty: numpy.dtype(ty), dtype))\n            if len(dtypes) != len(tags):\n                raise ValueError(\"dtype list of invalid length\")\n\n        if isinstance(required, bool):\n            required = repeat(required)\n\n        if rename is None:\n            rename = repeat(None)\n\n        d = {}\n        for (tag, ty, req, name) in zip(tags, dtypes, required, rename):\n            if tag not in self.data_dict:\n                if req:\n                    raise ValueError(f\"Tag '{tag}' missing from CIF file\")\n                continue\n            try:\n                arr = numpy.array(self.data_dict[tag], dtype=ty)\n                d[name or tag] = arr\n            except TypeError:\n                raise TypeError(f\"Tag '{tag}' of invalid or heterogeneous type.\")\n\n        if len(d) == 0:\n            return polars.DataFrame({})\n\n        tag_len = len(next(iter(d.values())))\n        if any(len(arr) != tag_len for arr in d.values()):\n            raise ValueError(f\"Tags of mismatching lengths: {tuple(map(len, d.values()))}\")\n\n        return polars.DataFrame(d)\n\n    def cell_size(self) -> t.Optional[t.Tuple[float, float, float]]:\n        \"\"\"Return cell size (in angstroms).\"\"\"\n        try:\n            a = float(self['cell_length_a'])  # type: ignore\n            b = float(self['cell_length_b'])  # type: ignore\n            c = float(self['cell_length_c'])  # type: ignore\n            return (a, b, c)\n        except (ValueError, TypeError, KeyError):\n            return None\n\n    def cell_angle(self) -> t.Optional[t.Tuple[float, float, float]]:\n        \"\"\"Return cell angle (in degrees).\"\"\"\n        try:\n            a = float(self['cell_angle_alpha'])  # type: ignore\n            b = float(self['cell_angle_beta'])   # type: ignore\n            g = float(self['cell_angle_gamma'])  # type: ignore\n            return (a, b, g)\n        except (ValueError, TypeError, KeyError):\n            return None\n\n    def get_symmetry(self) -> t.Iterator[AffineTransform3D]:\n        syms = self.data_dict.get('space_group_symop_operation_xyz')\n        if syms is None:\n            # old name for symmetry\n            syms = self.data_dict.get('symmetry_equiv_pos_as_xyz')\n        if syms is None:\n            syms = ()\n        if not hasattr(syms, '__iter__'):\n            syms = (syms,)\n        return map(parse_symmetry, map(str, syms))  # type: ignore\n\n    def __getitem__(self, key: str) -> t.Union[Value, t.List[Value]]:\n        return self.data_dict.__getitem__(key)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.name","title":"name instance-attribute","text":"
    name: Optional[str]\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.data","title":"data instance-attribute","text":"
    data: Tuple[Union[Tuple[str, Value], CIFTable], ...]\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.data_dict","title":"data_dict class-attribute instance-attribute","text":"
    data_dict: Dict[str, Union[List[Value], Value]] = field(\n    init=False\n)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> Iterator[CIFDataBlock]\n
    Source code in atomlib/io/cif.py
    @staticmethod\ndef from_file(file: FileOrPath) -> t.Iterator[CIFDataBlock]:\n    with open_file(file) as f:\n        yield from CifReader(f).parse()\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.from_atoms","title":"from_atoms staticmethod","text":"
    from_atoms(atoms: HasAtoms) -> CIFDataBlock\n
    Source code in atomlib/io/cif.py
    @staticmethod\ndef from_atoms(atoms: HasAtoms) -> CIFDataBlock:\n    data: t.List[t.Union[t.Tuple[str, Value], CIFTable]] = []\n\n    data.append(('audit_creation_method', 'Generated by atomlib'))\n\n    keys: t.Sequence[t.Tuple[str, t.Union[str, polars.Expr], t.Union[str, bool]]] = (\n        # col, expr, predicate (column or boolean)\n        ('atom_site_type_symbol', 'symbol', True),\n        ('atom_site_label', 'label', 'label'),\n        ('atom_site_occupancy', 'frac_occupancy', 'frac_occupancy'),\n        ('atom_site_Cartn_x', polars.col('coords').arr.get(0), True),\n        ('atom_site_Cartn_y', polars.col('coords').arr.get(1), True),\n        ('atom_site_Cartn_z', polars.col('coords').arr.get(2), True),\n        ('atom_site_U_iso_or_equiv', 'wobble', 'wobble'),\n    )\n    data.append(CIFTable({\n        key: atoms.select(expr).to_series().to_list() for (key, expr, pred) in keys\n        if (atoms.try_get_column(pred) is not None if isinstance(pred, str) else pred)\n    }))\n\n    return CIFDataBlock(\"\", tuple(data))\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.from_atomcell","title":"from_atomcell staticmethod","text":"
    from_atomcell(atomcell: HasAtomCell) -> CIFDataBlock\n
    Source code in atomlib/io/cif.py
    @staticmethod\ndef from_atomcell(atomcell: HasAtomCell) -> CIFDataBlock:\n    atoms = atomcell.get_atoms('cell_box')\n    ortho = atomcell.get_transform('local', 'cell_box').to_linear()\n    (cell_size, cell_angle) = ortho_to_cell(ortho)\n    cell_angle *= 180./numpy.pi  # convert to degrees\n\n    data: t.List[t.Union[t.Tuple[str, Value], CIFTable]] = []\n\n    data.append(('audit_creation_method', 'Generated by atomlib'))\n\n    # symmetry information\n    data.append(CIFTable({\n        'space_group_symop_id': [1],\n        'space_group_symop_operation_xyz': ['x,y,z'],\n    }))\n\n    # cell information\n    data.append(('cell_length_a', cell_size[0]))\n    data.append(('cell_length_b', cell_size[1]))\n    data.append(('cell_length_c', cell_size[2]))\n    data.append(('cell_angle_alpha', cell_angle[0]))\n    data.append(('cell_angle_beta', cell_angle[1]))\n    data.append(('cell_angle_gamma', cell_angle[2]))\n    data.append(('cell_volume', ortho.det()))\n\n    keys: t.Sequence[t.Tuple[str, t.Union[str, polars.Expr], t.Union[str, bool]]] = (\n        # col, expr, predicate (column or boolean)\n        ('atom_site_type_symbol', 'symbol', True),\n        ('atom_site_label', 'label', 'label'),\n        ('atom_site_occupancy', 'frac_occupancy', 'frac_occupancy'),\n        ('atom_site_fract_x', polars.col('coords').arr.get(0), True),\n        ('atom_site_fract_y', polars.col('coords').arr.get(1), True),\n        ('atom_site_fract_z', polars.col('coords').arr.get(2), True),\n        ('atom_site_U_iso_or_equiv', 'wobble', 'wobble'),\n    )\n    data.append(CIFTable({\n        key: atoms.select(expr).to_series().to_list() for (key, expr, pred) in keys\n        if (atoms.try_get_column(pred) is not None if isinstance(pred, str) else pred)\n    }))\n\n    return CIFDataBlock(\"\", tuple(data))\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.write","title":"write","text":"
    write(file: FileOrPath)\n
    Source code in atomlib/io/cif.py
    def write(self, file: FileOrPath):\n    with open_file(file, 'w') as f:\n        self._write(f)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.stack_tags","title":"stack_tags","text":"
    stack_tags(\n    *tags: str,\n    dtype: Union[\n        str, dtype, Iterable[Union[str, dtype]], None\n    ] = None,\n    rename: Optional[Iterable[Optional[str]]] = None,\n    required: Union[bool, Iterable[bool]] = True\n) -> DataFrame\n
    Source code in atomlib/io/cif.py
    def stack_tags(self, *tags: str, dtype: t.Union[str, numpy.dtype, t.Iterable[t.Union[str, numpy.dtype]], None] = None,\n               rename: t.Optional[t.Iterable[t.Optional[str]]] = None, required: t.Union[bool, t.Iterable[bool]] = True) -> polars.DataFrame:\n    dtypes: t.Iterable[t.Optional[numpy.dtype]]\n    if dtype is None:\n        dtypes = repeat(None)\n    elif isinstance(dtype, (numpy.dtype, str)):\n        dtypes = (numpy.dtype(dtype),) * len(tags)\n    else:\n        dtypes = tuple(map(lambda ty: numpy.dtype(ty), dtype))\n        if len(dtypes) != len(tags):\n            raise ValueError(\"dtype list of invalid length\")\n\n    if isinstance(required, bool):\n        required = repeat(required)\n\n    if rename is None:\n        rename = repeat(None)\n\n    d = {}\n    for (tag, ty, req, name) in zip(tags, dtypes, required, rename):\n        if tag not in self.data_dict:\n            if req:\n                raise ValueError(f\"Tag '{tag}' missing from CIF file\")\n            continue\n        try:\n            arr = numpy.array(self.data_dict[tag], dtype=ty)\n            d[name or tag] = arr\n        except TypeError:\n            raise TypeError(f\"Tag '{tag}' of invalid or heterogeneous type.\")\n\n    if len(d) == 0:\n        return polars.DataFrame({})\n\n    tag_len = len(next(iter(d.values())))\n    if any(len(arr) != tag_len for arr in d.values()):\n        raise ValueError(f\"Tags of mismatching lengths: {tuple(map(len, d.values()))}\")\n\n    return polars.DataFrame(d)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.cell_size","title":"cell_size","text":"
    cell_size() -> Optional[Tuple[float, float, float]]\n

    Return cell size (in angstroms).

    Source code in atomlib/io/cif.py
    def cell_size(self) -> t.Optional[t.Tuple[float, float, float]]:\n    \"\"\"Return cell size (in angstroms).\"\"\"\n    try:\n        a = float(self['cell_length_a'])  # type: ignore\n        b = float(self['cell_length_b'])  # type: ignore\n        c = float(self['cell_length_c'])  # type: ignore\n        return (a, b, c)\n    except (ValueError, TypeError, KeyError):\n        return None\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.cell_angle","title":"cell_angle","text":"
    cell_angle() -> Optional[Tuple[float, float, float]]\n

    Return cell angle (in degrees).

    Source code in atomlib/io/cif.py
    def cell_angle(self) -> t.Optional[t.Tuple[float, float, float]]:\n    \"\"\"Return cell angle (in degrees).\"\"\"\n    try:\n        a = float(self['cell_angle_alpha'])  # type: ignore\n        b = float(self['cell_angle_beta'])   # type: ignore\n        g = float(self['cell_angle_gamma'])  # type: ignore\n        return (a, b, g)\n    except (ValueError, TypeError, KeyError):\n        return None\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.get_symmetry","title":"get_symmetry","text":"
    get_symmetry() -> Iterator[AffineTransform3D]\n
    Source code in atomlib/io/cif.py
    def get_symmetry(self) -> t.Iterator[AffineTransform3D]:\n    syms = self.data_dict.get('space_group_symop_operation_xyz')\n    if syms is None:\n        # old name for symmetry\n        syms = self.data_dict.get('symmetry_equiv_pos_as_xyz')\n    if syms is None:\n        syms = ()\n    if not hasattr(syms, '__iter__'):\n        syms = (syms,)\n    return map(parse_symmetry, map(str, syms))  # type: ignore\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFTable","title":"CIFTable dataclass","text":"Source code in atomlib/io/cif.py
    @dataclass\nclass CIFTable:\n    data: t.Dict[str, t.List[Value]]\n\n    def _write(self, f: TextIOBase):\n        print(\"\\nloop_\", file=f)\n        for tag in self.data.keys():\n            print(f\" _{tag}\", file=f)\n\n        for row in zip(*self.data.values()):\n            print(f' {\"  \".join(map(_format_val, row))}', file=f)\n\n        print(file=f)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFTable.data","title":"data instance-attribute","text":"
    data: Dict[str, List[Value]]\n
    "},{"location":"api/io/cif/#atomlib.io.cif.SymmetryVec","title":"SymmetryVec","text":"Source code in atomlib/io/cif.py
    class SymmetryVec:\n    @classmethod\n    def parse(cls, s: str) -> SymmetryVec:\n        if s[0] in ('x', 'y', 'z'):\n            a = numpy.zeros((4,))\n            a[('x', 'y', 'z').index(s[0])] += 1.\n            return cls(a)\n        return cls(float(s))\n\n    def __init__(self, val: t.Union[float, NDArray[numpy.floating]]):\n       self.inner: t.Union[float, NDArray[numpy.floating]] = val\n\n    def is_scalar(self) -> bool:\n        return isinstance(self.inner, float)\n\n    def to_vec(self) -> NDArray[numpy.floating]:\n        if isinstance(self.inner, (int, float)):\n            vec = numpy.zeros((4,))\n            vec[3] = self.inner\n            return vec\n        return self.inner\n\n    def __add__(self, rhs: SymmetryVec) -> SymmetryVec:\n        if self.is_scalar() and rhs.is_scalar():\n            return SymmetryVec(self.inner + rhs.inner)\n        return SymmetryVec(rhs.to_vec() + self.to_vec())\n\n    def __neg__(self) -> SymmetryVec:\n        return SymmetryVec(-self.inner)\n\n    def __pos__(self) -> SymmetryVec:\n        return self\n\n    def __sub__(self, rhs: SymmetryVec) -> SymmetryVec:\n        if self.is_scalar() and rhs.is_scalar():\n            return SymmetryVec(self.inner - rhs.inner)\n        return SymmetryVec(rhs.to_vec() - self.to_vec())\n\n    def __mul__(self, rhs: SymmetryVec) -> SymmetryVec:\n        if not self.is_scalar() and not rhs.is_scalar():\n            raise ValueError(\"Can't multiply two symmetry directions\")\n        return SymmetryVec(rhs.inner * self.inner)\n\n    def __truediv__(self, rhs: SymmetryVec) -> SymmetryVec:\n        if not self.is_scalar() and not rhs.is_scalar():\n            raise ValueError(\"Can't divide two symmetry directions\")\n        return SymmetryVec(rhs.inner / self.inner)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.SymmetryVec.inner","title":"inner instance-attribute","text":"
    inner: Union[float, NDArray[floating]] = val\n
    "},{"location":"api/io/cif/#atomlib.io.cif.SymmetryVec.parse","title":"parse classmethod","text":"
    parse(s: str) -> SymmetryVec\n
    Source code in atomlib/io/cif.py
    @classmethod\ndef parse(cls, s: str) -> SymmetryVec:\n    if s[0] in ('x', 'y', 'z'):\n        a = numpy.zeros((4,))\n        a[('x', 'y', 'z').index(s[0])] += 1.\n        return cls(a)\n    return cls(float(s))\n
    "},{"location":"api/io/cif/#atomlib.io.cif.SymmetryVec.is_scalar","title":"is_scalar","text":"
    is_scalar() -> bool\n
    Source code in atomlib/io/cif.py
    def is_scalar(self) -> bool:\n    return isinstance(self.inner, float)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.SymmetryVec.to_vec","title":"to_vec","text":"
    to_vec() -> NDArray[floating]\n
    Source code in atomlib/io/cif.py
    def to_vec(self) -> NDArray[numpy.floating]:\n    if isinstance(self.inner, (int, float)):\n        vec = numpy.zeros((4,))\n        vec[3] = self.inner\n        return vec\n    return self.inner\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader","title":"CifReader","text":"Source code in atomlib/io/cif.py
    class CifReader:\n    def __init__(self, file: TextIOBase):\n        self.line = 0\n        self._file: TextIOBase = file\n        self._buf: t.Optional[str] = None\n        self._after_eol = True\n        self._eof = False\n\n    def parse(self) -> t.Iterator[CIFDataBlock]:\n        while True:\n            line = self.line\n            word = self.peek_word()\n            if word is None:\n                return\n            if word.lower().startswith('data_'):\n                self.next_word()\n                name = word[len('data_'):]\n            elif word.startswith('_'):\n                name = None\n            else:\n                raise ValueError(f\"While parsing line {line}: Unexpected token {word}\")\n\n            yield self.parse_datablock(name)\n\n    def after_eol(self) -> bool:\n        \"\"\"\n        Returns whether the current token (the one that will be returned\n        by the next peek() or next()) is after a newline.\n        \"\"\"\n        return self._after_eol\n\n    def peek_line(self) -> t.Optional[str]:\n        buf = self._try_fill_buf()\n        return buf\n\n    def next_line(self) -> t.Optional[str]:\n        line = self.peek_line()\n        self._buf = None\n        return line\n\n    def next_until(self, marker: str) -> t.Optional[str]:\n        \"\"\"\n        Collect words until `marker`. Because of the weirdness of CIF,\n        `marker` must occur immediately before a whitespace boundary.\n        \"\"\"\n        s = \"\"\n        buf = self._try_fill_buf()\n        if buf is None:\n            return None\n        while not (match := re.search(re.escape(marker) + r'(?=\\s|$)', buf)):\n            s += buf\n            buf = self._try_fill_buf(True)\n            if buf is None:\n                return None\n        s += buf[:match.end()]\n        self._buf = buf[match.end():]\n        if len(self._buf) == 0 or self._buf.isspace():\n            self._buf = None\n        return s\n\n    def peek_word(self) -> t.Optional[str]:\n        while True:\n            buf = self._try_fill_buf()\n            if buf is None:\n                return None\n            buf = buf.lstrip()\n            if len(buf) == 0 or buf.isspace() or buf.startswith('#'):\n                # eat comment or blank line\n                self._buf = None\n                continue\n            break\n\n        #print(f\"buf: '{buf}'\")\n        return buf.split(maxsplit=1)[0]\n\n    def next_word(self) -> t.Optional[str]:\n        w = self.peek_word()\n        if w is None:\n            return None\n        assert self._buf is not None\n        self._buf = self._buf.lstrip()[len(w)+1:].lstrip()\n        if len(self._buf) == 0 or self._buf.isspace():\n            # eat whitespace at end of line\n            self._buf = None\n            self._after_eol = True\n        else:\n            self._after_eol = False\n        return w\n\n    def _try_fill_buf(self, force: bool = False) -> t.Optional[str]:\n        if force:\n            self._buf = None\n        if self._buf is None:\n            try:\n                self._buf = next(self._file)\n                self.line += 1\n            except StopIteration:\n                pass\n        return self._buf\n\n    def parse_bare(self) -> t.Union[int, float, str]:\n        w = self.next_word()\n        if w is None:\n            raise ValueError(\"Unexpected EOF while parsing value.\")\n        if _INT_RE.fullmatch(w):\n            return int(w)  # may raise\n        if (m := _FLOAT_RE.fullmatch(w)):\n            if m[1] != '.':\n                return float(m[1])  # may raise\n        return w\n\n    def parse_datablock(self, name: t.Optional[str] = None) -> CIFDataBlock:\n        logging.debug(f\"parse datablock '{name}'\")\n        #data: t.Dict[str, t.Union[t.List[Value], Value]] = {}\n\n        data: t.List[t.Union[CIFTable, t.Tuple[str, Value]]] = []\n\n        while True:\n            word = self.peek_word()\n            if word is None:\n                break\n            if word.lower() == 'loop_':\n                self.next_word()\n                data.append(self.parse_loop())\n            elif word.startswith('_'):\n                self.next_word()\n                (k, v) = (word[1:], self.parse_value())\n                logging.debug(f\"{k} = {v}\")\n                data.append((k, v))\n            else:\n                break\n\n        return CIFDataBlock(name, tuple(data))\n\n    def eat_saveframe(self):\n        line = self.line\n        while True:\n            w = self.next_word()\n            if w is None:\n                raise ValueError(f\"EOF before end of save frame starting at line {line}\")\n            if w.lower() == 'save_':\n                break\n\n    def parse_loop(self) -> CIFTable:\n        line = self.line\n        tags = []\n        while True:\n            w = self.peek_word()\n            if w is None:\n                raise ValueError(f\"EOF before loop values at line {line}\")\n            if w.startswith('_'):\n                self.next_word()\n                tags.append(w[1:])\n            else:\n                break\n\n        vals: t.Tuple[t.List[Value], ...] = tuple([] for _ in tags)\n        i = 0\n\n        while True:\n            w = self.peek_word()\n            if w is None or w.startswith('_') or w.endswith('_'):\n                break\n            vals[i].append(self.parse_value())\n            i = (i + 1) % len(tags)\n\n        if i != 0:\n            n_vals = sum(map(len, vals))\n            raise ValueError(f\"While parsing loop at line {line}: \"\n                            f\"Got {n_vals} vals, expected a multiple of {len(tags)}\")\n\n        return CIFTable(dict(zip(tags, vals)))\n\n    def parse_value(self) -> Value:\n        logging.debug(\"parse_value\")\n        w = self.peek_word()\n        assert w is not None\n        if w in ('.', '?'):\n            self.next_word()\n            return None\n\n        if self.after_eol() and w == ';':\n            return self.parse_text_field()\n\n        if w[0] in ('\"', \"'\"):\n            return self.parse_quoted()\n\n        return self.parse_bare()\n\n    def parse_text_field(self) -> str:\n        start_line = self.line\n        line = self.next_line()\n        assert line is not None\n        s = line.lstrip().removeprefix(';').lstrip()\n        while True:\n            line = self.next_line()\n            if line is None:\n                raise ValueError(f\"While parsing text field at line {start_line}: Unexpected EOF\")\n            if line.strip() == ';':\n                break\n            s += line\n        return s.rstrip()\n\n    def parse_quoted(self) -> str:\n        line = self.line\n        w = self.peek_word()\n        assert w is not None\n        quote = w[0]\n        if quote not in ('\"', \"'\"):\n            raise ValueError(f\"While parsing string at line {line}: Invalid quote char {quote}\")\n\n        s = self.next_until(quote)\n        if s is None:\n            raise ValueError(f\"While parsing string {w}... at line {line}: Unexpected EOF\")\n        return s.lstrip()[1:-1]\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.line","title":"line instance-attribute","text":"
    line = 0\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.parse","title":"parse","text":"
    parse() -> Iterator[CIFDataBlock]\n
    Source code in atomlib/io/cif.py
    def parse(self) -> t.Iterator[CIFDataBlock]:\n    while True:\n        line = self.line\n        word = self.peek_word()\n        if word is None:\n            return\n        if word.lower().startswith('data_'):\n            self.next_word()\n            name = word[len('data_'):]\n        elif word.startswith('_'):\n            name = None\n        else:\n            raise ValueError(f\"While parsing line {line}: Unexpected token {word}\")\n\n        yield self.parse_datablock(name)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.after_eol","title":"after_eol","text":"
    after_eol() -> bool\n

    Returns whether the current token (the one that will be returned by the next peek() or next()) is after a newline.

    Source code in atomlib/io/cif.py
    def after_eol(self) -> bool:\n    \"\"\"\n    Returns whether the current token (the one that will be returned\n    by the next peek() or next()) is after a newline.\n    \"\"\"\n    return self._after_eol\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.peek_line","title":"peek_line","text":"
    peek_line() -> Optional[str]\n
    Source code in atomlib/io/cif.py
    def peek_line(self) -> t.Optional[str]:\n    buf = self._try_fill_buf()\n    return buf\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.next_line","title":"next_line","text":"
    next_line() -> Optional[str]\n
    Source code in atomlib/io/cif.py
    def next_line(self) -> t.Optional[str]:\n    line = self.peek_line()\n    self._buf = None\n    return line\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.next_until","title":"next_until","text":"
    next_until(marker: str) -> Optional[str]\n

    Collect words until marker. Because of the weirdness of CIF, marker must occur immediately before a whitespace boundary.

    Source code in atomlib/io/cif.py
    def next_until(self, marker: str) -> t.Optional[str]:\n    \"\"\"\n    Collect words until `marker`. Because of the weirdness of CIF,\n    `marker` must occur immediately before a whitespace boundary.\n    \"\"\"\n    s = \"\"\n    buf = self._try_fill_buf()\n    if buf is None:\n        return None\n    while not (match := re.search(re.escape(marker) + r'(?=\\s|$)', buf)):\n        s += buf\n        buf = self._try_fill_buf(True)\n        if buf is None:\n            return None\n    s += buf[:match.end()]\n    self._buf = buf[match.end():]\n    if len(self._buf) == 0 or self._buf.isspace():\n        self._buf = None\n    return s\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.peek_word","title":"peek_word","text":"
    peek_word() -> Optional[str]\n
    Source code in atomlib/io/cif.py
    def peek_word(self) -> t.Optional[str]:\n    while True:\n        buf = self._try_fill_buf()\n        if buf is None:\n            return None\n        buf = buf.lstrip()\n        if len(buf) == 0 or buf.isspace() or buf.startswith('#'):\n            # eat comment or blank line\n            self._buf = None\n            continue\n        break\n\n    #print(f\"buf: '{buf}'\")\n    return buf.split(maxsplit=1)[0]\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.next_word","title":"next_word","text":"
    next_word() -> Optional[str]\n
    Source code in atomlib/io/cif.py
    def next_word(self) -> t.Optional[str]:\n    w = self.peek_word()\n    if w is None:\n        return None\n    assert self._buf is not None\n    self._buf = self._buf.lstrip()[len(w)+1:].lstrip()\n    if len(self._buf) == 0 or self._buf.isspace():\n        # eat whitespace at end of line\n        self._buf = None\n        self._after_eol = True\n    else:\n        self._after_eol = False\n    return w\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.parse_bare","title":"parse_bare","text":"
    parse_bare() -> Union[int, float, str]\n
    Source code in atomlib/io/cif.py
    def parse_bare(self) -> t.Union[int, float, str]:\n    w = self.next_word()\n    if w is None:\n        raise ValueError(\"Unexpected EOF while parsing value.\")\n    if _INT_RE.fullmatch(w):\n        return int(w)  # may raise\n    if (m := _FLOAT_RE.fullmatch(w)):\n        if m[1] != '.':\n            return float(m[1])  # may raise\n    return w\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.parse_datablock","title":"parse_datablock","text":"
    parse_datablock(name: Optional[str] = None) -> CIFDataBlock\n
    Source code in atomlib/io/cif.py
    def parse_datablock(self, name: t.Optional[str] = None) -> CIFDataBlock:\n    logging.debug(f\"parse datablock '{name}'\")\n    #data: t.Dict[str, t.Union[t.List[Value], Value]] = {}\n\n    data: t.List[t.Union[CIFTable, t.Tuple[str, Value]]] = []\n\n    while True:\n        word = self.peek_word()\n        if word is None:\n            break\n        if word.lower() == 'loop_':\n            self.next_word()\n            data.append(self.parse_loop())\n        elif word.startswith('_'):\n            self.next_word()\n            (k, v) = (word[1:], self.parse_value())\n            logging.debug(f\"{k} = {v}\")\n            data.append((k, v))\n        else:\n            break\n\n    return CIFDataBlock(name, tuple(data))\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.eat_saveframe","title":"eat_saveframe","text":"
    eat_saveframe()\n
    Source code in atomlib/io/cif.py
    def eat_saveframe(self):\n    line = self.line\n    while True:\n        w = self.next_word()\n        if w is None:\n            raise ValueError(f\"EOF before end of save frame starting at line {line}\")\n        if w.lower() == 'save_':\n            break\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.parse_loop","title":"parse_loop","text":"
    parse_loop() -> CIFTable\n
    Source code in atomlib/io/cif.py
    def parse_loop(self) -> CIFTable:\n    line = self.line\n    tags = []\n    while True:\n        w = self.peek_word()\n        if w is None:\n            raise ValueError(f\"EOF before loop values at line {line}\")\n        if w.startswith('_'):\n            self.next_word()\n            tags.append(w[1:])\n        else:\n            break\n\n    vals: t.Tuple[t.List[Value], ...] = tuple([] for _ in tags)\n    i = 0\n\n    while True:\n        w = self.peek_word()\n        if w is None or w.startswith('_') or w.endswith('_'):\n            break\n        vals[i].append(self.parse_value())\n        i = (i + 1) % len(tags)\n\n    if i != 0:\n        n_vals = sum(map(len, vals))\n        raise ValueError(f\"While parsing loop at line {line}: \"\n                        f\"Got {n_vals} vals, expected a multiple of {len(tags)}\")\n\n    return CIFTable(dict(zip(tags, vals)))\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.parse_value","title":"parse_value","text":"
    parse_value() -> Value\n
    Source code in atomlib/io/cif.py
    def parse_value(self) -> Value:\n    logging.debug(\"parse_value\")\n    w = self.peek_word()\n    assert w is not None\n    if w in ('.', '?'):\n        self.next_word()\n        return None\n\n    if self.after_eol() and w == ';':\n        return self.parse_text_field()\n\n    if w[0] in ('\"', \"'\"):\n        return self.parse_quoted()\n\n    return self.parse_bare()\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.parse_text_field","title":"parse_text_field","text":"
    parse_text_field() -> str\n
    Source code in atomlib/io/cif.py
    def parse_text_field(self) -> str:\n    start_line = self.line\n    line = self.next_line()\n    assert line is not None\n    s = line.lstrip().removeprefix(';').lstrip()\n    while True:\n        line = self.next_line()\n        if line is None:\n            raise ValueError(f\"While parsing text field at line {start_line}: Unexpected EOF\")\n        if line.strip() == ';':\n            break\n        s += line\n    return s.rstrip()\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.parse_quoted","title":"parse_quoted","text":"
    parse_quoted() -> str\n
    Source code in atomlib/io/cif.py
    def parse_quoted(self) -> str:\n    line = self.line\n    w = self.peek_word()\n    assert w is not None\n    quote = w[0]\n    if quote not in ('\"', \"'\"):\n        raise ValueError(f\"While parsing string at line {line}: Invalid quote char {quote}\")\n\n    s = self.next_until(quote)\n    if s is None:\n        raise ValueError(f\"While parsing string {w}... at line {line}: Unexpected EOF\")\n    return s.lstrip()[1:-1]\n
    "},{"location":"api/io/cif/#atomlib.io.cif.parse_symmetry","title":"parse_symmetry","text":"
    parse_symmetry(s: str) -> AffineTransform3D\n
    Source code in atomlib/io/cif.py
    def parse_symmetry(s: str) -> AffineTransform3D:\n    axes = s.split(',')\n    if not len(axes) == 3:\n        raise ValueError(f\"Error parsing symmetry expression '{s}': Expected 3 values, got {len(axes)}\")\n\n    axes = [SYMMETRY_PARSER.parse(StringIO(ax)).eval(lambda v: v).to_vec() for ax in axes]\n    axes.append(numpy.array([0., 0., 0., 1.]))\n    return AffineTransform3D(numpy.stack(axes, axis=0))\n
    "},{"location":"api/io/lmp/","title":"atomlib.io.lmp","text":"

    IO for LAAMPS data files, as described here.

    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMP","title":"LMP dataclass","text":"Source code in atomlib/io/lmp.py
    @dataclass\nclass LMP:\n    comment: t.Optional[str]\n    headers: t.Dict[str, t.Any]\n    sections: t.Tuple[LMPSection, ...]\n\n    def get_cell(self) -> Cell:\n        dims = numpy.array([\n            self.headers.get(f\"{c}lo {c}hi\", (-0.5, 0.5))\n            for c in \"xyz\"\n        ])\n        origin = dims[:, 0]\n        tilts = self.headers.get(\"xy xz yz\", (0., 0., 0.))\n\n        ortho = numpy.diag(dims[:, 1] - dims[:, 0])\n        (ortho[0, 1], ortho[0, 2], ortho[1, 2]) = tilts\n\n        return Cell.from_ortho(LinearTransform3D(ortho).translate(origin))\n\n    def get_atoms(self, type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> AtomCell:\n        if type_map is not None:\n            try:\n                type_map_df = polars.DataFrame({\n                    'type': polars.Series(type_map.keys(), dtype=polars.Int32),\n                    'elem': polars.Series(list(map(get_elem, type_map.values())), dtype=polars.UInt8),\n                    'symbol': polars.Series([get_sym(v) if isinstance(v, int) else v for v in type_map.values()], dtype=polars.Utf8),\n                })\n            except ValueError as e:\n                raise ValueError(\"Invalid type map\") from e\n        else:\n            type_map_df = None\n\n        cell = self.get_cell()\n\n        def _apply_type_labels(df: polars.DataFrame, section_name: str, labels: t.Optional[polars.DataFrame] = None) -> polars.DataFrame:\n            if labels is not None:\n                #df = df.with_columns(polars.col('type').replace(d, default=polars.col('type').cast(polars.Int32, strict=False), return_dtype=polars.Int32))\n                df = df.with_columns(polars.col('type').replace_strict(labels['symbol'], labels['type'], default=polars.col('type').cast(polars.Int32, strict=False), return_dtype=polars.Int32))\n                if df['type'].is_null().any():\n                    raise ValueError(f\"While parsing section {section_name}: Unknown atom label or invalid atom type\")\n            try:\n                return df.with_columns(polars.col('type').cast(polars.Int32))\n            except polars.ComputeError:\n                raise ValueError(f\"While parsing section {section_name}: Invalid atom type(s)\")\n\n        atoms: t.Optional[polars.DataFrame] = None\n        labels: t.Optional[polars.DataFrame] = None\n        masses: t.Optional[polars.DataFrame] = None\n        velocities = None\n\n        for section in self.sections:\n            start_line = section.start_line + 1\n\n            if section.name == 'Atoms':\n                if section.style not in (None, 'atomic'):\n                    # TODO support other styles\n                    raise ValueError(f\"Only 'atomic' atom_style is supported, instead got '{section.style}'\")\n\n                atoms = parse_whitespace_separated(section.body, {\n                    'i': polars.Int64, 'type': polars.Utf8,\n                    'coords': polars.Array(polars.Float64, 3),\n                }, start_line=start_line)\n                atoms = _apply_type_labels(atoms, 'Atoms', labels)\n            elif section.name == 'Atom Type Labels':\n                labels = parse_whitespace_separated(section.body, {'type': polars.Int32, 'symbol': polars.Utf8}, start_line=start_line)\n            elif section.name == 'Masses':\n                masses = parse_whitespace_separated(section.body, {'type': polars.Utf8, 'mass': polars.Float64}, start_line=start_line)\n                masses = _apply_type_labels(masses, 'Masses', labels)\n            elif section.name == 'Velocities':\n                velocities = parse_whitespace_separated(section.body, {\n                    'i': polars.Int64, 'velocity': polars.Array(polars.Float64, 3),\n                }, start_line=start_line)\n\n        # now all 'type's should be in Int32\n\n        if atoms is None:\n            if self.headers['atoms'] > 0:\n                raise ValueError(\"Missing required section 'Atoms'\")\n            return AtomCell(Atoms.empty(), cell=cell, frame='local')\n\n        # next we need to assign element symbols\n        # first, if type_map is specified, use that:\n        #if type_map_elem is not None and type_map_sym is not None:\n        if type_map_df is not None:\n            try:\n                atoms = checked_left_join(atoms, type_map_df, on='type')\n            except CheckedJoinError as e:\n                raise ValueError(f\"Missing type_map specification for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n        elif labels is not None:\n            try:\n                labels = labels.with_columns(get_elem(labels['symbol']))\n            except ValueError as e:\n                raise ValueError(\"Failed to auto-detect elements from type labels. Please pass 'type_map' explicitly\") from e\n            try:\n                atoms = checked_left_join(atoms, labels, 'type')\n            except CheckedJoinError as e:\n                raise ValueError(f\"Missing labels for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n        # otherwise we have no way\n        else:\n            raise ValueError(\"Failed to auto-detect elements from type labels. Please pass 'type_map' explicitly\")\n\n        if velocities is not None:\n            # join velocities\n            try:\n                # TODO use join_asof here?\n                atoms = checked_left_join(atoms, velocities, 'i')\n            except CheckedJoinError as e:\n                raise ValueError(f\"Missing velocities for {len(e.missing_keys)}/{len(atoms)} atoms\")\n\n        if masses is not None:\n            # join masses\n            try:\n                atoms = checked_left_join(atoms, masses, 'type')\n            except CheckedJoinError as e:\n                raise ValueError(f\"Missing masses for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n\n        return AtomCell(atoms, cell=cell, frame='local')\n\n    @staticmethod\n    def from_atoms(atoms: HasAtoms) -> LMP:\n        if isinstance(atoms, HasAtomCell):\n            # we're basically converting everything to the ortho frame, but including the affine shift\n\n            # transform affine shift into ortho frame\n            origin = atoms.get_transform('ortho', 'local').to_linear().round_near_zero() \\\n                .transform(atoms.get_cell().affine.translation())\n\n            # get the orthogonalization transform only, without affine\n            ortho = atoms.get_transform('ortho', 'cell_box').to_linear().round_near_zero().inner\n\n            # get atoms in ortho frame, and then add the affine shift\n            frame = atoms.get_atoms('ortho').transform_atoms(AffineTransform3D.translate(origin)) \\\n                .round_near_zero().with_type()\n        else:\n            bbox = atoms.bbox_atoms()\n            ortho = numpy.diag(bbox.size)\n            origin = bbox.min\n\n            frame = atoms.get_atoms('local').with_type()\n\n        types = frame.unique(subset='type')\n        types = types.with_mass().sort('type')\n\n        now = localtime()\n        comment = f\"# Generated by atomlib on {now.isoformat(' ', 'seconds')}\"\n\n        headers = {}\n        sections = []\n\n        headers['atoms'] = len(frame)\n        headers['atom types'] = len(types)\n\n        for (s, low, diff) in zip(('x', 'y', 'z'), origin, ortho.diagonal()):\n            headers[f\"{s}lo {s}hi\"] = (low, low + diff)\n\n        headers['xy xz yz'] = (ortho[0, 1], ortho[0, 2], ortho[1, 2])\n\n        body = [\n            f\" {ty:8} {sym:>4}\\n\"\n            for (ty, sym) in types.select('type', 'symbol').rows()\n        ]\n        sections.append(LMPSection(\"Atom Type Labels\", tuple(body)))\n\n        if 'mass' in types:\n            body = [\n                f\" {ty:8} {mass:14.7f}  # {sym}\\n\"\n                for (ty, sym, mass) in types.select(('type', 'symbol', 'mass')).rows()\n            ]\n            sections.append(LMPSection(\"Masses\", tuple(body)))\n\n        body = [\n            f\" {i+1:8} {ty:4} {x:14.7f} {y:14.7f} {z:14.7f}\\n\"\n            for (i, (ty, (x, y, z))) in enumerate(frame.select(('type', 'coords')).rows())\n        ]\n        sections.append(LMPSection(\"Atoms\", tuple(body), 'atomic'))\n\n        if (velocities := frame.velocities()) is not None:\n            body = [\n                f\" {i+1:8} {v_x:14.7f} {v_y:14.7f} {v_z:14.7f}\\n\"\n                for (i, (v_x, v_y, v_z)) in enumerate(velocities)\n            ]\n            sections.append(LMPSection(\"Velocities\", tuple(body)))\n\n        return LMP(comment, headers, tuple(sections))\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> LMP:\n        with open_file(file, 'r') as f:\n            return LMPReader(f).parse()\n\n    def write(self, file: FileOrPath):\n        with open_file(file, 'w') as f:\n            print((self.comment or \"\") + '\\n', file=f)\n\n            # print headers\n            for (name, val) in self.headers.items():\n                val = _HEADER_FMT.get(name, lambda s: f\"{s:8}\")(val)\n                print(f\" {val} {name}\", file=f)\n\n            # print sections\n            for section in self.sections:\n                line = section.name\n                if section.style is not None:\n                    line += f'  # {section.style}'\n                print(f\"\\n{line}\\n\", file=f)\n\n                f.writelines(section.body)\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMP.comment","title":"comment instance-attribute","text":"
    comment: Optional[str]\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMP.headers","title":"headers instance-attribute","text":"
    headers: Dict[str, Any]\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMP.sections","title":"sections instance-attribute","text":"
    sections: Tuple[LMPSection, ...]\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMP.get_cell","title":"get_cell","text":"
    get_cell() -> Cell\n
    Source code in atomlib/io/lmp.py
    def get_cell(self) -> Cell:\n    dims = numpy.array([\n        self.headers.get(f\"{c}lo {c}hi\", (-0.5, 0.5))\n        for c in \"xyz\"\n    ])\n    origin = dims[:, 0]\n    tilts = self.headers.get(\"xy xz yz\", (0., 0., 0.))\n\n    ortho = numpy.diag(dims[:, 1] - dims[:, 0])\n    (ortho[0, 1], ortho[0, 2], ortho[1, 2]) = tilts\n\n    return Cell.from_ortho(LinearTransform3D(ortho).translate(origin))\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMP.get_atoms","title":"get_atoms","text":"
    get_atoms(\n    type_map: Optional[Dict[int, Union[str, int]]] = None\n) -> AtomCell\n
    Source code in atomlib/io/lmp.py
    def get_atoms(self, type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> AtomCell:\n    if type_map is not None:\n        try:\n            type_map_df = polars.DataFrame({\n                'type': polars.Series(type_map.keys(), dtype=polars.Int32),\n                'elem': polars.Series(list(map(get_elem, type_map.values())), dtype=polars.UInt8),\n                'symbol': polars.Series([get_sym(v) if isinstance(v, int) else v for v in type_map.values()], dtype=polars.Utf8),\n            })\n        except ValueError as e:\n            raise ValueError(\"Invalid type map\") from e\n    else:\n        type_map_df = None\n\n    cell = self.get_cell()\n\n    def _apply_type_labels(df: polars.DataFrame, section_name: str, labels: t.Optional[polars.DataFrame] = None) -> polars.DataFrame:\n        if labels is not None:\n            #df = df.with_columns(polars.col('type').replace(d, default=polars.col('type').cast(polars.Int32, strict=False), return_dtype=polars.Int32))\n            df = df.with_columns(polars.col('type').replace_strict(labels['symbol'], labels['type'], default=polars.col('type').cast(polars.Int32, strict=False), return_dtype=polars.Int32))\n            if df['type'].is_null().any():\n                raise ValueError(f\"While parsing section {section_name}: Unknown atom label or invalid atom type\")\n        try:\n            return df.with_columns(polars.col('type').cast(polars.Int32))\n        except polars.ComputeError:\n            raise ValueError(f\"While parsing section {section_name}: Invalid atom type(s)\")\n\n    atoms: t.Optional[polars.DataFrame] = None\n    labels: t.Optional[polars.DataFrame] = None\n    masses: t.Optional[polars.DataFrame] = None\n    velocities = None\n\n    for section in self.sections:\n        start_line = section.start_line + 1\n\n        if section.name == 'Atoms':\n            if section.style not in (None, 'atomic'):\n                # TODO support other styles\n                raise ValueError(f\"Only 'atomic' atom_style is supported, instead got '{section.style}'\")\n\n            atoms = parse_whitespace_separated(section.body, {\n                'i': polars.Int64, 'type': polars.Utf8,\n                'coords': polars.Array(polars.Float64, 3),\n            }, start_line=start_line)\n            atoms = _apply_type_labels(atoms, 'Atoms', labels)\n        elif section.name == 'Atom Type Labels':\n            labels = parse_whitespace_separated(section.body, {'type': polars.Int32, 'symbol': polars.Utf8}, start_line=start_line)\n        elif section.name == 'Masses':\n            masses = parse_whitespace_separated(section.body, {'type': polars.Utf8, 'mass': polars.Float64}, start_line=start_line)\n            masses = _apply_type_labels(masses, 'Masses', labels)\n        elif section.name == 'Velocities':\n            velocities = parse_whitespace_separated(section.body, {\n                'i': polars.Int64, 'velocity': polars.Array(polars.Float64, 3),\n            }, start_line=start_line)\n\n    # now all 'type's should be in Int32\n\n    if atoms is None:\n        if self.headers['atoms'] > 0:\n            raise ValueError(\"Missing required section 'Atoms'\")\n        return AtomCell(Atoms.empty(), cell=cell, frame='local')\n\n    # next we need to assign element symbols\n    # first, if type_map is specified, use that:\n    #if type_map_elem is not None and type_map_sym is not None:\n    if type_map_df is not None:\n        try:\n            atoms = checked_left_join(atoms, type_map_df, on='type')\n        except CheckedJoinError as e:\n            raise ValueError(f\"Missing type_map specification for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n    elif labels is not None:\n        try:\n            labels = labels.with_columns(get_elem(labels['symbol']))\n        except ValueError as e:\n            raise ValueError(\"Failed to auto-detect elements from type labels. Please pass 'type_map' explicitly\") from e\n        try:\n            atoms = checked_left_join(atoms, labels, 'type')\n        except CheckedJoinError as e:\n            raise ValueError(f\"Missing labels for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n    # otherwise we have no way\n    else:\n        raise ValueError(\"Failed to auto-detect elements from type labels. Please pass 'type_map' explicitly\")\n\n    if velocities is not None:\n        # join velocities\n        try:\n            # TODO use join_asof here?\n            atoms = checked_left_join(atoms, velocities, 'i')\n        except CheckedJoinError as e:\n            raise ValueError(f\"Missing velocities for {len(e.missing_keys)}/{len(atoms)} atoms\")\n\n    if masses is not None:\n        # join masses\n        try:\n            atoms = checked_left_join(atoms, masses, 'type')\n        except CheckedJoinError as e:\n            raise ValueError(f\"Missing masses for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n\n    return AtomCell(atoms, cell=cell, frame='local')\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMP.from_atoms","title":"from_atoms staticmethod","text":"
    from_atoms(atoms: HasAtoms) -> LMP\n
    Source code in atomlib/io/lmp.py
    @staticmethod\ndef from_atoms(atoms: HasAtoms) -> LMP:\n    if isinstance(atoms, HasAtomCell):\n        # we're basically converting everything to the ortho frame, but including the affine shift\n\n        # transform affine shift into ortho frame\n        origin = atoms.get_transform('ortho', 'local').to_linear().round_near_zero() \\\n            .transform(atoms.get_cell().affine.translation())\n\n        # get the orthogonalization transform only, without affine\n        ortho = atoms.get_transform('ortho', 'cell_box').to_linear().round_near_zero().inner\n\n        # get atoms in ortho frame, and then add the affine shift\n        frame = atoms.get_atoms('ortho').transform_atoms(AffineTransform3D.translate(origin)) \\\n            .round_near_zero().with_type()\n    else:\n        bbox = atoms.bbox_atoms()\n        ortho = numpy.diag(bbox.size)\n        origin = bbox.min\n\n        frame = atoms.get_atoms('local').with_type()\n\n    types = frame.unique(subset='type')\n    types = types.with_mass().sort('type')\n\n    now = localtime()\n    comment = f\"# Generated by atomlib on {now.isoformat(' ', 'seconds')}\"\n\n    headers = {}\n    sections = []\n\n    headers['atoms'] = len(frame)\n    headers['atom types'] = len(types)\n\n    for (s, low, diff) in zip(('x', 'y', 'z'), origin, ortho.diagonal()):\n        headers[f\"{s}lo {s}hi\"] = (low, low + diff)\n\n    headers['xy xz yz'] = (ortho[0, 1], ortho[0, 2], ortho[1, 2])\n\n    body = [\n        f\" {ty:8} {sym:>4}\\n\"\n        for (ty, sym) in types.select('type', 'symbol').rows()\n    ]\n    sections.append(LMPSection(\"Atom Type Labels\", tuple(body)))\n\n    if 'mass' in types:\n        body = [\n            f\" {ty:8} {mass:14.7f}  # {sym}\\n\"\n            for (ty, sym, mass) in types.select(('type', 'symbol', 'mass')).rows()\n        ]\n        sections.append(LMPSection(\"Masses\", tuple(body)))\n\n    body = [\n        f\" {i+1:8} {ty:4} {x:14.7f} {y:14.7f} {z:14.7f}\\n\"\n        for (i, (ty, (x, y, z))) in enumerate(frame.select(('type', 'coords')).rows())\n    ]\n    sections.append(LMPSection(\"Atoms\", tuple(body), 'atomic'))\n\n    if (velocities := frame.velocities()) is not None:\n        body = [\n            f\" {i+1:8} {v_x:14.7f} {v_y:14.7f} {v_z:14.7f}\\n\"\n            for (i, (v_x, v_y, v_z)) in enumerate(velocities)\n        ]\n        sections.append(LMPSection(\"Velocities\", tuple(body)))\n\n    return LMP(comment, headers, tuple(sections))\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMP.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> LMP\n
    Source code in atomlib/io/lmp.py
    @staticmethod\ndef from_file(file: FileOrPath) -> LMP:\n    with open_file(file, 'r') as f:\n        return LMPReader(f).parse()\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMP.write","title":"write","text":"
    write(file: FileOrPath)\n
    Source code in atomlib/io/lmp.py
    def write(self, file: FileOrPath):\n    with open_file(file, 'w') as f:\n        print((self.comment or \"\") + '\\n', file=f)\n\n        # print headers\n        for (name, val) in self.headers.items():\n            val = _HEADER_FMT.get(name, lambda s: f\"{s:8}\")(val)\n            print(f\" {val} {name}\", file=f)\n\n        # print sections\n        for section in self.sections:\n            line = section.name\n            if section.style is not None:\n                line += f'  # {section.style}'\n            print(f\"\\n{line}\\n\", file=f)\n\n            f.writelines(section.body)\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPSection","title":"LMPSection dataclass","text":"Source code in atomlib/io/lmp.py
    @dataclass\nclass LMPSection:\n    name: str\n    body: t.Tuple[str, ...]\n    style: t.Optional[str] = None\n    start_line: int = 0\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPSection.name","title":"name instance-attribute","text":"
    name: str\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPSection.body","title":"body instance-attribute","text":"
    body: Tuple[str, ...]\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPSection.style","title":"style class-attribute instance-attribute","text":"
    style: Optional[str] = None\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPSection.start_line","title":"start_line class-attribute instance-attribute","text":"
    start_line: int = 0\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPReader","title":"LMPReader","text":"Source code in atomlib/io/lmp.py
    class LMPReader:\n    def __init__(self, f: TextIOBase):\n        self.line = 0\n        self._file: TextIOBase = f\n        self._buf: t.Optional[str] = None\n\n    def _split_comment(self, line: str) -> t.Tuple[str, t.Optional[str]]:\n        split = _COMMENT_RE.split(line, maxsplit=1)\n        return (split[0], split[1] if len(split) > 1 else None)\n\n    def parse(self) -> LMP:\n        # parse comment\n        comment = self.next_line(skip_blank=False)\n        if comment is None:\n            raise ValueError(\"Unexpected EOF (file is blank)\")\n        if comment.isspace():\n            comment = None\n        else:\n            comment = comment[:-1]\n\n        headers = self.parse_headers()\n        sections = self.parse_sections(headers)\n\n        return LMP(comment, headers, sections)\n\n    def parse_headers(self) -> t.Dict[str, t.Any]:\n        headers: t.Dict[str, t.Any] = {}\n        while True:\n            line = self.peek_line()\n            if line is None:\n                break\n            body = self._split_comment(line)[0]\n\n            if (match := _HEADER_KW_RE.search(body)) is None:\n                # probably a body\n                break\n            self.next_line()\n\n            name = match[0]\n            value = body[:match.start(0)].strip()\n\n            try:\n                if name in _HEADER_PARSE:\n                    value = _HEADER_PARSE[name](value)\n            except Exception as e:\n                raise ValueError(f\"While parsing header '{name}' at line {self.line}: Failed to parse value '{value}\") from e\n\n            #print(f\"header {name} => {value} (type {type(value)})\")\n            headers[name] = value\n\n        return headers\n\n    def parse_sections(self, headers: t.Dict[str, t.Any]) -> t.Tuple[LMPSection, ...]:\n        first = True\n\n        sections: t.List[LMPSection] = []\n\n        while True:\n            start_line = self.line\n            line = self.next_line()\n            if line is None:\n                break\n            name, comment = self._split_comment(line)\n            name = name.strip()\n\n            try:\n                n_lines_header = _SECTION_KWS[name]\n            except KeyError:\n                if first:\n                    raise ValueError(f\"While parsing line {self.line}: Unknown header or section keyword '{line}'\") from None\n                else:\n                    raise ValueError(f\"While parsing line {self.line}: Unknown section keyword '{line}'\") from None\n\n            try:\n                if n_lines_header is None:\n                    # special case for PairIJ Coeffs:\n                    n = int(headers['atom types'])\n                    n_lines = (n * (n + 1)) // 2\n                else:\n                    n_lines = int(headers[n_lines_header])\n            except KeyError:\n                raise ValueError(f\"While parsing body section '{name}' at line {self.line}: \"\n                                 f\"Missing required header '{n_lines_header or 'atom types'}'\") from None\n\n            style = comment if name in _SECTION_STYLE_KWS else None\n            if style is not None:\n                style = style.strip()\n\n            #print(f\"section '{name}' @ {self.line}, {n_lines} lines, style {style}\")\n\n            lines = self.collect_lines(n_lines)\n            if lines is None:\n                raise ValueError(f\"While parsing body section '{name}' starting at line {self.line}: \"\n                                 f\"Unexpected EOF before {n_lines} lines were read\")\n\n            sections.append(LMPSection(\n                name, tuple(lines), style, start_line\n            ))\n            first = False\n\n        return tuple(sections)\n\n    def _try_fill_buf(self, skip_blank: bool = True) -> t.Optional[str]:\n        if self._buf is None:\n            try:\n                # skip blank lines\n                while True:\n                    self._buf = next(self._file)\n                    self.line += 1\n                    if not (skip_blank and self._buf.isspace()):\n                        break\n            except StopIteration:\n                pass\n        return self._buf\n\n    def peek_line(self, skip_blank: bool = True) -> t.Optional[str]:\n        return self._try_fill_buf(skip_blank)\n\n    def next_line(self, skip_blank: bool = True) -> t.Optional[str]:\n        line = self._try_fill_buf(skip_blank)\n        self._buf = None\n        return line\n\n    def collect_lines(self, n: int) -> t.Optional[t.List[str]]:\n        assert self._buf is None\n        lines = []\n        try:\n            for _ in range(n):\n                while True:\n                    line = next(self._file)\n                    if not line.isspace():\n                        lines.append(line)\n                        break\n        except StopIteration:\n            return None\n        self.line += n\n        return lines\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPReader.line","title":"line instance-attribute","text":"
    line = 0\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPReader.parse","title":"parse","text":"
    parse() -> LMP\n
    Source code in atomlib/io/lmp.py
    def parse(self) -> LMP:\n    # parse comment\n    comment = self.next_line(skip_blank=False)\n    if comment is None:\n        raise ValueError(\"Unexpected EOF (file is blank)\")\n    if comment.isspace():\n        comment = None\n    else:\n        comment = comment[:-1]\n\n    headers = self.parse_headers()\n    sections = self.parse_sections(headers)\n\n    return LMP(comment, headers, sections)\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPReader.parse_headers","title":"parse_headers","text":"
    parse_headers() -> Dict[str, Any]\n
    Source code in atomlib/io/lmp.py
    def parse_headers(self) -> t.Dict[str, t.Any]:\n    headers: t.Dict[str, t.Any] = {}\n    while True:\n        line = self.peek_line()\n        if line is None:\n            break\n        body = self._split_comment(line)[0]\n\n        if (match := _HEADER_KW_RE.search(body)) is None:\n            # probably a body\n            break\n        self.next_line()\n\n        name = match[0]\n        value = body[:match.start(0)].strip()\n\n        try:\n            if name in _HEADER_PARSE:\n                value = _HEADER_PARSE[name](value)\n        except Exception as e:\n            raise ValueError(f\"While parsing header '{name}' at line {self.line}: Failed to parse value '{value}\") from e\n\n        #print(f\"header {name} => {value} (type {type(value)})\")\n        headers[name] = value\n\n    return headers\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPReader.parse_sections","title":"parse_sections","text":"
    parse_sections(\n    headers: Dict[str, Any]\n) -> Tuple[LMPSection, ...]\n
    Source code in atomlib/io/lmp.py
    def parse_sections(self, headers: t.Dict[str, t.Any]) -> t.Tuple[LMPSection, ...]:\n    first = True\n\n    sections: t.List[LMPSection] = []\n\n    while True:\n        start_line = self.line\n        line = self.next_line()\n        if line is None:\n            break\n        name, comment = self._split_comment(line)\n        name = name.strip()\n\n        try:\n            n_lines_header = _SECTION_KWS[name]\n        except KeyError:\n            if first:\n                raise ValueError(f\"While parsing line {self.line}: Unknown header or section keyword '{line}'\") from None\n            else:\n                raise ValueError(f\"While parsing line {self.line}: Unknown section keyword '{line}'\") from None\n\n        try:\n            if n_lines_header is None:\n                # special case for PairIJ Coeffs:\n                n = int(headers['atom types'])\n                n_lines = (n * (n + 1)) // 2\n            else:\n                n_lines = int(headers[n_lines_header])\n        except KeyError:\n            raise ValueError(f\"While parsing body section '{name}' at line {self.line}: \"\n                             f\"Missing required header '{n_lines_header or 'atom types'}'\") from None\n\n        style = comment if name in _SECTION_STYLE_KWS else None\n        if style is not None:\n            style = style.strip()\n\n        #print(f\"section '{name}' @ {self.line}, {n_lines} lines, style {style}\")\n\n        lines = self.collect_lines(n_lines)\n        if lines is None:\n            raise ValueError(f\"While parsing body section '{name}' starting at line {self.line}: \"\n                             f\"Unexpected EOF before {n_lines} lines were read\")\n\n        sections.append(LMPSection(\n            name, tuple(lines), style, start_line\n        ))\n        first = False\n\n    return tuple(sections)\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPReader.peek_line","title":"peek_line","text":"
    peek_line(skip_blank: bool = True) -> Optional[str]\n
    Source code in atomlib/io/lmp.py
    def peek_line(self, skip_blank: bool = True) -> t.Optional[str]:\n    return self._try_fill_buf(skip_blank)\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPReader.next_line","title":"next_line","text":"
    next_line(skip_blank: bool = True) -> Optional[str]\n
    Source code in atomlib/io/lmp.py
    def next_line(self, skip_blank: bool = True) -> t.Optional[str]:\n    line = self._try_fill_buf(skip_blank)\n    self._buf = None\n    return line\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPReader.collect_lines","title":"collect_lines","text":"
    collect_lines(n: int) -> Optional[List[str]]\n
    Source code in atomlib/io/lmp.py
    def collect_lines(self, n: int) -> t.Optional[t.List[str]]:\n    assert self._buf is None\n    lines = []\n    try:\n        for _ in range(n):\n            while True:\n                line = next(self._file)\n                if not line.isspace():\n                    lines.append(line)\n                    break\n    except StopIteration:\n        return None\n    self.line += n\n    return lines\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.write_lmp","title":"write_lmp","text":"
    write_lmp(atoms: HasAtoms, f: FileOrPath)\n
    Source code in atomlib/io/lmp.py
    def write_lmp(atoms: HasAtoms, f: FileOrPath):\n    LMP.from_atoms(atoms).write(f)\n    return\n
    "},{"location":"api/io/mp_api/","title":"atomlib.io.mp_api","text":"

    IO for the Materials Project API, .

    "},{"location":"api/io/mp_api/#atomlib.io.mp_api.get_api_key","title":"get_api_key","text":"
    get_api_key(key: Optional[str] = None) -> str\n
    Source code in atomlib/io/mp_api.py
    def get_api_key(key: t.Optional[str] = None) -> str:\n    try:\n        return environ['MP_API_KEY']\n    except KeyError:\n        raise RuntimeError(\"No materials project API key specified. \"\n                           \"Either pass `api_key` or set `MP_API_KEY` in your environment.\") from None\n
    "},{"location":"api/io/mp_api/#atomlib.io.mp_api.get_api_endpoint","title":"get_api_endpoint","text":"
    get_api_endpoint() -> str\n
    Source code in atomlib/io/mp_api.py
    def get_api_endpoint() -> str:\n    return environ.get(\"MP_API_ENDPOINT\") or \"https://api.materialsproject.org/\"\n
    "},{"location":"api/io/mp_api/#atomlib.io.mp_api.resolve_id","title":"resolve_id","text":"
    resolve_id(id: Union[str, int]) -> str\n
    Source code in atomlib/io/mp_api.py
    def resolve_id(id: t.Union[str, int]) -> str:\n    if isinstance(id, int):\n        return str(id)\n    if id.lower().startswith('mp-'):\n        return id[3:]\n    return id\n
    "},{"location":"api/io/mp_api/#atomlib.io.mp_api.load_materials_project","title":"load_materials_project","text":"
    load_materials_project(\n    id: Union[str, int],\n    *,\n    api_key: Optional[str] = None,\n    api_endpoint: Optional[str] = None\n) -> AtomCell\n
    Source code in atomlib/io/mp_api.py
    def load_materials_project(id: t.Union[str, int], *, api_key: t.Optional[str] = None,\n                           api_endpoint: t.Optional[str] = None) -> AtomCell:\n    id = resolve_id(id)\n    api_key = api_key or get_api_key()\n    if len(api_key) != 32:\n        raise RuntimeError(\"Materials project API key must be a 32-character alphanumeric string. \"\n                           f\"Instead got '{api_key}' of length '{len(api_key)}'.\")\n    api_endpoint = (api_endpoint or get_api_endpoint()).rstrip('/')\n\n    logging.info(f\"Fetching structure mp-{id} from materials project...\")\n    response: requests.Response = requests.get(\n        api_endpoint + f'/materials/core/mp-{id}/',\n        headers={'X-Api-Key': api_key},\n        params={'_fields': 'structure'}\n    )\n    try:\n        response.raise_for_status()\n    except requests.HTTPError as e:\n        raise ValueError(f\"Failed to fetch structure 'mp-{id}'\") from e\n    try:\n        data = response.json()['data'][0]\n        structure = data['structure']\n    except (KeyError, ValueError, TypeError) as e:\n        raise ValueError(f\"Unexpected API response: {response}\") from e\n\n    ortho = LinearTransform3D(numpy.array(structure['lattice']['matrix']).T)\n    sites = structure['sites']\n\n    rows = []\n    for site in sites:\n        (x, y, z) = site['xyz']\n        for species in site['species']:\n            rows.append({\n                'symbol': site['label'],\n                'elem': get_elem(species['element']),\n                'x': x, 'y': y, 'z': z,\n                'frac': species['occu'],\n                **site['properties'],\n            })\n\n    frame = Atoms(rows, orient='row')\n    return AtomCell.from_ortho(frame, ortho)\n
    "},{"location":"api/io/mslice/","title":"atomlib.io.mslice","text":"

    IO support for the pyMultislicer XML file format.

    Writes mslice files with the help of a user-supplied template.

    "},{"location":"api/io/mslice/#atomlib.io.mslice.ElementTree","title":"ElementTree module-attribute","text":"
    ElementTree: TypeAlias = _ElementTree\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.Element","title":"Element module-attribute","text":"
    Element: TypeAlias = _Element\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.MSliceFile","title":"MSliceFile module-attribute","text":"
    MSliceFile: TypeAlias = Union[ElementTree, FileOrPath]\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.DEFAULT_TEMPLATE_PATH","title":"DEFAULT_TEMPLATE_PATH module-attribute","text":"
    DEFAULT_TEMPLATE_PATH = (\n    files(\"atomlib.data\") / \"template.mslice\"\n)\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.DEFAULT_TEMPLATE","title":"DEFAULT_TEMPLATE module-attribute","text":"
    DEFAULT_TEMPLATE: Optional[ElementTree] = None\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.default_template","title":"default_template","text":"
    default_template() -> ElementTree\n
    Source code in atomlib/io/mslice.py
    def default_template() -> ElementTree:\n    global DEFAULT_TEMPLATE\n\n    if DEFAULT_TEMPLATE is None:\n        with DEFAULT_TEMPLATE_PATH.open('r') as f:  # type: ignore\n            DEFAULT_TEMPLATE = t.cast(ElementTree, et.parse(f, None))\n\n    return deepcopy(DEFAULT_TEMPLATE)\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.convert_xml_value","title":"convert_xml_value","text":"
    convert_xml_value(val: str, ty: str)\n

    Convert an XML value val to a Python type determined by the XML type name ty.

    Source code in atomlib/io/mslice.py
    def convert_xml_value(val: str, ty: str):\n    \"\"\"Convert an XML value `val` to a Python type determined by the XML type name `ty`.\"\"\"\n    if ty == 'string':\n        ty = 'str'\n    elif ty == 'int16' or ty == 'int32':\n        val = val.split('.')[0]\n        ty = 'int'\n    elif ty == 'bool':\n        return bool(int(val))\n\n    return getattr(builtins, ty)(val)\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.parse_xml_object","title":"parse_xml_object","text":"
    parse_xml_object(obj: Element) -> Dict[str, Any]\n

    Parse the attributes of a passed XML object.

    Source code in atomlib/io/mslice.py
    def parse_xml_object(obj: Element) -> t.Dict[str, t.Any]:\n    \"\"\"Parse the attributes of a passed XML object.\"\"\"\n    params = {}\n    for attr in t.cast(t.Iterator[Element], obj.iter(None)):\n        if attr.tag == 'attribute':\n            params[attr.attrib['name']] = convert_xml_value(attr.text, attr.attrib['type'])\n        elif attr.tag == 'relationship':\n            # todo give this a better API\n            if 'idrefs' in attr.attrib:\n                params[f\"{attr.attrib['name']}ID\"] = attr.attrib['idrefs']\n    return params\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.find_xml_object","title":"find_xml_object","text":"
    find_xml_object(\n    xml: Element, typename: str\n) -> Dict[str, Any]\n

    Find and parse XML objects named typename, flattening them into a single Dict.

    Source code in atomlib/io/mslice.py
    def find_xml_object(xml: Element, typename: str) -> t.Dict[str, t.Any]:\n    \"\"\"Find and parse XML objects named `typename`, flattening them into a single Dict.\"\"\"\n    params = {}\n    for obj in xml.findall(f\".//*[@type='{typename}']\", None):\n        params.update(parse_xml_object(obj))\n    return params\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.find_xml_object_list","title":"find_xml_object_list","text":"
    find_xml_object_list(\n    xml: Element, typename: str\n) -> List[Any]\n

    Find and parse a list of XML objects named typename.

    Source code in atomlib/io/mslice.py
    def find_xml_object_list(xml: Element, typename: str) -> t.List[t.Any]:\n    \"\"\"Find and parse a list of XML objects named `typename`.\"\"\"\n    return [parse_xml_object(obj) for obj in xml.findall(f\".//*[@type='{typename}']\", None)]\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.find_xml_object_dict","title":"find_xml_object_dict","text":"
    find_xml_object_dict(\n    xml: Element, typename: str, key: str = \"id\"\n) -> Dict[str, Any]\n

    Find and parse XML objects named typename, combining them into a dict.

    Source code in atomlib/io/mslice.py
    def find_xml_object_dict(xml: Element, typename: str, key: str = \"id\") -> t.Dict[str, t.Any]:\n    \"\"\"Find and parse XML objects named `typename`, combining them into a dict.\"\"\"\n    return {\n        obj.attrib[key]: parse_xml_object(obj)\n        for obj in xml.findall(f\".//*[@type='{typename}']\", None)\n    }\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.read_mslice","title":"read_mslice","text":"
    read_mslice(path: MSliceFile) -> AtomCell\n
    Source code in atomlib/io/mslice.py
    def read_mslice(path: MSliceFile) -> AtomCell:\n    tree: ElementTree\n    if isinstance(path, ElementTree):\n        tree = path\n    else:\n        with open_file(path, 'r') as temp:\n            tree = et.parse(temp, None)\n\n    xml: Element = tree.getroot()\n\n    structure = find_xml_object(xml, \"STRUCTURE\")\n    structure_atoms = find_xml_object_list(xml, \"STRUCTUREATOM\")\n\n    n_cells = tuple(structure.get(k, 1) for k in ('repeata', 'repeatb', 'repeatc'))\n    cell_size = tuple(structure[k] for k in ('aparam', 'bparam', 'cparam'))\n\n    atoms = Atoms(\n        polars.from_dicts(structure_atoms, schema={\n            'atomicnumber': polars.UInt8,\n            'x': polars.Float64, 'y': polars.Float64, 'z': polars.Float64,\n            'wobble': polars.Float64, 'fracoccupancy': polars.Float64,\n        })\n        .rename({'atomicnumber': 'elem', 'fracoccupancy': 'frac_occupancy'}) \\\n        # 1d sigma -> <u^2>\n        .with_columns((3. * polars.col('wobble')**2).alias('wobble'))\n    )\n    cell = Cell.from_ortho(LinearTransform3D.scale(cell_size), n_cells, [True, True, False])\n\n    return AtomCell(atoms, cell, frame='cell_frac')\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.write_mslice","title":"write_mslice","text":"
    write_mslice(\n    cell: HasAtomCell,\n    f: BinaryFileOrPath,\n    template: Optional[MSliceFile] = None,\n    *,\n    slice_thickness: Optional[float] = None,\n    scan_points: Optional[ArrayLike] = None,\n    scan_extent: Optional[ArrayLike] = None,\n    noise_sigma: Optional[float] = None,\n    conv_angle: Optional[float] = None,\n    energy: Optional[float] = None,\n    defocus: Optional[float] = None,\n    tilt: Optional[Tuple[float, float]] = None,\n    tds: Optional[bool] = None,\n    n_cells: Optional[ArrayLike] = None\n)\n

    Write a structure to an mslice file. The structure must be orthogonal and aligned with the local coordinate system. It should be periodic in X and Y.

    template may be a file, path, or ElementTree containing an existing mslice file. Its structure will be modified to make the final output. If not specified, a default template will be used.

    Additional options modify simulation properties. If an option is not specified, the template's properties are used.

    Source code in atomlib/io/mslice.py
    def write_mslice(cell: HasAtomCell, f: BinaryFileOrPath, template: t.Optional[MSliceFile] = None, *,\n                 slice_thickness: t.Optional[float] = None,  # angstrom\n                 scan_points: t.Optional[ArrayLike] = None,\n                 scan_extent: t.Optional[ArrayLike] = None,\n                 noise_sigma: t.Optional[float] = None,  # angstrom\n                 conv_angle: t.Optional[float] = None,  # mrad\n                 energy: t.Optional[float] = None,  # keV\n                 defocus: t.Optional[float] = None,  # angstrom\n                 tilt: t.Optional[t.Tuple[float, float]] = None,  # (mrad, mrad)\n                 tds: t.Optional[bool] = None,\n                 n_cells: t.Optional[ArrayLike] = None):\n    \"\"\"\n    Write a structure to an mslice file. The structure must be orthogonal and aligned\n    with the local coordinate system. It should be periodic in X and Y.\n\n    ``template`` may be a file, path, or ElementTree containing an existing mslice file.\n    Its structure will be modified to make the final output. If not specified, a default\n    template will be used.\n\n    Additional options modify simulation properties. If an option is not specified, the\n    template's properties are used.\n    \"\"\"\n    #if not issubclass(type(cell), HasAtomCell):\n    #    raise TypeError(\"mslice format requires an AtomCell.\")\n\n    if not cell.is_orthogonal_in_local():\n        raise ValueError(\"mslice requires an orthogonal AtomCell.\")\n\n    if not numpy.all(cell.pbc[:2]):\n        warn(\"AtomCell may not be periodic\", UserWarning, stacklevel=2)\n\n    box_size = cell._box_size_in_local()\n\n    # get atoms in local frame (which we verified aligns with the cell's axes)\n    # then scale into fractional coordinates\n    atoms = cell.get_atoms('linear') \\\n        .transform(AffineTransform3D.scale(1/box_size)) \\\n        .with_wobble().with_occupancy()\n\n    out: ElementTree\n    if template is None:\n        out = default_template()\n    elif not isinstance(template, ElementTree):\n        with open_file(template, 'r') as temp:\n            out = et.parse(temp, None)\n    else:\n        out = deepcopy(template)\n\n    # TODO clean up this code\n    db: t.Optional[Element] = out.getroot() if out.getroot().tag == 'database' else out.find(\"./database\", None)\n    if db is None:\n        raise ValueError(\"Couldn't find 'database' tag in template.\")\n\n    struct = db.find(\".//object[@type='STRUCTURE']\", None)\n    if struct is None:\n        raise ValueError(\"Couldn't find STRUCTURE object in template.\")\n\n    params = db.find(\".//object[@type='SIMPARAMETERS']\", None)\n    if params is None:\n        raise ValueError(\"Couldn't find SIMPARAMETERS object in template.\")\n\n    microscope = db.find(\".//object[@type='MICROSCOPE']\", None)\n    if microscope is None:\n        raise ValueError(\"Couldn't find MICROSCOPE object in template.\")\n\n    scan = db.find(\".//object[@type='SCAN']\", None)\n    aberrations = db.findall(\".//object[@type='ABERRATION']\", None)\n\n    def set_attr(struct: Element, name: str, type: str, val: str):\n        node = t.cast(t.Optional[Element], struct.find(f\".//attribute[@name='{name}']\", None))\n        if node is None:\n            node = t.cast(Element, et.Element('attribute', dict(name=name, type=type), None))\n            struct.append(node)\n        else:\n            node.attrib['type'] = type\n        node.text = val  # type: ignore\n\n    def parse_xml_object(obj: Element) -> t.Dict[str, t.Any]:\n        \"\"\"Parse the attributes of a passed XML object.\"\"\"\n        params = {}\n        for attr in obj.iterchildren(None):\n            if attr.tag == 'attribute':\n                params[attr.attrib['name']] = convert_xml_value(attr.text, attr.attrib['type'])\n            elif attr.tag == 'relationship':\n                # todo give this a better API\n                if 'idrefs' in attr.attrib:\n                    params[f\"{attr.attrib['name']}ID\"] = attr.attrib['idrefs']\n        return params\n\n    # TODO how to store atoms in unexploded form\n    (n_a, n_b, n_c) = map(str, (1, 1, 1) if n_cells is None else numpy.asarray(n_cells).astype(int))\n    set_attr(struct, 'repeata', 'int16', n_a)\n    set_attr(struct, 'repeatb', 'int16', n_b)\n    set_attr(struct, 'repeatc', 'int16', n_c)\n\n    (a, b, c) = map(lambda v: f\"{v:.8g}\", box_size)\n    set_attr(struct, 'aparam', 'float', a)\n    set_attr(struct, 'bparam', 'float', b)\n    set_attr(struct, 'cparam', 'float', c)\n\n    if tilt is not None:\n        (tiltx, tilty) = tilt\n        set_attr(struct, 'tiltx', 'float', f\"{tiltx:.4g}\")\n        set_attr(struct, 'tilty', 'float', f\"{tilty:.4g}\")\n\n    if slice_thickness is not None:\n        set_attr(params, 'slicethickness', 'float', f\"{float(slice_thickness):.8g}\")\n    if tds is not None:\n        set_attr(params, 'includetds', 'bool', str(int(bool(tds))))\n    if conv_angle is not None:\n        set_attr(microscope, 'aperture', 'float', f\"{float(conv_angle):.8g}\")\n    if energy is not None:\n        set_attr(microscope, 'kv', 'float', f\"{float(energy):.8g}\")\n    if noise_sigma is not None:\n        if scan is None:\n            raise ValueError(\"New scan specification required for 'noise_sigma'.\")\n        set_attr(scan, 'noise_sigma', 'float', f\"{float(noise_sigma):.8g}\")\n\n    if defocus is not None:\n        for aberration in aberrations:\n            obj = parse_xml_object(aberration)\n            if obj['n'] == 1 and obj['m'] == 0:\n                set_attr(aberration, 'cnma', 'float', f\"{float(defocus):.8g}\")  # A, + is over\n                set_attr(aberration, 'cnmb', 'float', \"0.0\")\n                break\n        else:\n            raise ValueError(\"Couldn't find defocus aberration to modify.\")\n\n    if scan_points is not None:\n        (nx, ny) = numpy.broadcast_to(scan_points, 2,).astype(int)\n        if scan is not None:\n            set_attr(scan, 'nx', 'int16', str(nx))\n            set_attr(scan, 'ny', 'int16', str(ny))\n        else:\n            set_attr(params, 'numscanx', 'int16', str(nx))\n            set_attr(params, 'numscany', 'int16', str(ny))\n\n    if scan_extent is not None:\n        scan_extent = numpy.asarray(scan_extent, dtype=float)\n        try:\n            if scan_extent.ndim < 2:\n                if not scan_extent.shape == (4,):\n                    scan_extent = numpy.broadcast_to(scan_extent, (2,))\n                    scan_extent = numpy.stack(((0., 0.), scan_extent), axis=-1)\n            else:\n                scan_extent = numpy.broadcast_to(scan_extent, (2, 2))\n        except ValueError as e:\n            raise ValueError(f\"Invalid scan_extent '{scan_extent}'. Expected an array of shape (2,), (4,), or (2, 2).\") from e\n\n        if scan is not None:\n            names = ('x_i', 'x_f', 'y_i', 'y_f')\n            elem = scan\n        else:\n            names = ('intx', 'finx', 'inty', 'finy')\n            elem = params\n\n        for (name, val) in zip(names, scan_extent.ravel()):\n            set_attr(elem, name, 'float', f\"{float(val):.8g}\")\n\n    # remove existing atoms\n    for elem in db.findall(\"./object[@type='STRUCTUREATOM']\", None):\n        db.remove(elem)\n\n    # <u^2> -> 1d sigma\n    atoms = atoms.with_wobble((polars.col('wobble') / 3.).sqrt())\n    rows = atoms.select(('elem', 'coords', 'wobble', 'frac_occupancy')).rows()\n    for (i, (elem, (x, y, z), wobble, frac_occupancy)) in enumerate(rows):\n        e = _atom_elem(i, elem, x, y, z, wobble, frac_occupancy)\n        db.append(e)\n\n    et.indent(db, space=\"    \", level=0)  # type: ignore\n\n    with open_file_binary(f, 'w') as f:\n        doctype = b\"\"\"<!DOCTYPE database SYSTEM \"file:///System/Library/DTDs/CoreData.dtd\">\\n\"\"\"\n        out.write(f, encoding='UTF-8', xml_declaration=True, standalone=True, doctype=doctype)  # type: ignore\n        f.write(b'\\n')\n
    "},{"location":"api/io/qe/","title":"atomlib.io.qe","text":"

    IO for Quantum Espresso pw.x files.

    "},{"location":"api/io/qe/#atomlib.io.qe.write_qe","title":"write_qe","text":"
    write_qe(\n    atomcell: HasAtomCell,\n    f: FileOrPath,\n    pseudo: Optional[Mapping[str, str]] = None,\n)\n

    Write a structure to a Quantum Espresso pw.x file.

    PARAMETER DESCRIPTION atomcell

    Structure to write

    TYPE: HasAtomCell

    f

    File or path to write to

    TYPE: FileOrPath

    pseudo

    Mapping from atom symbol

    TYPE: Optional[Mapping[str, str]] DEFAULT: None

    Source code in atomlib/io/qe.py
    def write_qe(atomcell: HasAtomCell, f: FileOrPath, pseudo: t.Optional[t.Mapping[str, str]] = None):\n    \"\"\"\n    Write a structure to a Quantum Espresso pw.x file.\n\n    Args:\n      atomcell: Structure to write\n      f: File or path to write to\n      pseudo: Mapping from atom symbol\n    \"\"\"\n    if not isinstance(atomcell, HasAtomCell):\n        raise TypeError(\"'qe' format requires an AtomCell.\")\n\n    atoms = atomcell.wrap().get_atoms('cell_box').with_mass()\n\n    types = atoms.select(('symbol', 'mass')).unique(subset='symbol').sort('mass')\n    if pseudo is not None:\n        types = types.with_columns(_get_symbol_mapping(types, pseudo, ty=polars.Utf8).alias('pot'))\n    else:\n        types = types.with_columns((polars.col('symbol') + polars.lit('.UPF')).alias('pot'))\n        #types = types.with_columns(polars.col('symbol').apply(lambda sym: f\"{sym}.UPF\").alias('pot'))\n\n    with open_file(f, 'w') as f:\n        print(f\"\"\"\\\n&SYSTEM\n  ibrav=0,\n  nat={len(atoms)},\n  ntyp={len(types)}\n/\"\"\", file=f)\n\n        ortho = atomcell.get_transform('local', 'cell_box').to_linear().inner\n        print(\"\\nCELL_PARAMETERS angstrom\", file=f)\n        for row in ortho.T:\n            print(f\"  {row[0]:12.8f} {row[1]:12.8f} {row[2]:12.8f}\", file=f)\n\n        print(\"\\nATOMIC_SPECIES\", file=f)\n        for (symbol, mass, pot) in types.select(('symbol', 'mass', 'pot')).rows():\n            print(f\"{symbol:>4} {mass:10.3f}  {pot}\", file=f)\n\n        print(\"\\nATOMIC_POSITIONS crystal\", file=f)\n        for (symbol, (x, y, z)) in atoms.select(('symbol', 'coords')).rows():\n            print(f\"{symbol:>4} {x:.8f} {y:.8f} {z:.8f}\", file=f)\n\n        print(file=f)  # allows for easy concatenation\n
    "},{"location":"api/io/xsf/","title":"atomlib.io.xsf","text":"

    IO for XCrySDen's XSF format, specified here.

    "},{"location":"api/io/xsf/#atomlib.io.xsf.Periodicity","title":"Periodicity module-attribute","text":"
    Periodicity: TypeAlias = Literal[\n    \"crystal\", \"slab\", \"polymer\", \"molecule\"\n]\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF","title":"XSF dataclass","text":"Source code in atomlib/io/xsf.py
    @dataclass\nclass XSF:\n    periodicity: Periodicity = 'crystal'\n    primitive_cell: t.Optional[LinearTransform3D] = None\n    conventional_cell: t.Optional[LinearTransform3D] = None\n\n    prim_coords: t.Optional[polars.DataFrame] = None\n    conv_coords: t.Optional[polars.DataFrame] = None\n    atoms: t.Optional[polars.DataFrame] = None\n\n    def get_atoms(self) -> polars.DataFrame:\n        if self.prim_coords is not None:\n            return self.prim_coords\n        if self.atoms is not None:\n            return self.atoms\n        if self.conv_coords is not None:\n            raise NotImplementedError()  # TODO untransform conv_coords by conventional_cell?\n        raise ValueError(\"No coordinates specified in XSF file.\")\n\n    def get_pbc(self) -> NDArray[numpy.bool_]:\n        return _periodicity_to_pbc(self.periodicity)\n\n    @staticmethod\n    def from_cell(cell: HasAtomCell) -> XSF:\n        ortho = cell.get_transform('local', 'cell_box').to_linear()\n        return XSF(\n            primitive_cell=ortho,\n            conventional_cell=ortho,\n            prim_coords=cell.get_atoms('linear').inner,\n            periodicity=_pbc_to_periodicity(cell.pbc)\n        )\n\n    @staticmethod\n    def from_atoms(atoms: HasAtoms) -> XSF:\n        return XSF(\n            periodicity='molecule',\n            atoms=atoms.get_atoms('local').inner\n        )\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> XSF:\n        logging.info(f\"Loading XSF {file.name if hasattr(file, 'name') else file!r}...\")  # type: ignore\n        with open_file(file) as f:\n            return XSFParser(f).parse()\n\n    def __post_init__(self):\n        if self.prim_coords is None and self.conv_coords is None and self.atoms is None:\n            raise ValueError(\"Error: No coordinates are specified (atoms, primitive, or conventional).\")\n\n        if self.prim_coords is not None and self.conv_coords is not None:\n            logging.warning(\"Warning: Both 'primcoord' and 'convcoord' are specified. 'convcoord' will be ignored.\")\n        elif self.conv_coords is not None and self.conventional_cell is None:\n            raise ValueError(\"If 'convcoord' is specified, 'convvec' must be specified as well.\")\n\n        if self.periodicity == 'molecule':\n            if self.atoms is None:\n                raise ValueError(\"'atoms' must be specified for molecules.\")\n\n    def write(self, path: FileOrPath):\n        with open_file(path, 'w') as f:\n            print(self.periodicity.upper(), file=f)\n            if self.primitive_cell is not None:\n                print('PRIMVEC', file=f)\n                self._write_cell(f, self.primitive_cell)\n            if self.conventional_cell is not None:\n                print('CONVVEC', file=f)\n                self._write_cell(f, self.conventional_cell)\n            print(file=f)\n\n            if self.prim_coords is not None:\n                print(\"PRIMCOORD\", file=f)\n                print(f\"{len(self.prim_coords)} 1\", file=f)\n                self._write_coords(f, self.prim_coords)\n            if self.conv_coords is not None:\n                print(\"CONVCOORD\", file=f)\n                print(f\"{len(self.conv_coords)} 1\", file=f)\n                self._write_coords(f, self.conv_coords)\n            if self.atoms is not None:\n                print(\"ATOMS\", file=f)\n                self._write_coords(f, self.atoms)\n\n    def _write_cell(self, f: TextIOBase, cell: LinearTransform3D):\n        for row in cell.inner.T:\n            for val in row:\n                f.write(f\"{val:12.7f}\")\n            f.write('\\n')\n\n    def _write_coords(self, f: TextIOBase, coords: polars.DataFrame):\n        for (elem, [x, y, z]) in coords.select(['elem', 'coords']).rows():\n            print(f\"{elem:2d} {x:11.6f} {y:11.6f} {z:11.6f}\", file=f)\n        print(file=f)\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.periodicity","title":"periodicity class-attribute instance-attribute","text":"
    periodicity: Periodicity = 'crystal'\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.primitive_cell","title":"primitive_cell class-attribute instance-attribute","text":"
    primitive_cell: Optional[LinearTransform3D] = None\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.conventional_cell","title":"conventional_cell class-attribute instance-attribute","text":"
    conventional_cell: Optional[LinearTransform3D] = None\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.prim_coords","title":"prim_coords class-attribute instance-attribute","text":"
    prim_coords: Optional[DataFrame] = None\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.conv_coords","title":"conv_coords class-attribute instance-attribute","text":"
    conv_coords: Optional[DataFrame] = None\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.atoms","title":"atoms class-attribute instance-attribute","text":"
    atoms: Optional[DataFrame] = None\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.get_atoms","title":"get_atoms","text":"
    get_atoms() -> DataFrame\n
    Source code in atomlib/io/xsf.py
    def get_atoms(self) -> polars.DataFrame:\n    if self.prim_coords is not None:\n        return self.prim_coords\n    if self.atoms is not None:\n        return self.atoms\n    if self.conv_coords is not None:\n        raise NotImplementedError()  # TODO untransform conv_coords by conventional_cell?\n    raise ValueError(\"No coordinates specified in XSF file.\")\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.get_pbc","title":"get_pbc","text":"
    get_pbc() -> NDArray[bool_]\n
    Source code in atomlib/io/xsf.py
    def get_pbc(self) -> NDArray[numpy.bool_]:\n    return _periodicity_to_pbc(self.periodicity)\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.from_cell","title":"from_cell staticmethod","text":"
    from_cell(cell: HasAtomCell) -> XSF\n
    Source code in atomlib/io/xsf.py
    @staticmethod\ndef from_cell(cell: HasAtomCell) -> XSF:\n    ortho = cell.get_transform('local', 'cell_box').to_linear()\n    return XSF(\n        primitive_cell=ortho,\n        conventional_cell=ortho,\n        prim_coords=cell.get_atoms('linear').inner,\n        periodicity=_pbc_to_periodicity(cell.pbc)\n    )\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.from_atoms","title":"from_atoms staticmethod","text":"
    from_atoms(atoms: HasAtoms) -> XSF\n
    Source code in atomlib/io/xsf.py
    @staticmethod\ndef from_atoms(atoms: HasAtoms) -> XSF:\n    return XSF(\n        periodicity='molecule',\n        atoms=atoms.get_atoms('local').inner\n    )\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> XSF\n
    Source code in atomlib/io/xsf.py
    @staticmethod\ndef from_file(file: FileOrPath) -> XSF:\n    logging.info(f\"Loading XSF {file.name if hasattr(file, 'name') else file!r}...\")  # type: ignore\n    with open_file(file) as f:\n        return XSFParser(f).parse()\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.write","title":"write","text":"
    write(path: FileOrPath)\n
    Source code in atomlib/io/xsf.py
    def write(self, path: FileOrPath):\n    with open_file(path, 'w') as f:\n        print(self.periodicity.upper(), file=f)\n        if self.primitive_cell is not None:\n            print('PRIMVEC', file=f)\n            self._write_cell(f, self.primitive_cell)\n        if self.conventional_cell is not None:\n            print('CONVVEC', file=f)\n            self._write_cell(f, self.conventional_cell)\n        print(file=f)\n\n        if self.prim_coords is not None:\n            print(\"PRIMCOORD\", file=f)\n            print(f\"{len(self.prim_coords)} 1\", file=f)\n            self._write_coords(f, self.prim_coords)\n        if self.conv_coords is not None:\n            print(\"CONVCOORD\", file=f)\n            print(f\"{len(self.conv_coords)} 1\", file=f)\n            self._write_coords(f, self.conv_coords)\n        if self.atoms is not None:\n            print(\"ATOMS\", file=f)\n            self._write_coords(f, self.atoms)\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSFParser","title":"XSFParser","text":"Source code in atomlib/io/xsf.py
    class XSFParser:\n    def __init__(self, file: TextIOBase):\n        self._file: TextIOBase = file\n        self._peek_line: t.Optional[str] = None\n        self.lineno = 0\n\n    def skip_line(self, line: t.Optional[str]) -> bool:\n        return line is None or line.isspace() or line.lstrip().startswith('#')\n\n    def peek_line(self) -> t.Optional[str]:\n        try:\n            while self.skip_line(self._peek_line):\n                self._peek_line = next(self._file)\n                self.lineno += 1\n            return self._peek_line\n        except StopIteration:\n            return None\n\n    def next_line(self) -> t.Optional[str]:\n        line = self.peek_line()\n        self._peek_line = None\n        return line\n\n    def parse_atoms(self, expected_length: t.Optional[int] = None) -> polars.DataFrame:\n        zs = []\n        coords = []\n        words = None\n\n        while (line := self.peek_line()):\n            words = line.split()\n            if len(words) == 0:\n                continue\n            if words[0].isalpha():\n                break\n            self.next_line()\n            try:\n                z = int(words[0])\n                if z < 0 or z > 118:\n                    raise ValueError()\n            except (ValueError, TypeError):\n                raise ValueError(f\"Invalid atomic number '{words[0]}'\") from None\n\n            try:\n                coords.append(numpy.array(list(map(float, words[1:]))))\n                zs.append(z)\n            except (ValueError, TypeError):\n                raise ValueError(f\"Invalid atomic coordinates '{' '.join(words[1:])}'\") from None\n\n        if expected_length is not None:\n            if not expected_length == len(zs):\n                logging.warning(f\"Warning: List length {len(zs)} doesn't match declared length {expected_length}\")\n        elif len(zs) == 0:\n            raise ValueError(f\"Expected atom list after keyword 'ATOMS'. Got '{line or 'EOF'}' instead.\")\n\n        if len(zs) == 0:\n            return polars.DataFrame({}, schema=['elem', 'x', 'y', 'z'])  # type: ignore\n\n        coord_lens = list(map(len, coords))\n        if not all(coord_len == coord_lens[0] for coord_len in coord_lens[1:]):\n            raise ValueError(\"Mismatched atom dimensions.\")\n        if coord_lens[0] < 3:\n            raise ValueError(\"Expected at least 3 coordinates per atom.\")\n\n        coords = numpy.stack(coords, axis=0)[:, :3]\n        (x, y, z) = map(lambda a: a[:, 0], numpy.split(coords, 3, axis=1))\n\n        return polars.DataFrame({'elem': zs, 'x': x, 'y': y, 'z': z})\n\n    def parse_coords(self) -> polars.DataFrame:\n        line = self.next_line()\n        if line is None:\n            raise ValueError(\"Unexpected EOF before atom list\")\n        words = line.split()\n        try:\n            if not len(words) == 2:\n                raise ValueError()\n            (n, _) = map(int, words)\n        except (ValueError, TypeError):\n            raise ValueError(f\"Invalid atom list length: {line}\") from None\n\n        return self.parse_atoms(n)\n\n    def parse_lattice(self) -> LinearTransform3D:\n        rows = []\n        for _ in range(3):\n            line = self.next_line()\n            if line is None:\n                raise ValueError(\"Unexpected EOF in vector section.\")\n            words = line.split()\n            try:\n                if not len(words) == 3:\n                    raise ValueError()\n                row = numpy.array(list(map(float, words)))\n                rows.append(row)\n            except (ValueError, TypeError):\n                raise ValueError(f\"Invalid lattice vector: {line}\") from None\n\n        matrix = numpy.stack(rows, axis=-1)\n        return LinearTransform3D(matrix)\n\n    def eat_sandwich(self, keyword: str):\n        begin_keyword = 'begin_' + keyword\n        end_keyword = 'end_' + keyword\n        lineno = self.lineno\n\n        while (line := self.next_line()):\n            keyword = line.lstrip().split(maxsplit=1)[0].lower()\n            if keyword.lower() == begin_keyword:\n                # recurse to inner (identical) section\n                self.eat_sandwich(keyword)\n                continue\n            if keyword.lower() == end_keyword:\n                break\n        else:\n            raise ValueError(f\"Unclosed section '{keyword}' opened at line {lineno}\")\n\n    def parse(self) -> XSF:\n        data: t.Dict[str, t.Any] = {}\n        periodicity: Periodicity = 'molecule'\n\n        while (line := self.next_line()):\n            keyword = line.lstrip().split(maxsplit=1)[0].lower()\n            logging.debug(f\"Parsing keyword {keyword}\")\n\n            if keyword == 'animsteps':\n                raise ValueError(\"Animated XSF files are not supported.\")\n            elif keyword == 'atoms':\n                data['atoms'] = self.parse_atoms()\n            elif keyword in ('primcoord', 'convcoord'):\n                data[keyword] = self.parse_coords()\n            elif keyword in ('primvec', 'convvec'):\n                data[keyword] = self.parse_lattice()\n            elif keyword in ('crystal', 'slab', 'polymer', 'molecule'):\n                periodicity = keyword\n            elif keyword.startswith('begin_'):\n                self.eat_sandwich(keyword.removeprefix('begin_'))\n            elif keyword.startswith('end_'):\n                raise ValueError(f\"Unopened section close keyword '{keyword}'\")\n            else:\n                raise ValueError(f\"Unexpected keyword '{keyword.upper()}'.\")\n\n        if len(data) == 0:\n            raise ValueError(\"Unexpected EOF while parsing XSF file.\")\n\n        # most validation is performed in XSF\n        return XSF(\n            periodicity, atoms=data.get('atoms'),\n            prim_coords=data.get('primcoord'),\n            conv_coords=data.get('convcoord'),\n            primitive_cell=data.get('primvec'),\n            conventional_cell=data.get('convvec'),\n        )\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSFParser.lineno","title":"lineno instance-attribute","text":"
    lineno = 0\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSFParser.skip_line","title":"skip_line","text":"
    skip_line(line: Optional[str]) -> bool\n
    Source code in atomlib/io/xsf.py
    def skip_line(self, line: t.Optional[str]) -> bool:\n    return line is None or line.isspace() or line.lstrip().startswith('#')\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSFParser.peek_line","title":"peek_line","text":"
    peek_line() -> Optional[str]\n
    Source code in atomlib/io/xsf.py
    def peek_line(self) -> t.Optional[str]:\n    try:\n        while self.skip_line(self._peek_line):\n            self._peek_line = next(self._file)\n            self.lineno += 1\n        return self._peek_line\n    except StopIteration:\n        return None\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSFParser.next_line","title":"next_line","text":"
    next_line() -> Optional[str]\n
    Source code in atomlib/io/xsf.py
    def next_line(self) -> t.Optional[str]:\n    line = self.peek_line()\n    self._peek_line = None\n    return line\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSFParser.parse_atoms","title":"parse_atoms","text":"
    parse_atoms(\n    expected_length: Optional[int] = None,\n) -> DataFrame\n
    Source code in atomlib/io/xsf.py
    def parse_atoms(self, expected_length: t.Optional[int] = None) -> polars.DataFrame:\n    zs = []\n    coords = []\n    words = None\n\n    while (line := self.peek_line()):\n        words = line.split()\n        if len(words) == 0:\n            continue\n        if words[0].isalpha():\n            break\n        self.next_line()\n        try:\n            z = int(words[0])\n            if z < 0 or z > 118:\n                raise ValueError()\n        except (ValueError, TypeError):\n            raise ValueError(f\"Invalid atomic number '{words[0]}'\") from None\n\n        try:\n            coords.append(numpy.array(list(map(float, words[1:]))))\n            zs.append(z)\n        except (ValueError, TypeError):\n            raise ValueError(f\"Invalid atomic coordinates '{' '.join(words[1:])}'\") from None\n\n    if expected_length is not None:\n        if not expected_length == len(zs):\n            logging.warning(f\"Warning: List length {len(zs)} doesn't match declared length {expected_length}\")\n    elif len(zs) == 0:\n        raise ValueError(f\"Expected atom list after keyword 'ATOMS'. Got '{line or 'EOF'}' instead.\")\n\n    if len(zs) == 0:\n        return polars.DataFrame({}, schema=['elem', 'x', 'y', 'z'])  # type: ignore\n\n    coord_lens = list(map(len, coords))\n    if not all(coord_len == coord_lens[0] for coord_len in coord_lens[1:]):\n        raise ValueError(\"Mismatched atom dimensions.\")\n    if coord_lens[0] < 3:\n        raise ValueError(\"Expected at least 3 coordinates per atom.\")\n\n    coords = numpy.stack(coords, axis=0)[:, :3]\n    (x, y, z) = map(lambda a: a[:, 0], numpy.split(coords, 3, axis=1))\n\n    return polars.DataFrame({'elem': zs, 'x': x, 'y': y, 'z': z})\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSFParser.parse_coords","title":"parse_coords","text":"
    parse_coords() -> DataFrame\n
    Source code in atomlib/io/xsf.py
    def parse_coords(self) -> polars.DataFrame:\n    line = self.next_line()\n    if line is None:\n        raise ValueError(\"Unexpected EOF before atom list\")\n    words = line.split()\n    try:\n        if not len(words) == 2:\n            raise ValueError()\n        (n, _) = map(int, words)\n    except (ValueError, TypeError):\n        raise ValueError(f\"Invalid atom list length: {line}\") from None\n\n    return self.parse_atoms(n)\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSFParser.parse_lattice","title":"parse_lattice","text":"
    parse_lattice() -> LinearTransform3D\n
    Source code in atomlib/io/xsf.py
    def parse_lattice(self) -> LinearTransform3D:\n    rows = []\n    for _ in range(3):\n        line = self.next_line()\n        if line is None:\n            raise ValueError(\"Unexpected EOF in vector section.\")\n        words = line.split()\n        try:\n            if not len(words) == 3:\n                raise ValueError()\n            row = numpy.array(list(map(float, words)))\n            rows.append(row)\n        except (ValueError, TypeError):\n            raise ValueError(f\"Invalid lattice vector: {line}\") from None\n\n    matrix = numpy.stack(rows, axis=-1)\n    return LinearTransform3D(matrix)\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSFParser.eat_sandwich","title":"eat_sandwich","text":"
    eat_sandwich(keyword: str)\n
    Source code in atomlib/io/xsf.py
    def eat_sandwich(self, keyword: str):\n    begin_keyword = 'begin_' + keyword\n    end_keyword = 'end_' + keyword\n    lineno = self.lineno\n\n    while (line := self.next_line()):\n        keyword = line.lstrip().split(maxsplit=1)[0].lower()\n        if keyword.lower() == begin_keyword:\n            # recurse to inner (identical) section\n            self.eat_sandwich(keyword)\n            continue\n        if keyword.lower() == end_keyword:\n            break\n    else:\n        raise ValueError(f\"Unclosed section '{keyword}' opened at line {lineno}\")\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSFParser.parse","title":"parse","text":"
    parse() -> XSF\n
    Source code in atomlib/io/xsf.py
    def parse(self) -> XSF:\n    data: t.Dict[str, t.Any] = {}\n    periodicity: Periodicity = 'molecule'\n\n    while (line := self.next_line()):\n        keyword = line.lstrip().split(maxsplit=1)[0].lower()\n        logging.debug(f\"Parsing keyword {keyword}\")\n\n        if keyword == 'animsteps':\n            raise ValueError(\"Animated XSF files are not supported.\")\n        elif keyword == 'atoms':\n            data['atoms'] = self.parse_atoms()\n        elif keyword in ('primcoord', 'convcoord'):\n            data[keyword] = self.parse_coords()\n        elif keyword in ('primvec', 'convvec'):\n            data[keyword] = self.parse_lattice()\n        elif keyword in ('crystal', 'slab', 'polymer', 'molecule'):\n            periodicity = keyword\n        elif keyword.startswith('begin_'):\n            self.eat_sandwich(keyword.removeprefix('begin_'))\n        elif keyword.startswith('end_'):\n            raise ValueError(f\"Unopened section close keyword '{keyword}'\")\n        else:\n            raise ValueError(f\"Unexpected keyword '{keyword.upper()}'.\")\n\n    if len(data) == 0:\n        raise ValueError(\"Unexpected EOF while parsing XSF file.\")\n\n    # most validation is performed in XSF\n    return XSF(\n        periodicity, atoms=data.get('atoms'),\n        prim_coords=data.get('primcoord'),\n        conv_coords=data.get('convcoord'),\n        primitive_cell=data.get('primvec'),\n        conventional_cell=data.get('convvec'),\n    )\n
    "},{"location":"api/io/xyz/","title":"atomlib.io.xyz","text":"

    IO for the informal XYZ file format.

    Supports the extended XYZ format, described here.

    "},{"location":"api/io/xyz/#atomlib.io.xyz.XYZFormat","title":"XYZFormat module-attribute","text":"
    XYZFormat: TypeAlias = Literal['xyz', 'exyz']\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.XYZ","title":"XYZ dataclass","text":"Source code in atomlib/io/xyz.py
    @dataclass\nclass XYZ:\n    atoms: polars.DataFrame\n    comment: t.Optional[str] = None\n    params: t.Dict[str, str] = field(default_factory=dict)\n\n    @staticmethod\n    def from_atoms(atoms: HasAtoms) -> XYZ:\n        params = {}\n        if isinstance(atoms, HasAtomCell):\n            coords = atoms.get_cell().to_ortho().to_linear().inner.ravel()\n            lattice_str = \" \".join((f\"{c:.8f}\" for c in coords))\n            params['Lattice'] = lattice_str\n\n            pbc_str = \" \".join(str(int(v)) for v in atoms.get_cell().pbc)\n            params['pbc'] = pbc_str\n\n        return XYZ(\n            atoms.get_atoms('local')._get_frame(),\n            params=params\n        )\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> XYZ:\n        logging.info(f\"Loading XYZ {file.name if hasattr(file, 'name') else file!r}...\")  # type: ignore\n\n        with open_file(file, 'r') as f:\n            try:\n                # TODO be more gracious about whitespace here\n                length = int(f.readline())\n            except ValueError:\n                raise ValueError(\"Error parsing XYZ file: Invalid length\") from None\n            except IOError as e:\n                raise IOError(f\"Error parsing XYZ file: {e}\") from None\n\n            comment = f.readline().rstrip('\\n')\n            # TODO handle if there's not a gap here\n\n            try:\n                params = ExtXYZParser(comment).parse()\n            except ValueError:\n                params = None\n\n            schema = _get_columns_from_params(params)\n\n            df = parse_whitespace_separated(f, schema, start_line=1)\n\n            # map atomic numbers -> symbols (on columns which are Int8)\n            df = df.with_columns(\n                get_sym(df.select(polars.col('symbol').cast(polars.Int8, strict=False)).to_series())\n                    .fill_null(df['symbol']).alias('symbol')\n            )\n            # ensure all symbols are recognizable (this will raise ValueError if not)\n            get_elem(df['symbol'])\n\n            if length < len(df):\n                warnings.warn(f\"Warning: truncating structure of length {len(df)} \"\n                            f\"to match declared length of {length}\")\n                df = df[:length]\n            elif length > len(df):\n                warnings.warn(f\"Warning: structure length {len(df)} doesn't match \"\n                            f\"declared length {length}.\\nData could be corrupted.\")\n\n            try:\n                params = ExtXYZParser(comment).parse()\n                return XYZ(df, comment, params)\n            except ValueError:\n                pass\n\n            return XYZ(df, comment)\n\n    def write(self, file: FileOrPath, fmt: XYZFormat = 'exyz'):\n        with open_file(file, 'w', newline='\\r\\n') as f:\n\n            f.write(f\"{len(self.atoms)}\\n\")\n            if len(self.params) > 0 and fmt == 'exyz':\n                f.write(\" \".join(_param_strings(self.params)))\n            else:\n                f.write(self.comment or \"\")\n            f.write(\"\\n\")\n\n            # not my best work\n            col_space = (3, 12, 12, 12)\n            f.writelines(\n                \"\".join(\n                    f\"{val:< {space}.8f}\" if isinstance(val, float) else f\"{val:<{space}}\" for (val, space) in zip(_flatten(row), col_space)\n                ).strip() + '\\n' for row in self.atoms.select(('symbol', 'coords')).rows()\n            )\n\n    def cell_matrix(self) -> t.Optional[NDArray[numpy.float64]]:\n        if (s := self.params.get('Lattice')) is None:\n            return None\n\n        try:\n            items = list(map(float, s.split()))\n            if not len(items) == 9:\n                raise ValueError(\"Invalid length\")\n            return numpy.array(items, dtype=numpy.float64).reshape((3, 3)).T\n        except ValueError:\n            warnings.warn(f\"Warning: Invalid format for key 'Lattice=\\\"{s}\\\"'\")\n        return None\n\n    def pbc(self) -> t.Optional[NDArray[numpy.bool_]]:\n        if (s := self.params.get('pbc')) is None:\n            return None\n\n        val_map = {'0': False, 'f': False, '1': True, 't': True}\n        try:\n            items = [val_map[v.lower()] for v in s.split()]\n            if not len(items) == 3:\n                raise ValueError(\"Invalid length\")\n            return numpy.array(items, dtype=numpy.bool_)\n        except ValueError:\n            warnings.warn(f\"Warning: Invalid format for key 'pbc=\\\"{s}\\\"'\")\n        return None\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.XYZ.atoms","title":"atoms instance-attribute","text":"
    atoms: DataFrame\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.XYZ.comment","title":"comment class-attribute instance-attribute","text":"
    comment: Optional[str] = None\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.XYZ.params","title":"params class-attribute instance-attribute","text":"
    params: Dict[str, str] = field(default_factory=dict)\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.XYZ.from_atoms","title":"from_atoms staticmethod","text":"
    from_atoms(atoms: HasAtoms) -> XYZ\n
    Source code in atomlib/io/xyz.py
    @staticmethod\ndef from_atoms(atoms: HasAtoms) -> XYZ:\n    params = {}\n    if isinstance(atoms, HasAtomCell):\n        coords = atoms.get_cell().to_ortho().to_linear().inner.ravel()\n        lattice_str = \" \".join((f\"{c:.8f}\" for c in coords))\n        params['Lattice'] = lattice_str\n\n        pbc_str = \" \".join(str(int(v)) for v in atoms.get_cell().pbc)\n        params['pbc'] = pbc_str\n\n    return XYZ(\n        atoms.get_atoms('local')._get_frame(),\n        params=params\n    )\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.XYZ.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> XYZ\n
    Source code in atomlib/io/xyz.py
    @staticmethod\ndef from_file(file: FileOrPath) -> XYZ:\n    logging.info(f\"Loading XYZ {file.name if hasattr(file, 'name') else file!r}...\")  # type: ignore\n\n    with open_file(file, 'r') as f:\n        try:\n            # TODO be more gracious about whitespace here\n            length = int(f.readline())\n        except ValueError:\n            raise ValueError(\"Error parsing XYZ file: Invalid length\") from None\n        except IOError as e:\n            raise IOError(f\"Error parsing XYZ file: {e}\") from None\n\n        comment = f.readline().rstrip('\\n')\n        # TODO handle if there's not a gap here\n\n        try:\n            params = ExtXYZParser(comment).parse()\n        except ValueError:\n            params = None\n\n        schema = _get_columns_from_params(params)\n\n        df = parse_whitespace_separated(f, schema, start_line=1)\n\n        # map atomic numbers -> symbols (on columns which are Int8)\n        df = df.with_columns(\n            get_sym(df.select(polars.col('symbol').cast(polars.Int8, strict=False)).to_series())\n                .fill_null(df['symbol']).alias('symbol')\n        )\n        # ensure all symbols are recognizable (this will raise ValueError if not)\n        get_elem(df['symbol'])\n\n        if length < len(df):\n            warnings.warn(f\"Warning: truncating structure of length {len(df)} \"\n                        f\"to match declared length of {length}\")\n            df = df[:length]\n        elif length > len(df):\n            warnings.warn(f\"Warning: structure length {len(df)} doesn't match \"\n                        f\"declared length {length}.\\nData could be corrupted.\")\n\n        try:\n            params = ExtXYZParser(comment).parse()\n            return XYZ(df, comment, params)\n        except ValueError:\n            pass\n\n        return XYZ(df, comment)\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.XYZ.write","title":"write","text":"
    write(file: FileOrPath, fmt: XYZFormat = 'exyz')\n
    Source code in atomlib/io/xyz.py
    def write(self, file: FileOrPath, fmt: XYZFormat = 'exyz'):\n    with open_file(file, 'w', newline='\\r\\n') as f:\n\n        f.write(f\"{len(self.atoms)}\\n\")\n        if len(self.params) > 0 and fmt == 'exyz':\n            f.write(\" \".join(_param_strings(self.params)))\n        else:\n            f.write(self.comment or \"\")\n        f.write(\"\\n\")\n\n        # not my best work\n        col_space = (3, 12, 12, 12)\n        f.writelines(\n            \"\".join(\n                f\"{val:< {space}.8f}\" if isinstance(val, float) else f\"{val:<{space}}\" for (val, space) in zip(_flatten(row), col_space)\n            ).strip() + '\\n' for row in self.atoms.select(('symbol', 'coords')).rows()\n        )\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.XYZ.cell_matrix","title":"cell_matrix","text":"
    cell_matrix() -> Optional[NDArray[float64]]\n
    Source code in atomlib/io/xyz.py
    def cell_matrix(self) -> t.Optional[NDArray[numpy.float64]]:\n    if (s := self.params.get('Lattice')) is None:\n        return None\n\n    try:\n        items = list(map(float, s.split()))\n        if not len(items) == 9:\n            raise ValueError(\"Invalid length\")\n        return numpy.array(items, dtype=numpy.float64).reshape((3, 3)).T\n    except ValueError:\n        warnings.warn(f\"Warning: Invalid format for key 'Lattice=\\\"{s}\\\"'\")\n    return None\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.XYZ.pbc","title":"pbc","text":"
    pbc() -> Optional[NDArray[bool_]]\n
    Source code in atomlib/io/xyz.py
    def pbc(self) -> t.Optional[NDArray[numpy.bool_]]:\n    if (s := self.params.get('pbc')) is None:\n        return None\n\n    val_map = {'0': False, 'f': False, '1': True, 't': True}\n    try:\n        items = [val_map[v.lower()] for v in s.split()]\n        if not len(items) == 3:\n            raise ValueError(\"Invalid length\")\n        return numpy.array(items, dtype=numpy.bool_)\n    except ValueError:\n        warnings.warn(f\"Warning: Invalid format for key 'pbc=\\\"{s}\\\"'\")\n    return None\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.ExtXYZParser","title":"ExtXYZParser","text":"Source code in atomlib/io/xyz.py
    class ExtXYZParser:\n    def __init__(self, comment: str):\n        self._tokens = list(filter(len, _EXT_TOKEN_RE.split(comment)))\n        self._tokens.reverse()\n\n    def peek(self) -> t.Optional[str]:\n        return None if len(self._tokens) == 0 else self._tokens[-1]\n\n    def next(self) -> str:\n        return self._tokens.pop()\n\n    def skip_wspace(self):\n        word = self.peek()\n        while word is not None and word.isspace():\n            self.next()\n            word = self.peek()\n\n    def parse(self) -> t.Dict[str, str]:\n        self.skip_wspace()\n        d = {}\n        while len(self._tokens) > 0:\n            key = self.parse_val()\n            eq = self.next()\n            if not eq == \"=\":\n                raise ValueError(f\"Expected key-value separator, instead got '{eq}'\")\n            val = self.parse_val()\n            d[key] = val\n            self.skip_wspace()\n        return d\n\n    def parse_val(self) -> str:\n        token = self.peek()\n        if token == \"=\":\n            raise ValueError(\"Expected value, instead got '='\")\n        if not token == \"\\\"\":\n            return self.next()\n\n        # quoted string\n        self.next()\n        words = []\n        while not (word := self.peek()) == \"\\\"\":\n            if word is None:\n                raise ValueError(\"EOF while parsing string value\")\n            words += self.next()\n        self.next()\n        return \"\".join(words)\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.ExtXYZParser.peek","title":"peek","text":"
    peek() -> Optional[str]\n
    Source code in atomlib/io/xyz.py
    def peek(self) -> t.Optional[str]:\n    return None if len(self._tokens) == 0 else self._tokens[-1]\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.ExtXYZParser.next","title":"next","text":"
    next() -> str\n
    Source code in atomlib/io/xyz.py
    def next(self) -> str:\n    return self._tokens.pop()\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.ExtXYZParser.skip_wspace","title":"skip_wspace","text":"
    skip_wspace()\n
    Source code in atomlib/io/xyz.py
    def skip_wspace(self):\n    word = self.peek()\n    while word is not None and word.isspace():\n        self.next()\n        word = self.peek()\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.ExtXYZParser.parse","title":"parse","text":"
    parse() -> Dict[str, str]\n
    Source code in atomlib/io/xyz.py
    def parse(self) -> t.Dict[str, str]:\n    self.skip_wspace()\n    d = {}\n    while len(self._tokens) > 0:\n        key = self.parse_val()\n        eq = self.next()\n        if not eq == \"=\":\n            raise ValueError(f\"Expected key-value separator, instead got '{eq}'\")\n        val = self.parse_val()\n        d[key] = val\n        self.skip_wspace()\n    return d\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.ExtXYZParser.parse_val","title":"parse_val","text":"
    parse_val() -> str\n
    Source code in atomlib/io/xyz.py
    def parse_val(self) -> str:\n    token = self.peek()\n    if token == \"=\":\n        raise ValueError(\"Expected value, instead got '='\")\n    if not token == \"\\\"\":\n        return self.next()\n\n    # quoted string\n    self.next()\n    words = []\n    while not (word := self.peek()) == \"\\\"\":\n        if word is None:\n            raise ValueError(\"EOF while parsing string value\")\n        words += self.next()\n    self.next()\n    return \"\".join(words)\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.batched","title":"batched","text":"
    batched(\n    iterable: Iterable[T], n: int\n) -> Iterator[Tuple[T, ...]]\n
    Source code in atomlib/io/xyz.py
    def batched(iterable: t.Iterable[T], n: int) -> t.Iterator[t.Tuple[T, ...]]:\n    if n < 1:\n        raise ValueError('n must be at least one')\n    it = iter(iterable)\n    while batch := tuple(islice(it, n)):\n        yield batch\n
    "},{"location":"using/coords/","title":"Coordinate systems","text":"

    Under construction

    "},{"location":"using/getting_started/","title":"Getting started","text":"

    atomlib is a package for creating, modifying, and controlling atomic structures. It focuses on flexibilty, correctness, and the preservation of structure metadata, while still managing to support a wide variety of file formats.

    "}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"atomlib: A modern, extensible library for creating atomic structures","text":"

    atomlib is a package for creating, modifying, and controlling atomic structures. It draws heavy inspiration from previous tools like Atomsk and ASE, but attempts to provide a cleaner, more consistent interface that can be used from Python or a command line.

    atomlib has minimal dependencies: numpy, scipy, and polars are required for core atom structure manipulation, and click is required for command line functionality.

    "},{"location":"#atomic-representation-supported-properties","title":"Atomic representation & supported properties","text":"

    Atomic structures are stored as polars DataFrames, providing a clean, immutable interface that maximizes expressiveness and minimizes errors. For formats that allow arbitrary properties, these properties can be passed through transparently. atomlib has first-class support for fractional occupancy, Debye-Waller factors, atomic forces, and labels.

    Translational symmetry is stored in Cell objects, which represent a fully generic cell. Atoms can be modified in any coordinate system that makes sense (global, local real-space, cell fraction, box fraction, etc.). Support for non-translational symmetry operations is limited at this point.

    For more information, check out the example notebooks and the API documentation.

    "},{"location":"#currently-supported-file-formats","title":"Currently supported file formats","text":"

    File format support is still a work in progress. Where possible, parsers are implemented from scratch in-repo. Most formats are implemented in two steps: Parsing to an intermediate representation which preserves all format-specific information, and then conversion to the generic Atoms & AtomCell types for manipulation & display. This means you can write your own code to utilize advanced format features even if they're not supported out of the box.

    Format Ext. Read Write Notes CIF .cif CIF1 & CIF2. Isotropic B-factor only XCrysDen .xsf AtomEye CFG .cfg Currently basic format only Basic XYZ .xyz Ext. XYZ .exyz Arbitrary properties not implemented Special XYZ .sxyz To be implemented LAMMPS Data .lmp Quantum Espresso .qe pw.x format pyMultislicer .mslice Currently XML format only"},{"location":"api/","title":"Index","text":"

    Top-level exports:

    "},{"location":"api/#atomlib.AtomSelection","title":"AtomSelection module-attribute","text":"
    AtomSelection: TypeAlias = Union[\n    IntoExprColumn,\n    NDArray[bool_],\n    ArrayLike,\n    Mapping[str, Any],\n]\n

    Polars expression selecting a subset of atoms. Can be used with many Atoms methods.

    "},{"location":"api/#atomlib.CoordinateFrame","title":"CoordinateFrame module-attribute","text":"
    CoordinateFrame: TypeAlias = Literal[\n    \"cell\",\n    \"cell_frac\",\n    \"cell_box\",\n    \"ortho\",\n    \"ortho_frac\",\n    \"ortho_box\",\n    \"linear\",\n    \"local\",\n    \"global\",\n]\n

    A coordinate frame to use.

    • cell: Real-space units along crystal axes
    • cell_frac: Fraction of unit cells
    • cell_box: Fraction of cell box
    • ortho: Real-space units along orthogonal cell
    • ortho_frac: Fraction of orthogonal cell
    • ortho_box: Fraction of orthogonal box
    • linear: Angstroms in local coordinate system (without affine transformation)
    • local: Angstroms in local coordinate system (with affine transformation)
    • global: Angstroms in global coordinate system (with all transformations)

    For more information, see the documentation at Coordinate systems, or the example notebook at examples/coords.ipynb.

    "},{"location":"api/#atomlib.Atoms","title":"Atoms","text":"

    Bases: AtomsIOMixin, HasAtoms

    A collection of atoms, absent any implied coordinate system. Implemented as a wrapper around a polars.DataFrame.

    Must contain the following columns:

    • coords: array of [x, y, z] positions, float
    • elem: atomic number, int
    • symbol: atomic symbol (may contain charges)

    In addition, it commonly contains the following columns:

    • i: Initial atom number
    • wobble: Isotropic Debye-Waller mean-squared deviation (\\(\\left<u^2\\right> = B \\cdot \\frac{3}{8 \\pi^2}\\), dimensions of [Length^2])
    • frac_occupancy: Fractional occupancy, in the range [0., 1.]
    • mass: Atomic mass, in g/mol (approx. Da)
    • velocity: array of [x, y, z] velocities, float, dimensions of length/time
    • type: Numeric atom type, as used by programs like LAMMPS
    Source code in atomlib/atoms.py
    class Atoms(AtomsIOMixin, HasAtoms):\n    r\"\"\"\n    A collection of atoms, absent any implied coordinate system.\n    Implemented as a wrapper around a [`polars.DataFrame`][polars.DataFrame].\n\n    Must contain the following columns:\n\n    - coords: array of `[x, y, z]` positions, float\n    - elem: atomic number, int\n    - symbol: atomic symbol (may contain charges)\n\n    In addition, it commonly contains the following columns:\n\n    - i: Initial atom number\n    - wobble: Isotropic Debye-Waller mean-squared deviation ($\\left<u^2\\right> = B \\cdot \\frac{3}{8 \\pi^2}$, dimensions of [Length^2])\n    - frac_occupancy: Fractional occupancy, in the range [0., 1.]\n    - mass: Atomic mass, in g/mol (approx. Da)\n    - velocity: array of `[x, y, z]` velocities, float, dimensions of length/time\n    - type: Numeric atom type, as used by programs like LAMMPS\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n\n    def __init__(self, data: t.Optional[IntoAtoms] = None, columns: t.Optional[t.Sequence[str]] = None,\n                 orient: t.Union[t.Literal['row'], t.Literal['col'], None] = None,\n                 _unchecked: bool = False):\n        self._bbox: t.Optional[BBox3D] = None\n        self.inner: polars.DataFrame\n\n        if data is None:\n            assert columns is None\n            self.inner = polars.DataFrame([\n                polars.Series('coords', (), dtype=polars.Array(polars.Float64, 3)),\n                polars.Series('elem', (), dtype=polars.Int8),\n                polars.Series('symbol', (), dtype=polars.Utf8),\n            ])\n        elif isinstance(data, polars.DataFrame):\n            self.inner = data\n        elif isinstance(data, Atoms):\n            self.inner = data.inner\n            _unchecked = True\n        else:\n            self.inner = polars.DataFrame(data, schema=columns, orient=orient)\n\n        if not _unchecked:\n            # stack ('x', 'y', 'z') -> 'coords'\n            self.inner = _with_columns_stacked(self.inner, ('x', 'y', 'z'), 'coords')\n            self.inner = _with_columns_stacked(self.inner, ('v_x', 'v_y', 'v_z'), 'velocity')\n\n            missing: t.Tuple[str, ...] = tuple(set(['symbol', 'elem']) - set(self.columns))\n            if len(missing) > 1:\n                raise ValueError(\"'Atoms' missing columns 'elem' and/or 'symbol'.\")\n            # fill 'symbol' from 'elem' or vice-versa\n            if missing == ('symbol',):\n                self.inner = self.inner.with_columns(get_sym(self.inner['elem']))\n            elif missing == ('elem',):\n                # by convention, add before 'symbol' column\n                self.inner = self.inner.insert_column(\n                    self.inner.get_column_index('symbol'),\n                    get_elem(self.inner['symbol']),\n                )\n\n            # cast to standard dtypes\n            self.inner = self.inner.with_columns([\n                self.inner[col].cast(dtype)\n                for (col, dtype) in _COLUMN_DTYPES.items() if col in self.inner\n            ])\n\n            self._validate_atoms()\n\n    @staticmethod\n    def empty() -> Atoms:\n        \"\"\"\n        Return an empty Atoms with only the mandatory columns.\n        \"\"\"\n        return Atoms()\n\n    def _validate_atoms(self):\n        missing = [col for col in _REQUIRED_COLUMNS if col not in self.columns]\n        if len(missing):\n            raise ValueError(f\"'Atoms' missing column(s) {', '.join(map(repr, missing))}\")\n\n    def get_atoms(self, frame: t.Literal['local'] = 'local') -> Atoms:\n        if frame != 'local':\n            raise ValueError(f\"Atoms without a cell only support the 'local' coordinate frame, not '{frame}'.\")\n        return self\n\n    def with_atoms(self, atoms: HasAtoms, frame: t.Literal['local'] = 'local') -> Atoms:\n        if frame != 'local':\n            raise ValueError(f\"Atoms without a cell only support the 'local' coordinate frame, not '{frame}'.\")\n        return atoms.get_atoms()\n\n    @classmethod\n    def _combine_metadata(cls: t.Type[Atoms], *atoms: HasAtoms) -> Atoms:\n        return cls.empty()\n\n    def bbox(self) -> BBox3D:\n        \"\"\"Return the bounding box of all the points in `self`.\"\"\"\n        if self._bbox is None:\n            self._bbox = BBox3D.from_pts(self.coords())\n\n        return self._bbox\n\n    def __str__(self) -> str:\n        return f\"Atoms, {self.inner!s}\"\n\n    def __repr__(self) -> str:\n        buf = StringIO()\n        buf.write(\"Atoms([\\n\")\n\n        for series in self.inner.to_dict().values():\n            buf.write(f\"    Series({series.name!r}, {series.to_list()!r}, dtype={series.dtype!r}),\\n\")\n\n        buf.write(\"])\\n\")\n        return buf.getvalue()\n\n    def _repr_pretty_(self, p: t.Any, cycle: bool) -> None:\n        p.text('Atoms(...)') if cycle else p.text(str(self))\n
    "},{"location":"api/#atomlib.Atoms.columns","title":"columns property","text":"
    columns: List[str]\n

    Return the column names in self.

    RETURNS DESCRIPTION List[str]

    A sequence of column names

    "},{"location":"api/#atomlib.Atoms.dtypes","title":"dtypes property","text":"
    dtypes: List[DataType]\n

    Return the datatypes in self.

    RETURNS DESCRIPTION List[DataType]

    A sequence of column DataTypes

    "},{"location":"api/#atomlib.Atoms.schema","title":"schema property","text":"
    schema: Schema\n

    Return the schema of self.

    RETURNS DESCRIPTION Schema

    A dictionary of column names and DataTypes

    "},{"location":"api/#atomlib.Atoms.with_column","title":"with_column class-attribute instance-attribute","text":"
    with_column = with_columns\n
    "},{"location":"api/#atomlib.Atoms.transform","title":"transform class-attribute instance-attribute","text":"
    transform = transform_atoms\n
    "},{"location":"api/#atomlib.Atoms.crop_atoms","title":"crop_atoms class-attribute instance-attribute","text":"
    crop_atoms = crop\n
    "},{"location":"api/#atomlib.Atoms.unique","title":"unique class-attribute instance-attribute","text":"
    unique = deduplicate\n
    "},{"location":"api/#atomlib.Atoms.inner","title":"inner instance-attribute","text":"
    inner: DataFrame\n
    "},{"location":"api/#atomlib.Atoms.describe","title":"describe","text":"
    describe(\n    percentiles: Union[Sequence[float], float, None] = (\n        0.25,\n        0.5,\n        0.75,\n    ),\n    *,\n    interpolation: RollingInterpolationMethod = \"nearest\"\n) -> DataFrame\n

    Return summary statistics for self. See DataFrame.describe for more information.

    PARAMETER DESCRIPTION percentiles

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), 50% (median), and 75% (third quartile).

    TYPE: Union[Sequence[float], float, None] DEFAULT: (0.25, 0.5, 0.75)

    RETURNS DESCRIPTION DataFrame

    A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.describe)\ndef describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n             interpolation: RollingInterpolationMethod = 'nearest') -> polars.DataFrame:\n    \"\"\"\n    Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n    Args:\n      percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                   50% (median), and 75% (third quartile).\n\n    Returns:\n      A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.Atoms.with_columns","title":"with_columns","text":"
    with_columns(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Return a copy of self with the given columns added.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef with_columns(self,\n                 *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 **named_exprs: IntoExpr) -> polars.DataFrame:\n    \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n    return self._get_frame().with_columns(*exprs, **named_exprs)\n
    "},{"location":"api/#atomlib.Atoms.insert_column","title":"insert_column","text":"
    insert_column(index: int, column: Series) -> DataFrame\n
    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n    return self._get_frame().insert_column(index, column)\n
    "},{"location":"api/#atomlib.Atoms.get_column","title":"get_column","text":"
    get_column(name: str) -> Series\n

    Get the specified column from self, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(lambda df, name: df.get_column(name))\ndef get_column(self, name: str) -> polars.Series:\n    \"\"\"\n    Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.Atoms.get_columns","title":"get_columns","text":"
    get_columns() -> List[Series]\n

    Return all columns from self as a list of Series.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_columns)\ndef get_columns(self) -> t.List[polars.Series]:\n    \"\"\"\n    Return all columns from `self` as a list of [`Series`][polars.Series].\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.Atoms.get_column_index","title":"get_column_index","text":"
    get_column_index(name: str) -> int\n

    Get the index of a column by name, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_column_index)\ndef get_column_index(self, name: str) -> int:\n    \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.Atoms.group_by","title":"group_by","text":"
    group_by(\n    *by: Union[IntoExpr, Iterable[IntoExpr]],\n    maintain_order: bool = False,\n    **named_by: IntoExpr\n) -> GroupBy\n

    Start a group by operation. See DataFrame.group_by for more information.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.group_by)\ndef group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]], maintain_order: bool = False,\n             **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n    \"\"\"\n    Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.Atoms.pipe","title":"pipe","text":"
    pipe(\n    function: Callable[Concatenate[HasAtomsT, P], T],\n    *args: args,\n    **kwargs: kwargs\n) -> T\n

    Apply function to self (in method-call syntax).

    Source code in atomlib/atoms.py
    def pipe(self: HasAtomsT, function: t.Callable[Concatenate[HasAtomsT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n    \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n    return function(self, *args, **kwargs)\n
    "},{"location":"api/#atomlib.Atoms.clone","title":"clone","text":"
    clone() -> DataFrame\n

    Return a copy of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef clone(self) -> polars.DataFrame:\n    \"\"\"Return a copy of `self`.\"\"\"\n    return self._get_frame().clone()\n
    "},{"location":"api/#atomlib.Atoms.drop","title":"drop","text":"
    drop(\n    *columns: Union[str, Iterable[str]], strict: bool = True\n) -> DataFrame\n

    Return self with the specified columns removed.

    Source code in atomlib/atoms.py
    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n    \"\"\"Return `self` with the specified columns removed.\"\"\"\n    return self._get_frame().drop(*columns, strict=strict)\n
    "},{"location":"api/#atomlib.Atoms.filter","title":"filter","text":"
    filter(\n    *predicates: Union[\n        None,\n        IntoExprColumn,\n        Iterable[IntoExprColumn],\n        bool,\n        List[bool],\n        ndarray,\n    ],\n    **constraints: Any\n) -> Self\n

    Filter self, removing rows which evaluate to False.

    Source code in atomlib/atoms.py
    def filter(\n    self,\n    *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n    **constraints: t.Any,\n) -> Self:\n    \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n    # TODO clean up\n    preds_not_none = tuple(filter(lambda p: p is not None, predicates))\n    if not len(preds_not_none) and not len(constraints):\n        return self\n    return self.with_atoms(Atoms(self._get_frame().filter(*preds_not_none, **constraints), _unchecked=True))  # type: ignore\n
    "},{"location":"api/#atomlib.Atoms.sort","title":"sort","text":"
    sort(\n    by: Union[IntoExpr, Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: Union[bool, Sequence[bool]] = False,\n    nulls_last: bool = False\n) -> DataFrame\n

    Sort the atoms in self by the given columns/expressions.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef sort(\n    self,\n    by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: t.Union[bool, t.Sequence[bool]] = False,\n    nulls_last: bool = False,\n) -> polars.DataFrame:\n    \"\"\"\n    Sort the atoms in `self` by the given columns/expressions.\n    \"\"\"\n    return self._get_frame().sort(\n        by, *more_by, descending=descending, nulls_last=nulls_last\n    )\n
    "},{"location":"api/#atomlib.Atoms.slice","title":"slice","text":"
    slice(\n    offset: int, length: Optional[int] = None\n) -> DataFrame\n

    Return a slice of the rows in self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef slice(self, offset: int, length: t.Optional[int] = None) -> polars.DataFrame:\n    \"\"\"Return a slice of the rows in `self`.\"\"\"\n    return self._get_frame().slice(offset, length)\n
    "},{"location":"api/#atomlib.Atoms.head","title":"head","text":"
    head(n: int = 5) -> DataFrame\n

    Return the first n rows of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef head(self, n: int = 5) -> polars.DataFrame:\n    \"\"\"Return the first `n` rows of `self`.\"\"\"\n    return self._get_frame().head(n)\n
    "},{"location":"api/#atomlib.Atoms.tail","title":"tail","text":"
    tail(n: int = 5) -> DataFrame\n

    Return the last n rows of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef tail(self, n: int = 5) -> polars.DataFrame:\n    \"\"\"Return the last `n` rows of `self`.\"\"\"\n    return self._get_frame().tail(n)\n
    "},{"location":"api/#atomlib.Atoms.drop_nulls","title":"drop_nulls","text":"
    drop_nulls(\n    subset: Union[str, Collection[str], None] = None\n) -> DataFrame\n

    Drop rows that contain nulls in any of columns subset.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n    \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n    return self._get_frame().drop_nulls(subset)\n
    "},{"location":"api/#atomlib.Atoms.fill_null","title":"fill_null","text":"
    fill_null(\n    value: Any = None,\n    strategy: Optional[FillNullStrategy] = None,\n    limit: Optional[int] = None,\n    matches_supertype: bool = True,\n) -> DataFrame\n

    Fill null values in self, using the specified value or strategy.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef fill_null(\n    self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n    limit: t.Optional[int] = None, matches_supertype: bool = True,\n) -> polars.DataFrame:\n    \"\"\"Fill null values in `self`, using the specified value or strategy.\"\"\"\n    return self._get_frame().fill_null(value, strategy, limit, matches_supertype=matches_supertype)\n
    "},{"location":"api/#atomlib.Atoms.fill_nan","title":"fill_nan","text":"
    fill_nan(value: Union[Expr, int, float, None]) -> DataFrame\n

    Fill floating-point NaN values in self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef fill_nan(self, value: t.Union[polars.Expr, int, float, None]) -> polars.DataFrame:\n    \"\"\"Fill floating-point NaN values in `self`.\"\"\"\n    return self._get_frame().fill_nan(value)\n
    "},{"location":"api/#atomlib.Atoms.concat","title":"concat classmethod","text":"
    concat(\n    atoms: Union[\n        HasAtomsT,\n        IntoAtoms,\n        Iterable[Union[HasAtomsT, IntoAtoms]],\n    ],\n    *,\n    rechunk: bool = True,\n    how: ConcatMethod = \"vertical\"\n) -> HasAtomsT\n

    Concatenate multiple Atoms together, handling metadata appropriately.

    Source code in atomlib/atoms.py
    @classmethod\ndef concat(cls: t.Type[HasAtomsT],\n           atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n           rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n    \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n    # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n    # same type as ``cls``.\n    if _is_abstract(cls):\n        raise TypeError(\"concat() must be called on a concrete class.\")\n\n    if isinstance(atoms, HasAtoms):\n        atoms = (atoms,)\n    dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n    representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n    if len(dfs) == 0:\n        return representative.with_atoms(Atoms.empty(), 'local')\n\n    if how in ('vertical', 'vertical_relaxed'):\n        # get order from first member\n        cols = dfs[0].columns\n        dfs = [df.select(cols) for df in dfs]\n    elif how == 'inner':\n        cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n        schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n        if len(schema) == 0:\n            raise ValueError(\"Atoms have no columns in common\")\n\n        dfs = [_select_schema(df, schema) for df in dfs]\n        how = 'vertical'\n\n    return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n
    "},{"location":"api/#atomlib.Atoms.partition_by","title":"partition_by","text":"
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: Literal[False] = False\n) -> List[Self]\n
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: Literal[True] = ...\n) -> Dict[Any, Self]\n
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: bool = False\n) -> Union[List[Self], Dict[Any, Self]]\n

    Group by the given columns and partition into separate dataframes.

    Return the partitions as a dictionary by specifying as_dict=True.

    Source code in atomlib/atoms.py
    def partition_by(\n    self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n    maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n    \"\"\"\n    Group by the given columns and partition into separate dataframes.\n\n    Return the partitions as a dictionary by specifying `as_dict=True`.\n    \"\"\"\n    if as_dict:\n        d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n        return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n    return [\n        self.with_atoms(Atoms(df, _unchecked=True))\n        for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n    ]\n
    "},{"location":"api/#atomlib.Atoms.select","title":"select","text":"
    select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.select)\ndef select(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr,\n) -> polars.DataFrame:\n    \"\"\"\n    Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.Atoms.select_schema","title":"select_schema","text":"
    select_schema(schema: SchemaDict) -> DataFrame\n

    Select columns from self and cast to the given schema. Raises TypeError if a column is not found or if it can't be cast.

    Source code in atomlib/atoms.py
    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n    \"\"\"\n    Select columns from `self` and cast to the given schema.\n    Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n    \"\"\"\n    return _select_schema(self, schema)\n
    "},{"location":"api/#atomlib.Atoms.select_props","title":"select_props","text":"
    select_props(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Self\n

    Select exprs from self, while keeping required columns.

    RETURNS DESCRIPTION Self

    A HasAtoms filtered to contain the

    Self

    specified properties (as well as required columns).

    Source code in atomlib/atoms.py
    def select_props(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Self:\n    \"\"\"\n    Select `exprs` from `self`, while keeping required columns.\n\n    Returns:\n      A [`HasAtoms`][atomlib.atoms.HasAtoms] filtered to contain the\n      specified properties (as well as required columns).\n    \"\"\"\n    props = self._get_frame().lazy().select(*exprs, **named_exprs).drop(_REQUIRED_COLUMNS, strict=False).collect(_eager=True)\n    return self.with_atoms(\n        Atoms(self._get_frame().select(_REQUIRED_COLUMNS).hstack(props), _unchecked=False)\n    )\n
    "},{"location":"api/#atomlib.Atoms.try_select","title":"try_select","text":"
    try_select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Optional[DataFrame]\n

    Try to select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns. Returns None if any columns are missing.

    Source code in atomlib/atoms.py
    def try_select(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr,\n) -> t.Optional[polars.DataFrame]:\n    \"\"\"\n    Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns. Returns `None` if any\n    columns are missing.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    try:\n        return self._get_frame().select(*exprs, **named_exprs)\n    except polars.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/#atomlib.Atoms.try_get_column","title":"try_get_column","text":"
    try_get_column(name: str) -> Optional[Series]\n

    Try to get a column from self, returning None if it doesn't exist.

    Source code in atomlib/atoms.py
    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n    \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n    try:\n        return self.get_column(name)\n    except polars.exceptions.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/#atomlib.Atoms.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n
    Source code in atomlib/atoms.py
    def assert_equal(self, other: t.Any):\n    assert isinstance(other, HasAtoms)\n    assert dict(self.schema) == dict(other.schema)\n    for col in self.schema.keys():\n        polars.testing.assert_series_equal(self[col], other[col], check_names=False, rtol=1e-3, atol=1e-8)\n
    "},{"location":"api/#atomlib.Atoms.bbox_atoms","title":"bbox_atoms","text":"
    bbox_atoms() -> BBox3D\n

    Return the bounding box of all the atoms in self.

    Source code in atomlib/atoms.py
    def bbox_atoms(self) -> BBox3D:\n    \"\"\"Return the bounding box of all the atoms in ``self``.\"\"\"\n    return BBox3D.from_pts(self.coords())\n
    "},{"location":"api/#atomlib.Atoms.transform_atoms","title":"transform_atoms","text":"
    transform_atoms(\n    transform: IntoTransform3D,\n    selection: Optional[AtomSelection] = None,\n    *,\n    transform_velocities: bool = False\n) -> Self\n

    Transform the atoms in self by transform. If selection is given, only transform the atoms in selection.

    Source code in atomlib/atoms.py
    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                    transform_velocities: bool = False) -> Self:\n    \"\"\"\n    Transform the atoms in `self` by `transform`.\n    If `selection` is given, only transform the atoms in `selection`.\n    \"\"\"\n    transform = Transform3D.make(transform)\n    selection = _selection_to_numpy(self, selection)\n    transformed = self.with_coords(Transform3D.make(transform) @ self.coords(selection), selection)\n    # try to transform velocities as well\n    if transform_velocities and (velocities := self.velocities(selection)) is not None:\n        return transformed.with_velocity(transform.transform_vec(velocities), selection)\n    return transformed\n
    "},{"location":"api/#atomlib.Atoms.round_near_zero","title":"round_near_zero","text":"
    round_near_zero(tol: float = 1e-14) -> Self\n

    Round atom position values near zero to zero.

    Source code in atomlib/atoms.py
    def round_near_zero(self, tol: float = 1e-14) -> Self:\n    \"\"\"\n    Round atom position values near zero to zero.\n    \"\"\"\n    return self.with_columns(coords=polars.concat_list(\n        polars.when(_coord_expr(col).abs() >= tol).then(_coord_expr(col)).otherwise(polars.lit(0.))\n        for col in range(3)\n    ).list.to_array(3))\n
    "},{"location":"api/#atomlib.Atoms.crop","title":"crop","text":"
    crop(\n    x_min: float = -inf,\n    x_max: float = inf,\n    y_min: float = -inf,\n    y_max: float = inf,\n    z_min: float = -inf,\n    z_max: float = inf,\n) -> Self\n

    Crop, removing all atoms outside of the specified region, inclusive.

    Source code in atomlib/atoms.py
    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf) -> Self:\n    \"\"\"\n    Crop, removing all atoms outside of the specified region, inclusive.\n    \"\"\"\n\n    return self.filter(\n        self.x().is_between(x_min, x_max, closed='both'),\n        self.y().is_between(y_min, y_max, closed='both'),\n        self.z().is_between(z_min, z_max, closed='both'),\n    )\n
    "},{"location":"api/#atomlib.Atoms.deduplicate","title":"deduplicate","text":"
    deduplicate(\n    tol: float = 0.001,\n    subset: Iterable[str] = (\"x\", \"y\", \"z\", \"symbol\"),\n    keep: UniqueKeepStrategy = \"first\",\n    maintain_order: bool = True,\n) -> Self\n

    De-duplicate atoms in self. Atoms of the same symbol that are closer than tolerance to each other (by Euclidian distance) will be removed, leaving only the atom specified by keep (defaults to the first atom).

    If subset is specified, only those columns will be included while assessing duplicates. Floating point columns other than 'x', 'y', and 'z' will not by toleranced.

    Source code in atomlib/atoms.py
    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n    \"\"\"\n    De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n    to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n    `keep` (defaults to the first atom).\n\n    If `subset` is specified, only those columns will be included while assessing duplicates.\n    Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n    \"\"\"\n    import scipy.spatial\n\n    cols = set((subset,) if isinstance(subset, str) else subset)\n\n    indices = numpy.arange(len(self))\n\n    spatial_cols = cols.intersection(('x', 'y', 'z'))\n    cols -= spatial_cols\n    if len(spatial_cols) > 0:\n        coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n        tree = scipy.spatial.KDTree(coords)\n\n        # TODO This is a bad algorithm\n        while True:\n            changed = False\n            for (i, j) in tree.query_pairs(tol, 2.):\n                # whenever we encounter a pair, ensure their index matches\n                i_i, i_j = indices[[i, j]]\n                if i_i != i_j:\n                    indices[i] = indices[j] = min(i_i, i_j)\n                    changed = True\n            if not changed:\n                break\n\n        self = self.with_column(polars.Series('_unique_pts', indices))\n        cols.add('_unique_pts')\n\n    frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n    if len(spatial_cols) > 0:\n        frame = frame.drop('_unique_pts')\n\n    return self.with_atoms(Atoms(frame, _unchecked=True))\n
    "},{"location":"api/#atomlib.Atoms.with_bounds","title":"with_bounds","text":"
    with_bounds(\n    cell_size: Optional[VecLike] = None,\n    cell_origin: Optional[VecLike] = None,\n) -> \"AtomCell\"\n

    Return a periodic cell with the given orthogonal cell dimensions.

    If cell_size is not specified, it will be assumed (and may be incorrect).

    Source code in atomlib/atoms.py
    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n    \"\"\"\n    Return a periodic cell with the given orthogonal cell dimensions.\n\n    If cell_size is not specified, it will be assumed (and may be incorrect).\n    \"\"\"\n    # TODO: test this\n    from .atomcell import AtomCell\n\n    if cell_size is None:\n        warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n        cell_size = self.bbox().size\n        cell_origin = self.bbox().min\n\n    # TODO test this origin code\n    cell = Cell.from_unit_cell(cell_size)\n    if cell_origin is not None:\n        cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n    return AtomCell(self.get_atoms(), cell, frame='local')\n
    "},{"location":"api/#atomlib.Atoms.coords","title":"coords","text":"
    coords(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Literal[\"local\"] = \"local\"\n) -> NDArray[float64]\n

    Return a (N, 3) ndarray of atom coordinates (dtype numpy.float64).

    Source code in atomlib/atoms.py
    def coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> NDArray[numpy.float64]:\n    \"\"\"Return a `(N, 3)` ndarray of atom coordinates (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n    df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n    return df.get_column('coords').to_numpy().astype(numpy.float64)\n
    "},{"location":"api/#atomlib.Atoms.x","title":"x","text":"
    x() -> Expr\n
    Source code in atomlib/atoms.py
    def x(self) -> polars.Expr:\n    return polars.col('coords').arr.get(0).alias('x')\n
    "},{"location":"api/#atomlib.Atoms.y","title":"y","text":"
    y() -> Expr\n
    Source code in atomlib/atoms.py
    def y(self) -> polars.Expr:\n    return polars.col('coords').arr.get(1).alias('y')\n
    "},{"location":"api/#atomlib.Atoms.z","title":"z","text":"
    z() -> Expr\n
    Source code in atomlib/atoms.py
    def z(self) -> polars.Expr:\n    return polars.col('coords').arr.get(2).alias('z')\n
    "},{"location":"api/#atomlib.Atoms.velocities","title":"velocities","text":"
    velocities(\n    selection: Optional[AtomSelection] = None,\n) -> Optional[NDArray[float64]]\n

    Return a (N, 3) ndarray of atom velocities (dtype numpy.float64).

    Source code in atomlib/atoms.py
    def velocities(self, selection: t.Optional[AtomSelection] = None) -> t.Optional[NDArray[numpy.float64]]:\n    \"\"\"Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n    if 'velocity' not in self:\n        return None\n\n    df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n    return df.get_column('velocity').to_numpy().astype(numpy.float64)\n
    "},{"location":"api/#atomlib.Atoms.types","title":"types","text":"
    types() -> Optional[Series]\n

    Returns a Series of atom types (dtype polars.Int32).

    Source code in atomlib/atoms.py
    def types(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('type')\n
    "},{"location":"api/#atomlib.Atoms.masses","title":"masses","text":"
    masses() -> Optional[Series]\n

    Returns a Series of atom masses (dtype polars.Float32).

    Source code in atomlib/atoms.py
    def masses(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('mass')\n
    "},{"location":"api/#atomlib.Atoms.add_atom","title":"add_atom","text":"
    add_atom(\n    elem: Union[int, str],\n    x: ArrayLike,\n    /,\n    *,\n    y: None = None,\n    z: None = None,\n    **kwargs: Any,\n) -> Self\n
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: float,\n    y: float,\n    z: float,\n    **kwargs: Any,\n) -> Self\n
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: Union[ArrayLike, float],\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    **kwargs: Any,\n) -> Self\n

    Return a copy of self with an extra atom.

    By default, all extra columns present in self must be specified as **kwargs.

    Try to avoid calling this in a loop (Use HasAtoms.concat instead).

    Source code in atomlib/atoms.py
    def add_atom(self, elem: t.Union[int, str], /,\n             x: t.Union[ArrayLike, float],\n             y: t.Optional[float] = None,\n             z: t.Optional[float] = None,\n             **kwargs: t.Any) -> Self:\n    \"\"\"\n    Return a copy of `self` with an extra atom.\n\n    By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n    Try to avoid calling this in a loop (Use [`HasAtoms.concat`][atomlib.atoms.HasAtoms.concat] instead).\n    \"\"\"\n    if isinstance(elem, int):\n        kwargs.update(elem=elem)\n    else:\n        kwargs.update(symbol=elem)\n    if hasattr(x, '__len__') and len(x) > 1:  # type: ignore\n        (x, y, z) = to_vec3(x)\n    elif y is None or z is None:\n        raise ValueError(\"Must specify vector of positions or x, y, & z.\")\n\n    sym = get_sym(elem) if isinstance(elem, int) else elem\n    d: t.Dict[str, t.Any] = {'x': x, 'y': y, 'z': z, 'symbol': sym, **kwargs}\n    return self.concat(\n        (self, Atoms(d).select_schema(self.schema)),\n        how='vertical'\n    )\n
    "},{"location":"api/#atomlib.Atoms.pos","title":"pos","text":"
    pos(\n    x: Sequence[Optional[float]],\n    /,\n    *,\n    y: None = None,\n    z: None = None,\n    tol: float = 1e-06,\n    **kwargs: Any,\n) -> Expr\n
    pos(\n    x: Optional[float] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n
    pos(\n    x: Union[Sequence[Optional[float]], float, None] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n

    Select all atoms at a given position.

    Formally, returns all atoms within a cube of radius tol centered at (x,y,z), exclusive of the cube's surface.

    Additional parameters given as kwargs will be checked as additional parameters (with strict equality).

    Source code in atomlib/atoms.py
    def pos(self,\n        x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n        y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n        tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n    \"\"\"\n    Select all atoms at a given position.\n\n    Formally, returns all atoms within a cube of radius ``tol``\n    centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n    Additional parameters given as ``kwargs`` will be checked\n    as additional parameters (with strict equality).\n    \"\"\"\n\n    if isinstance(x, t.Sequence):\n        (x, y, z) = x\n\n    tol = abs(float(tol))\n    selection = polars.lit(True)\n    if x is not None:\n        selection &= self.x().is_between(x - tol, x + tol, closed='none')\n    if y is not None:\n        selection &= self.y().is_between(y - tol, y + tol, closed='none')\n    if z is not None:\n        selection &= self.z().is_between(z - tol, z + tol, closed='none')\n    for (col, val) in kwargs.items():\n        selection &= (polars.col(col) == val)\n\n    return selection\n
    "},{"location":"api/#atomlib.Atoms.with_index","title":"with_index","text":"
    with_index(index: Optional[AtomValues] = None) -> Self\n

    Returns self with a row index added in column 'i' (dtype polars.Int64). If index is not specified, defaults to an existing index or a new index.

    Source code in atomlib/atoms.py
    def with_index(self, index: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n    If `index` is not specified, defaults to an existing index or a new index.\n    \"\"\"\n    if index is None and 'i' in self.columns:\n        return self\n    if index is None:\n        index = numpy.arange(len(self), dtype=numpy.int64)\n    return self.with_column(_values_to_expr(self, index, polars.Int64).alias('i'))\n
    "},{"location":"api/#atomlib.Atoms.with_wobble","title":"with_wobble","text":"
    with_wobble(wobble: Optional[AtomValues] = None) -> Self\n

    Return self with the given displacements in column 'wobble' (dtype polars.Float64). If wobble is not specified, defaults to the already-existing wobbles or 0.

    Source code in atomlib/atoms.py
    def with_wobble(self, wobble: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n    \"\"\"\n    if wobble is None and 'wobble' in self.columns:\n        return self\n    wobble = 0. if wobble is None else wobble\n    return self.with_column(_values_to_expr(self, wobble, polars.Float64).alias('wobble'))\n
    "},{"location":"api/#atomlib.Atoms.with_occupancy","title":"with_occupancy","text":"
    with_occupancy(\n    frac_occupancy: Optional[AtomValues] = None,\n) -> Self\n

    Return self with the given fractional occupancies (dtype polars.Float64). If frac_occupancy is not specified, defaults to the already-existing occupancies or 1.

    Source code in atomlib/atoms.py
    def with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n    \"\"\"\n    if frac_occupancy is None and 'frac_occupancy' in self.columns:\n        return self\n    frac_occupancy = 1. if frac_occupancy is None else frac_occupancy\n    return self.with_column(_values_to_expr(self, frac_occupancy, polars.Float64).alias('frac_occupancy'))\n
    "},{"location":"api/#atomlib.Atoms.apply_wobble","title":"apply_wobble","text":"
    apply_wobble(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    Displace the atoms in self by the amount in the wobble column. wobble is interpretated as a mean-squared displacement, which is distributed equally over each axis.

    Source code in atomlib/atoms.py
    def apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    Displace the atoms in `self` by the amount in the `wobble` column.\n    `wobble` is interpretated as a mean-squared displacement, which is distributed\n    equally over each axis.\n    \"\"\"\n    if 'wobble' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    stddev = self.select((polars.col('wobble') / 3.).sqrt()).to_series().to_numpy()\n    coords = self.coords()\n    coords += stddev[:, None] * rng.standard_normal(coords.shape)\n    return self.with_coords(coords)\n
    "},{"location":"api/#atomlib.Atoms.apply_occupancy","title":"apply_occupancy","text":"
    apply_occupancy(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    For each atom in self, use its frac_occupancy to randomly decide whether to remove it.

    Source code in atomlib/atoms.py
    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n    \"\"\"\n    if 'frac_occupancy' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    frac = self.select('frac_occupancy').to_series().to_numpy()\n    choice = rng.binomial(1, frac).astype(numpy.bool_)\n    return self.filter(polars.lit(choice))\n
    "},{"location":"api/#atomlib.Atoms.with_type","title":"with_type","text":"
    with_type(types: Optional[AtomValues] = None) -> Self\n

    Return self with the given atom types in column 'type'. If types is not specified, use the already existing types or auto-assign them.

    When auto-assigning, each symbol is given a unique value, case-sensitive. Values are assigned from lowest atomic number to highest. For instance: [\"Ag+\", \"Na\", \"H\", \"Ag\"] => [3, 11, 1, 2]

    Source code in atomlib/atoms.py
    def with_type(self, types: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom types in column 'type'.\n    If `types` is not specified, use the already existing types or auto-assign them.\n\n    When auto-assigning, each symbol is given a unique value, case-sensitive.\n    Values are assigned from lowest atomic number to highest.\n    For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n    \"\"\"\n    if types is not None:\n        return self.with_columns(type=_values_to_expr(self, types, polars.Int32))\n    if 'type' in self.columns:\n        return self\n\n    unique = Atoms(self._get_frame().unique(maintain_order=False, subset=['elem', 'symbol']).sort(['elem', 'symbol']), _unchecked=True)\n    new = self.with_column(polars.Series('type', values=numpy.zeros(len(self)), dtype=polars.Int32))\n\n    logging.warning(\"Auto-assigning element types\")\n    for (i, (elem, sym)) in enumerate(unique.select(('elem', 'symbol')).rows()):\n        print(f\"Assigning type {i+1} to element '{sym}'\")\n        new = new.with_column(polars.when((polars.col('elem') == elem) & (polars.col('symbol') == sym))\n                                    .then(polars.lit(i+1))\n                                    .otherwise(polars.col('type'))\n                                    .alias('type'))\n\n    assert (new.get_column('type') == 0).sum() == 0\n    return new\n
    "},{"location":"api/#atomlib.Atoms.with_mass","title":"with_mass","text":"
    with_mass(mass: Optional[ArrayLike] = None) -> Self\n

    Return self with the given atom masses in column 'mass'. If mass is not specified, use the already existing masses or auto-assign them.

    Source code in atomlib/atoms.py
    def with_mass(self, mass: t.Optional[ArrayLike] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom masses in column `'mass'`.\n    If `mass` is not specified, use the already existing masses or auto-assign them.\n    \"\"\"\n    if mass is not None:\n        return self.with_column(_values_to_expr(self, mass, polars.Float32).alias('mass'))\n    if 'mass' in self.columns:\n        return self\n\n    unique_elems = self.get_column('elem').unique()\n    new = self.with_column(polars.Series('mass', values=numpy.zeros(len(self)), dtype=polars.Float32))\n\n    logging.warning(\"Auto-assigning element masses\")\n    for elem in unique_elems:\n        new = new.with_column(polars.when(polars.col('elem') == elem)\n                                    .then(polars.lit(get_mass(elem)))\n                                    .otherwise(polars.col('mass'))\n                                    .alias('mass'))\n\n    assert (new.get_column('mass').abs() < 1e-10).sum() == 0\n    return new\n
    "},{"location":"api/#atomlib.Atoms.with_symbol","title":"with_symbol","text":"
    with_symbol(\n    symbols: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n) -> Self\n

    Return self with the given atomic symbols.

    Source code in atomlib/atoms.py
    def with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atomic symbols.\n    \"\"\"\n    if selection is not None:\n        selection = _selection_to_numpy(self, selection)\n        new_symbols = self.get_column('symbol')\n        new_symbols[selection] = polars.Series(list(numpy.broadcast_to(symbols, len(selection))), dtype=polars.Utf8)\n        symbols = new_symbols\n\n    # TODO better cast here\n    symbols = polars.Series('symbol', list(numpy.broadcast_to(symbols, len(self))), dtype=polars.Utf8)\n    return self.with_columns((symbols, get_elem(symbols)))\n
    "},{"location":"api/#atomlib.Atoms.with_coords","title":"with_coords","text":"
    with_coords(\n    pts: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Literal[\"local\"] = \"local\"\n) -> Self\n

    Return self replaced with the given atomic positions.

    Source code in atomlib/atoms.py
    def with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic positions.\n    \"\"\"\n    if selection is not None:\n        selection = _selection_to_numpy(self, selection)\n        new_pts = self.coords()\n        pts = numpy.atleast_2d(pts)\n        assert pts.shape[-1] == 3\n        new_pts[selection] = pts\n        pts = new_pts\n\n    # https://github.com/pola-rs/polars/issues/18369\n    pts = numpy.broadcast_to(pts, (len(self), 3)) if len(self) else []\n    return self.with_columns(polars.Series('coords', pts, polars.Array(polars.Float64, 3)))\n
    "},{"location":"api/#atomlib.Atoms.with_velocity","title":"with_velocity","text":"
    with_velocity(\n    pts: Optional[ArrayLike] = None,\n    selection: Optional[AtomSelection] = None,\n) -> Self\n

    Return self replaced with the given atomic velocities. If pts is not specified, use the already existing velocities or zero.

    Source code in atomlib/atoms.py
    def with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                  selection: t.Optional[AtomSelection] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic velocities.\n    If `pts` is not specified, use the already existing velocities or zero.\n    \"\"\"\n    if pts is None:\n        if 'velocity' in self:\n            return self\n        all_pts = numpy.zeros((len(self), 3))\n    else:\n        all_pts = self['velocity'].to_numpy()\n\n    if selection is None:\n        all_pts = pts or all_pts\n    elif pts is not None:\n        selection = _selection_to_numpy(self, selection)\n        all_pts = numpy.require(all_pts, requirements=['WRITEABLE'])\n        pts = numpy.atleast_2d(pts)\n        assert pts.shape[-1] == 3\n        all_pts[selection] = pts\n\n    all_pts = numpy.broadcast_to(all_pts, (len(self), 3))\n    return self.with_columns(polars.Series('velocity', all_pts, polars.Array(polars.Float64, 3)))\n
    "},{"location":"api/#atomlib.Atoms.read","title":"read classmethod","text":"
    read(path: FileOrPath, ty: FileType) -> HasAtomsT\n
    read(\n    path: Union[str, Path, TextIO], ty: Literal[None] = None\n) -> HasAtomsT\n
    read(\n    path: FileOrPath, ty: Optional[FileType] = None\n) -> HasAtomsT\n

    Read a structure from a file.

    Supported types can be found in the io module. If no ty is specified, it is inferred from the file's extension.

    Source code in atomlib/mixins.py
    @classmethod\ndef read(cls: t.Type[HasAtomsT], path: FileOrPath, ty: t.Optional[FileType] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a file.\n\n    Supported types can be found in the [io][atomlib.io] module.\n    If no `ty` is specified, it is inferred from the file's extension.\n    \"\"\"\n    from .io import read\n    return _cast_atoms(read(path, ty), cls)  # type: ignore\n
    "},{"location":"api/#atomlib.Atoms.read_cif","title":"read_cif classmethod","text":"
    read_cif(\n    f: Union[FileOrPath, CIF, CIFDataBlock],\n    block: Union[int, str, None] = None,\n) -> HasAtomsT\n

    Read a structure from a CIF file.

    If block is specified, read data from the given block of the CIF file (index or name).

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cif(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CIF, CIFDataBlock], block: t.Union[int, str, None] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a CIF file.\n\n    If `block` is specified, read data from the given block of the CIF file (index or name).\n    \"\"\"\n    from .io import read_cif\n    return _cast_atoms(read_cif(f, block), cls)\n
    "},{"location":"api/#atomlib.Atoms.read_xyz","title":"read_xyz classmethod","text":"
    read_xyz(f: Union[FileOrPath, XYZ]) -> HasAtomsT\n

    Read a structure from an XYZ file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xyz(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XYZ]) -> HasAtomsT:\n    \"\"\"Read a structure from an XYZ file.\"\"\"\n    from .io import read_xyz\n    return _cast_atoms(read_xyz(f), cls)\n
    "},{"location":"api/#atomlib.Atoms.read_xsf","title":"read_xsf classmethod","text":"
    read_xsf(f: Union[FileOrPath, XSF]) -> HasAtomsT\n

    Read a structure from an XSF file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xsf(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XSF]) -> HasAtomsT:\n    \"\"\"Read a structure from an XSF file.\"\"\"\n    from .io import read_xsf\n    return _cast_atoms(read_xsf(f), cls)\n
    "},{"location":"api/#atomlib.Atoms.read_cfg","title":"read_cfg classmethod","text":"
    read_cfg(f: Union[FileOrPath, CFG]) -> HasAtomsT\n

    Read a structure from a CFG file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cfg(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CFG]) -> HasAtomsT:\n    \"\"\"Read a structure from a CFG file.\"\"\"\n    from .io import read_cfg\n    return _cast_atoms(read_cfg(f), cls)\n
    "},{"location":"api/#atomlib.Atoms.read_lmp","title":"read_lmp classmethod","text":"
    read_lmp(\n    f: Union[FileOrPath, LMP],\n    type_map: Optional[Dict[int, Union[str, int]]] = None,\n) -> HasAtomsT\n

    Read a structure from a LAAMPS data file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_lmp(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, LMP], type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> HasAtomsT:\n    \"\"\"Read a structure from a LAAMPS data file.\"\"\"\n    from .io import read_lmp\n    return _cast_atoms(read_lmp(f, type_map=type_map), cls)\n
    "},{"location":"api/#atomlib.Atoms.write_cif","title":"write_cif","text":"
    write_cif(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cif(self, f: FileOrPath):\n    from .io import write_cif\n    write_cif(self, f)\n
    "},{"location":"api/#atomlib.Atoms.write_xyz","title":"write_xyz","text":"
    write_xyz(f: FileOrPath, fmt: XYZFormat = 'exyz')\n
    Source code in atomlib/mixins.py
    def write_xyz(self, f: FileOrPath, fmt: XYZFormat = 'exyz'):\n    from .io import write_xyz\n    write_xyz(self, f, fmt)\n
    "},{"location":"api/#atomlib.Atoms.write_xsf","title":"write_xsf","text":"
    write_xsf(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_xsf(self, f: FileOrPath):\n    from .io import write_xsf\n    write_xsf(self, f)\n
    "},{"location":"api/#atomlib.Atoms.write_cfg","title":"write_cfg","text":"
    write_cfg(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cfg(self, f: FileOrPath):\n    from .io import write_cfg\n    write_cfg(self, f)\n
    "},{"location":"api/#atomlib.Atoms.write_lmp","title":"write_lmp","text":"
    write_lmp(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_lmp(self, f: FileOrPath):\n    from .io import write_lmp\n    write_lmp(self, f)\n
    "},{"location":"api/#atomlib.Atoms.write","title":"write","text":"
    write(path: FileOrPath, ty: FileType)\n
    write(\n    path: Union[str, Path, TextIO], ty: Literal[None] = None\n)\n
    write(path: FileOrPath, ty: Optional[FileType] = None)\n

    Write this structure to a file.

    A file type may be specified using ty. If no ty is specified, it is inferred from the path's extension.

    Source code in atomlib/mixins.py
    def write(self, path: FileOrPath, ty: t.Optional[FileType] = None):\n    \"\"\"\n    Write this structure to a file.\n\n    A file type may be specified using `ty`.\n    If no `ty` is specified, it is inferred from the path's extension.\n    \"\"\"\n    from .io import write\n    write(self, path, ty)  # type: ignore\n
    "},{"location":"api/#atomlib.Atoms.empty","title":"empty staticmethod","text":"
    empty() -> Atoms\n

    Return an empty Atoms with only the mandatory columns.

    Source code in atomlib/atoms.py
    @staticmethod\ndef empty() -> Atoms:\n    \"\"\"\n    Return an empty Atoms with only the mandatory columns.\n    \"\"\"\n    return Atoms()\n
    "},{"location":"api/#atomlib.Atoms.get_atoms","title":"get_atoms","text":"
    get_atoms(frame: Literal['local'] = 'local') -> Atoms\n
    Source code in atomlib/atoms.py
    def get_atoms(self, frame: t.Literal['local'] = 'local') -> Atoms:\n    if frame != 'local':\n        raise ValueError(f\"Atoms without a cell only support the 'local' coordinate frame, not '{frame}'.\")\n    return self\n
    "},{"location":"api/#atomlib.Atoms.with_atoms","title":"with_atoms","text":"
    with_atoms(\n    atoms: HasAtoms, frame: Literal[\"local\"] = \"local\"\n) -> Atoms\n
    Source code in atomlib/atoms.py
    def with_atoms(self, atoms: HasAtoms, frame: t.Literal['local'] = 'local') -> Atoms:\n    if frame != 'local':\n        raise ValueError(f\"Atoms without a cell only support the 'local' coordinate frame, not '{frame}'.\")\n    return atoms.get_atoms()\n
    "},{"location":"api/#atomlib.Atoms.bbox","title":"bbox","text":"
    bbox() -> BBox3D\n

    Return the bounding box of all the points in self.

    Source code in atomlib/atoms.py
    def bbox(self) -> BBox3D:\n    \"\"\"Return the bounding box of all the points in `self`.\"\"\"\n    if self._bbox is None:\n        self._bbox = BBox3D.from_pts(self.coords())\n\n    return self._bbox\n
    "},{"location":"api/#atomlib.HasAtoms","title":"HasAtoms","text":"

    Bases: ABC

    Abstract class representing any (possibly compound) collection of atoms.

    Source code in atomlib/atoms.py
    class HasAtoms(abc.ABC):\n    \"\"\"Abstract class representing any (possibly compound) collection of atoms.\"\"\"\n\n    # abstract methods\n\n    @abc.abstractmethod\n    def get_atoms(self, frame: t.Literal['local'] = 'local') -> Atoms:\n        \"\"\"\n        Get atoms contained in `self`. This should be a low cost method.\n\n        Args:\n          frame: Coordinate frame to return atoms in. For a plain [`HasAtoms`][atomlib.atoms.HasAtoms],\n                 only `'local'` is supported.\n\n        Return:\n          The contained atoms\n        \"\"\"\n        ...\n\n    @abc.abstractmethod\n    def with_atoms(self, atoms: HasAtoms, frame: t.Literal['local'] = 'local') -> Self:\n        \"\"\"\n        Return a copy of self with the inner [`Atoms`][atomlib.atoms.Atoms] replaced.\n\n        Args:\n          atoms: [`HasAtoms`][atomlib.atoms.HasAtoms] to replace these with.\n          frame: Coordinate frame inside atoms are in. For a plain [`HasAtoms`][atomlib.atoms.HasAtoms],\n                 only `'local'` is supported.\n\n        Return:\n          A copy of `self` updated with the given atoms\n        \"\"\"\n        ...\n\n    @classmethod\n    @abc.abstractmethod\n    def _combine_metadata(cls: t.Type[HasAtomsT], *atoms: HasAtoms) -> HasAtomsT:\n        \"\"\"\n        When combining multiple `HasAtoms`, check that they are compatible with each other,\n        and return a 'representative' which best represents the combined metadata.\n        Implementors should treat `Atoms` as acceptable, but having no metadata.\n        \"\"\"\n        ...\n\n    def _get_frame(self) -> polars.DataFrame:\n        return self.get_atoms().inner\n\n    # dataframe methods\n\n    @property\n    @_fwd_frame(lambda df: df.columns)\n    def columns(self) -> t.List[str]:\n        \"\"\"\n        Return the column names in `self`.\n\n        Returns:\n          A sequence of column names\n        \"\"\"\n        ...\n\n    @property\n    @_fwd_frame(lambda df: df.dtypes)\n    def dtypes(self) -> t.List[polars.DataType]:\n        \"\"\"\n        Return the datatypes in `self`.\n\n        Returns:\n          A sequence of column [`DataType`][polars.datatypes.DataType]s\n        \"\"\"\n        ...\n\n    @property\n    @_fwd_frame(lambda df: df.schema)  # type: ignore\n    def schema(self) -> Schema:\n        \"\"\"\n        Return the schema of `self`.\n\n        Returns:\n          A dictionary of column names and [`DataType`][polars.datatypes.DataType]s\n        \"\"\"\n        ...\n\n    @_fwd_frame(polars.DataFrame.describe)\n    def describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n                 interpolation: RollingInterpolationMethod = 'nearest') -> polars.DataFrame:\n        \"\"\"\n        Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n        Args:\n          percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                       50% (median), and 75% (third quartile).\n\n        Returns:\n          A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n        \"\"\"\n        ...\n\n    @_fwd_frame_map\n    def with_columns(self,\n                     *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                     **named_exprs: IntoExpr) -> polars.DataFrame:\n        \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n        return self._get_frame().with_columns(*exprs, **named_exprs)\n\n    with_column = with_columns\n\n    @_fwd_frame_map\n    def insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n        return self._get_frame().insert_column(index, column)\n\n    @_fwd_frame(lambda df, name: df.get_column(name))\n    def get_column(self, name: str) -> polars.Series:\n        \"\"\"\n        Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        ...\n\n    @_fwd_frame(polars.DataFrame.get_columns)\n    def get_columns(self) -> t.List[polars.Series]:\n        \"\"\"\n        Return all columns from `self` as a list of [`Series`][polars.Series].\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        ...\n\n    @_fwd_frame(polars.DataFrame.get_column_index)\n    def get_column_index(self, name: str) -> int:\n        \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n        ...\n\n    @_fwd_frame(polars.DataFrame.group_by)\n    def group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]], maintain_order: bool = False,\n                 **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n        \"\"\"\n        Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n        \"\"\"\n        ...\n\n    def pipe(self: HasAtomsT, function: t.Callable[Concatenate[HasAtomsT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n        \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n        return function(self, *args, **kwargs)\n\n    @_fwd_frame_map\n    def clone(self) -> polars.DataFrame:\n        \"\"\"Return a copy of `self`.\"\"\"\n        return self._get_frame().clone()\n\n    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n        \"\"\"Return `self` with the specified columns removed.\"\"\"\n        return self._get_frame().drop(*columns, strict=strict)\n\n    # row-wise operations\n\n    def filter(\n        self,\n        *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n        **constraints: t.Any,\n    ) -> Self:\n        \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n        # TODO clean up\n        preds_not_none = tuple(filter(lambda p: p is not None, predicates))\n        if not len(preds_not_none) and not len(constraints):\n            return self\n        return self.with_atoms(Atoms(self._get_frame().filter(*preds_not_none, **constraints), _unchecked=True))  # type: ignore\n\n    @_fwd_frame_map\n    def sort(\n        self,\n        by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        *more_by: IntoExpr,\n        descending: t.Union[bool, t.Sequence[bool]] = False,\n        nulls_last: bool = False,\n    ) -> polars.DataFrame:\n        \"\"\"\n        Sort the atoms in `self` by the given columns/expressions.\n        \"\"\"\n        return self._get_frame().sort(\n            by, *more_by, descending=descending, nulls_last=nulls_last\n        )\n\n    @_fwd_frame_map\n    def slice(self, offset: int, length: t.Optional[int] = None) -> polars.DataFrame:\n        \"\"\"Return a slice of the rows in `self`.\"\"\"\n        return self._get_frame().slice(offset, length)\n\n    @_fwd_frame_map\n    def head(self, n: int = 5) -> polars.DataFrame:\n        \"\"\"Return the first `n` rows of `self`.\"\"\"\n        return self._get_frame().head(n)\n\n    @_fwd_frame_map\n    def tail(self, n: int = 5) -> polars.DataFrame:\n        \"\"\"Return the last `n` rows of `self`.\"\"\"\n        return self._get_frame().tail(n)\n\n    @_fwd_frame_map\n    def drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n        \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n        return self._get_frame().drop_nulls(subset)\n\n    @_fwd_frame_map\n    def fill_null(\n        self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n        limit: t.Optional[int] = None, matches_supertype: bool = True,\n    ) -> polars.DataFrame:\n        \"\"\"Fill null values in `self`, using the specified value or strategy.\"\"\"\n        return self._get_frame().fill_null(value, strategy, limit, matches_supertype=matches_supertype)\n\n    @_fwd_frame_map\n    def fill_nan(self, value: t.Union[polars.Expr, int, float, None]) -> polars.DataFrame:\n        \"\"\"Fill floating-point NaN values in `self`.\"\"\"\n        return self._get_frame().fill_nan(value)\n\n    @classmethod\n    def concat(cls: t.Type[HasAtomsT],\n               atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n               rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n        \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n        # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n        # same type as ``cls``.\n        if _is_abstract(cls):\n            raise TypeError(\"concat() must be called on a concrete class.\")\n\n        if isinstance(atoms, HasAtoms):\n            atoms = (atoms,)\n        dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n        representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n        if len(dfs) == 0:\n            return representative.with_atoms(Atoms.empty(), 'local')\n\n        if how in ('vertical', 'vertical_relaxed'):\n            # get order from first member\n            cols = dfs[0].columns\n            dfs = [df.select(cols) for df in dfs]\n        elif how == 'inner':\n            cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n            schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n            if len(schema) == 0:\n                raise ValueError(\"Atoms have no columns in common\")\n\n            dfs = [_select_schema(df, schema) for df in dfs]\n            how = 'vertical'\n\n        return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n\n    @t.overload\n    def partition_by(\n        self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n        maintain_order: bool = True, include_key: bool = True, as_dict: t.Literal[False] = False\n    ) -> t.List[Self]:\n        ...\n\n    @t.overload\n    def partition_by(\n        self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n        maintain_order: bool = True, include_key: bool = True, as_dict: t.Literal[True] = ...\n    ) -> t.Dict[t.Any, Self]:\n        ...\n\n    def partition_by(\n        self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n        maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n    ) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n        \"\"\"\n        Group by the given columns and partition into separate dataframes.\n\n        Return the partitions as a dictionary by specifying `as_dict=True`.\n        \"\"\"\n        if as_dict:\n            d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n            return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n        return [\n            self.with_atoms(Atoms(df, _unchecked=True))\n            for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n        ]\n\n    # column-wise operations\n\n    @_fwd_frame(polars.DataFrame.select)\n    def select(\n        self,\n        *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        **named_exprs: IntoExpr,\n    ) -> polars.DataFrame:\n        \"\"\"\n        Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n        Expressions may either be columns or expressions of columns.\n\n        [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n        \"\"\"\n        ...\n\n    # some helpers we add\n\n    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n        \"\"\"\n        Select columns from `self` and cast to the given schema.\n        Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n        \"\"\"\n        return _select_schema(self, schema)\n\n    def select_props(\n        self,\n        *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        **named_exprs: IntoExpr\n    ) -> Self:\n        \"\"\"\n        Select `exprs` from `self`, while keeping required columns.\n\n        Returns:\n          A [`HasAtoms`][atomlib.atoms.HasAtoms] filtered to contain the\n          specified properties (as well as required columns).\n        \"\"\"\n        props = self._get_frame().lazy().select(*exprs, **named_exprs).drop(_REQUIRED_COLUMNS, strict=False).collect(_eager=True)\n        return self.with_atoms(\n            Atoms(self._get_frame().select(_REQUIRED_COLUMNS).hstack(props), _unchecked=False)\n        )\n\n    def try_select(\n        self,\n        *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        **named_exprs: IntoExpr,\n    ) -> t.Optional[polars.DataFrame]:\n        \"\"\"\n        Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n        Expressions may either be columns or expressions of columns. Returns `None` if any\n        columns are missing.\n\n        [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n        \"\"\"\n        try:\n            return self._get_frame().select(*exprs, **named_exprs)\n        except polars.ColumnNotFoundError:\n            return None\n\n    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n        \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n        try:\n            return self.get_column(name)\n        except polars.exceptions.ColumnNotFoundError:\n            return None\n\n    def assert_equal(self, other: t.Any):\n        assert isinstance(other, HasAtoms)\n        assert dict(self.schema) == dict(other.schema)\n        for col in self.schema.keys():\n            polars.testing.assert_series_equal(self[col], other[col], check_names=False, rtol=1e-3, atol=1e-8)\n\n    # dunders\n\n    @_fwd_frame(polars.DataFrame.__len__)\n    def __len__(self) -> int:\n        \"\"\"Return the number of atoms in `self`.\"\"\"\n        ...\n\n    @_fwd_frame(polars.DataFrame.__contains__)\n    def __contains__(self, key: str) -> bool:\n        \"\"\"Return whether `self` contains the given column.\"\"\"\n        ...\n\n    def __add__(self, other: IntoAtoms) -> HasAtoms:\n        return self.__class__.concat((self, other), how='inner')\n\n    def __radd__(self, other: IntoAtoms) -> HasAtoms:\n        return self.__class__.concat((other, self), how='inner')\n\n    def __getitem__(self, column: str) -> polars.Series:\n        try:\n            return self.get_column(column)\n        except polars.exceptions.ColumnNotFoundError:\n            if column in ('x', 'y', 'z'):\n                return self.select(_coord_expr(column)).to_series()\n            raise\n\n    @_fwd_frame(polars.DataFrame.__dataframe__)\n    def __dataframe__(self, nan_as_null: bool = False, allow_copy: bool = True) -> polars.interchange.dataframe.PolarsDataFrame:\n        ...\n\n    # atoms-specific methods\n\n    def bbox_atoms(self) -> BBox3D:\n        \"\"\"Return the bounding box of all the atoms in ``self``.\"\"\"\n        return BBox3D.from_pts(self.coords())\n\n    bbox = bbox_atoms\n\n    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                        transform_velocities: bool = False) -> Self:\n        \"\"\"\n        Transform the atoms in `self` by `transform`.\n        If `selection` is given, only transform the atoms in `selection`.\n        \"\"\"\n        transform = Transform3D.make(transform)\n        selection = _selection_to_numpy(self, selection)\n        transformed = self.with_coords(Transform3D.make(transform) @ self.coords(selection), selection)\n        # try to transform velocities as well\n        if transform_velocities and (velocities := self.velocities(selection)) is not None:\n            return transformed.with_velocity(transform.transform_vec(velocities), selection)\n        return transformed\n\n    transform = transform_atoms\n\n    def round_near_zero(self, tol: float = 1e-14) -> Self:\n        \"\"\"\n        Round atom position values near zero to zero.\n        \"\"\"\n        return self.with_columns(coords=polars.concat_list(\n            polars.when(_coord_expr(col).abs() >= tol).then(_coord_expr(col)).otherwise(polars.lit(0.))\n            for col in range(3)\n        ).list.to_array(3))\n\n    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n             y_min: float = -numpy.inf, y_max: float = numpy.inf,\n             z_min: float = -numpy.inf, z_max: float = numpy.inf) -> Self:\n        \"\"\"\n        Crop, removing all atoms outside of the specified region, inclusive.\n        \"\"\"\n\n        return self.filter(\n            self.x().is_between(x_min, x_max, closed='both'),\n            self.y().is_between(y_min, y_max, closed='both'),\n            self.z().is_between(z_min, z_max, closed='both'),\n        )\n\n    crop_atoms = crop\n\n    def _wrap(self, eps: float = 1e-5) -> Self:\n        coords = (self.coords() + eps) % 1. - eps\n        return self.with_coords(coords)\n\n    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                    keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n        \"\"\"\n        De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n        to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n        `keep` (defaults to the first atom).\n\n        If `subset` is specified, only those columns will be included while assessing duplicates.\n        Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n        \"\"\"\n        import scipy.spatial\n\n        cols = set((subset,) if isinstance(subset, str) else subset)\n\n        indices = numpy.arange(len(self))\n\n        spatial_cols = cols.intersection(('x', 'y', 'z'))\n        cols -= spatial_cols\n        if len(spatial_cols) > 0:\n            coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n            tree = scipy.spatial.KDTree(coords)\n\n            # TODO This is a bad algorithm\n            while True:\n                changed = False\n                for (i, j) in tree.query_pairs(tol, 2.):\n                    # whenever we encounter a pair, ensure their index matches\n                    i_i, i_j = indices[[i, j]]\n                    if i_i != i_j:\n                        indices[i] = indices[j] = min(i_i, i_j)\n                        changed = True\n                if not changed:\n                    break\n\n            self = self.with_column(polars.Series('_unique_pts', indices))\n            cols.add('_unique_pts')\n\n        frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n        if len(spatial_cols) > 0:\n            frame = frame.drop('_unique_pts')\n\n        return self.with_atoms(Atoms(frame, _unchecked=True))\n\n    unique = deduplicate\n\n    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n        \"\"\"\n        Return a periodic cell with the given orthogonal cell dimensions.\n\n        If cell_size is not specified, it will be assumed (and may be incorrect).\n        \"\"\"\n        # TODO: test this\n        from .atomcell import AtomCell\n\n        if cell_size is None:\n            warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n            cell_size = self.bbox().size\n            cell_origin = self.bbox().min\n\n        # TODO test this origin code\n        cell = Cell.from_unit_cell(cell_size)\n        if cell_origin is not None:\n            cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n        return AtomCell(self.get_atoms(), cell, frame='local')\n\n    # property getters and setters\n\n    def coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> NDArray[numpy.float64]:\n        \"\"\"Return a `(N, 3)` ndarray of atom coordinates (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n        df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n        return df.get_column('coords').to_numpy().astype(numpy.float64)\n\n    def x(self) -> polars.Expr:\n        return polars.col('coords').arr.get(0).alias('x')\n\n    def y(self) -> polars.Expr:\n        return polars.col('coords').arr.get(1).alias('y')\n\n    def z(self) -> polars.Expr:\n        return polars.col('coords').arr.get(2).alias('z')\n\n    def velocities(self, selection: t.Optional[AtomSelection] = None) -> t.Optional[NDArray[numpy.float64]]:\n        \"\"\"Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n        if 'velocity' not in self:\n            return None\n\n        df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n        return df.get_column('velocity').to_numpy().astype(numpy.float64)\n\n    def types(self) -> t.Optional[polars.Series]:\n        \"\"\"\n        Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        return self.try_get_column('type')\n\n    def masses(self) -> t.Optional[polars.Series]:\n        \"\"\"\n        Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        return self.try_get_column('mass')\n\n    @t.overload\n    def add_atom(self, elem: t.Union[int, str], x: ArrayLike, /, *,\n                 y: None = None, z: None = None,\n                 **kwargs: t.Any) -> Self:\n        ...\n\n    @t.overload\n    def add_atom(self, elem: t.Union[int, str], /,\n                 x: float, y: float, z: float,\n                 **kwargs: t.Any) -> Self:\n        ...\n\n    def add_atom(self, elem: t.Union[int, str], /,\n                 x: t.Union[ArrayLike, float],\n                 y: t.Optional[float] = None,\n                 z: t.Optional[float] = None,\n                 **kwargs: t.Any) -> Self:\n        \"\"\"\n        Return a copy of `self` with an extra atom.\n\n        By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n        Try to avoid calling this in a loop (Use [`HasAtoms.concat`][atomlib.atoms.HasAtoms.concat] instead).\n        \"\"\"\n        if isinstance(elem, int):\n            kwargs.update(elem=elem)\n        else:\n            kwargs.update(symbol=elem)\n        if hasattr(x, '__len__') and len(x) > 1:  # type: ignore\n            (x, y, z) = to_vec3(x)\n        elif y is None or z is None:\n            raise ValueError(\"Must specify vector of positions or x, y, & z.\")\n\n        sym = get_sym(elem) if isinstance(elem, int) else elem\n        d: t.Dict[str, t.Any] = {'x': x, 'y': y, 'z': z, 'symbol': sym, **kwargs}\n        return self.concat(\n            (self, Atoms(d).select_schema(self.schema)),\n            how='vertical'\n        )\n\n    @t.overload\n    def pos(self, x: t.Sequence[t.Optional[float]], /, *,\n            y: None = None, z: None = None,\n            tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n        ...\n\n    @t.overload\n    def pos(self, x: t.Optional[float] = None, y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n            tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n        ...\n\n    def pos(self,\n            x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n            y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n            tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n        \"\"\"\n        Select all atoms at a given position.\n\n        Formally, returns all atoms within a cube of radius ``tol``\n        centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n        Additional parameters given as ``kwargs`` will be checked\n        as additional parameters (with strict equality).\n        \"\"\"\n\n        if isinstance(x, t.Sequence):\n            (x, y, z) = x\n\n        tol = abs(float(tol))\n        selection = polars.lit(True)\n        if x is not None:\n            selection &= self.x().is_between(x - tol, x + tol, closed='none')\n        if y is not None:\n            selection &= self.y().is_between(y - tol, y + tol, closed='none')\n        if z is not None:\n            selection &= self.z().is_between(z - tol, z + tol, closed='none')\n        for (col, val) in kwargs.items():\n            selection &= (polars.col(col) == val)\n\n        return selection\n\n    def with_index(self, index: t.Optional[AtomValues] = None) -> Self:\n        \"\"\"\n        Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n        If `index` is not specified, defaults to an existing index or a new index.\n        \"\"\"\n        if index is None and 'i' in self.columns:\n            return self\n        if index is None:\n            index = numpy.arange(len(self), dtype=numpy.int64)\n        return self.with_column(_values_to_expr(self, index, polars.Int64).alias('i'))\n\n    def with_wobble(self, wobble: t.Optional[AtomValues] = None) -> Self:\n        \"\"\"\n        Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n        If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n        \"\"\"\n        if wobble is None and 'wobble' in self.columns:\n            return self\n        wobble = 0. if wobble is None else wobble\n        return self.with_column(_values_to_expr(self, wobble, polars.Float64).alias('wobble'))\n\n    def with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None) -> Self:\n        \"\"\"\n        Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n        If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n        \"\"\"\n        if frac_occupancy is None and 'frac_occupancy' in self.columns:\n            return self\n        frac_occupancy = 1. if frac_occupancy is None else frac_occupancy\n        return self.with_column(_values_to_expr(self, frac_occupancy, polars.Float64).alias('frac_occupancy'))\n\n    def apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n        \"\"\"\n        Displace the atoms in `self` by the amount in the `wobble` column.\n        `wobble` is interpretated as a mean-squared displacement, which is distributed\n        equally over each axis.\n        \"\"\"\n        if 'wobble' not in self.columns:\n            return self\n        rng = numpy.random.default_rng(seed=rng)\n\n        stddev = self.select((polars.col('wobble') / 3.).sqrt()).to_series().to_numpy()\n        coords = self.coords()\n        coords += stddev[:, None] * rng.standard_normal(coords.shape)\n        return self.with_coords(coords)\n\n    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n        \"\"\"\n        For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n        \"\"\"\n        if 'frac_occupancy' not in self.columns:\n            return self\n        rng = numpy.random.default_rng(seed=rng)\n\n        frac = self.select('frac_occupancy').to_series().to_numpy()\n        choice = rng.binomial(1, frac).astype(numpy.bool_)\n        return self.filter(polars.lit(choice))\n\n    def with_type(self, types: t.Optional[AtomValues] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atom types in column 'type'.\n        If `types` is not specified, use the already existing types or auto-assign them.\n\n        When auto-assigning, each symbol is given a unique value, case-sensitive.\n        Values are assigned from lowest atomic number to highest.\n        For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n        \"\"\"\n        if types is not None:\n            return self.with_columns(type=_values_to_expr(self, types, polars.Int32))\n        if 'type' in self.columns:\n            return self\n\n        unique = Atoms(self._get_frame().unique(maintain_order=False, subset=['elem', 'symbol']).sort(['elem', 'symbol']), _unchecked=True)\n        new = self.with_column(polars.Series('type', values=numpy.zeros(len(self)), dtype=polars.Int32))\n\n        logging.warning(\"Auto-assigning element types\")\n        for (i, (elem, sym)) in enumerate(unique.select(('elem', 'symbol')).rows()):\n            print(f\"Assigning type {i+1} to element '{sym}'\")\n            new = new.with_column(polars.when((polars.col('elem') == elem) & (polars.col('symbol') == sym))\n                                        .then(polars.lit(i+1))\n                                        .otherwise(polars.col('type'))\n                                        .alias('type'))\n\n        assert (new.get_column('type') == 0).sum() == 0\n        return new\n\n    def with_mass(self, mass: t.Optional[ArrayLike] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atom masses in column `'mass'`.\n        If `mass` is not specified, use the already existing masses or auto-assign them.\n        \"\"\"\n        if mass is not None:\n            return self.with_column(_values_to_expr(self, mass, polars.Float32).alias('mass'))\n        if 'mass' in self.columns:\n            return self\n\n        unique_elems = self.get_column('elem').unique()\n        new = self.with_column(polars.Series('mass', values=numpy.zeros(len(self)), dtype=polars.Float32))\n\n        logging.warning(\"Auto-assigning element masses\")\n        for elem in unique_elems:\n            new = new.with_column(polars.when(polars.col('elem') == elem)\n                                        .then(polars.lit(get_mass(elem)))\n                                        .otherwise(polars.col('mass'))\n                                        .alias('mass'))\n\n        assert (new.get_column('mass').abs() < 1e-10).sum() == 0\n        return new\n\n    def with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atomic symbols.\n        \"\"\"\n        if selection is not None:\n            selection = _selection_to_numpy(self, selection)\n            new_symbols = self.get_column('symbol')\n            new_symbols[selection] = polars.Series(list(numpy.broadcast_to(symbols, len(selection))), dtype=polars.Utf8)\n            symbols = new_symbols\n\n        # TODO better cast here\n        symbols = polars.Series('symbol', list(numpy.broadcast_to(symbols, len(self))), dtype=polars.Utf8)\n        return self.with_columns((symbols, get_elem(symbols)))\n\n    def with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> Self:\n        \"\"\"\n        Return `self` replaced with the given atomic positions.\n        \"\"\"\n        if selection is not None:\n            selection = _selection_to_numpy(self, selection)\n            new_pts = self.coords()\n            pts = numpy.atleast_2d(pts)\n            assert pts.shape[-1] == 3\n            new_pts[selection] = pts\n            pts = new_pts\n\n        # https://github.com/pola-rs/polars/issues/18369\n        pts = numpy.broadcast_to(pts, (len(self), 3)) if len(self) else []\n        return self.with_columns(polars.Series('coords', pts, polars.Array(polars.Float64, 3)))\n\n    def with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                      selection: t.Optional[AtomSelection] = None) -> Self:\n        \"\"\"\n        Return `self` replaced with the given atomic velocities.\n        If `pts` is not specified, use the already existing velocities or zero.\n        \"\"\"\n        if pts is None:\n            if 'velocity' in self:\n                return self\n            all_pts = numpy.zeros((len(self), 3))\n        else:\n            all_pts = self['velocity'].to_numpy()\n\n        if selection is None:\n            all_pts = pts or all_pts\n        elif pts is not None:\n            selection = _selection_to_numpy(self, selection)\n            all_pts = numpy.require(all_pts, requirements=['WRITEABLE'])\n            pts = numpy.atleast_2d(pts)\n            assert pts.shape[-1] == 3\n            all_pts[selection] = pts\n\n        all_pts = numpy.broadcast_to(all_pts, (len(self), 3))\n        return self.with_columns(polars.Series('velocity', all_pts, polars.Array(polars.Float64, 3)))\n
    "},{"location":"api/#atomlib.HasAtoms.columns","title":"columns property","text":"
    columns: List[str]\n

    Return the column names in self.

    RETURNS DESCRIPTION List[str]

    A sequence of column names

    "},{"location":"api/#atomlib.HasAtoms.dtypes","title":"dtypes property","text":"
    dtypes: List[DataType]\n

    Return the datatypes in self.

    RETURNS DESCRIPTION List[DataType]

    A sequence of column DataTypes

    "},{"location":"api/#atomlib.HasAtoms.schema","title":"schema property","text":"
    schema: Schema\n

    Return the schema of self.

    RETURNS DESCRIPTION Schema

    A dictionary of column names and DataTypes

    "},{"location":"api/#atomlib.HasAtoms.with_column","title":"with_column class-attribute instance-attribute","text":"
    with_column = with_columns\n
    "},{"location":"api/#atomlib.HasAtoms.bbox","title":"bbox class-attribute instance-attribute","text":"
    bbox = bbox_atoms\n
    "},{"location":"api/#atomlib.HasAtoms.transform","title":"transform class-attribute instance-attribute","text":"
    transform = transform_atoms\n
    "},{"location":"api/#atomlib.HasAtoms.crop_atoms","title":"crop_atoms class-attribute instance-attribute","text":"
    crop_atoms = crop\n
    "},{"location":"api/#atomlib.HasAtoms.unique","title":"unique class-attribute instance-attribute","text":"
    unique = deduplicate\n
    "},{"location":"api/#atomlib.HasAtoms.get_atoms","title":"get_atoms abstractmethod","text":"
    get_atoms(frame: Literal['local'] = 'local') -> Atoms\n

    Get atoms contained in self. This should be a low cost method.

    PARAMETER DESCRIPTION frame

    Coordinate frame to return atoms in. For a plain HasAtoms, only 'local' is supported.

    TYPE: Literal['local'] DEFAULT: 'local'

    Return

    The contained atoms

    Source code in atomlib/atoms.py
    @abc.abstractmethod\ndef get_atoms(self, frame: t.Literal['local'] = 'local') -> Atoms:\n    \"\"\"\n    Get atoms contained in `self`. This should be a low cost method.\n\n    Args:\n      frame: Coordinate frame to return atoms in. For a plain [`HasAtoms`][atomlib.atoms.HasAtoms],\n             only `'local'` is supported.\n\n    Return:\n      The contained atoms\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtoms.with_atoms","title":"with_atoms abstractmethod","text":"
    with_atoms(\n    atoms: HasAtoms, frame: Literal[\"local\"] = \"local\"\n) -> Self\n

    Return a copy of self with the inner Atoms replaced.

    PARAMETER DESCRIPTION atoms

    HasAtoms to replace these with.

    TYPE: HasAtoms

    frame

    Coordinate frame inside atoms are in. For a plain HasAtoms, only 'local' is supported.

    TYPE: Literal['local'] DEFAULT: 'local'

    Return

    A copy of self updated with the given atoms

    Source code in atomlib/atoms.py
    @abc.abstractmethod\ndef with_atoms(self, atoms: HasAtoms, frame: t.Literal['local'] = 'local') -> Self:\n    \"\"\"\n    Return a copy of self with the inner [`Atoms`][atomlib.atoms.Atoms] replaced.\n\n    Args:\n      atoms: [`HasAtoms`][atomlib.atoms.HasAtoms] to replace these with.\n      frame: Coordinate frame inside atoms are in. For a plain [`HasAtoms`][atomlib.atoms.HasAtoms],\n             only `'local'` is supported.\n\n    Return:\n      A copy of `self` updated with the given atoms\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtoms.describe","title":"describe","text":"
    describe(\n    percentiles: Union[Sequence[float], float, None] = (\n        0.25,\n        0.5,\n        0.75,\n    ),\n    *,\n    interpolation: RollingInterpolationMethod = \"nearest\"\n) -> DataFrame\n

    Return summary statistics for self. See DataFrame.describe for more information.

    PARAMETER DESCRIPTION percentiles

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), 50% (median), and 75% (third quartile).

    TYPE: Union[Sequence[float], float, None] DEFAULT: (0.25, 0.5, 0.75)

    RETURNS DESCRIPTION DataFrame

    A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.describe)\ndef describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n             interpolation: RollingInterpolationMethod = 'nearest') -> polars.DataFrame:\n    \"\"\"\n    Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n    Args:\n      percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                   50% (median), and 75% (third quartile).\n\n    Returns:\n      A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtoms.with_columns","title":"with_columns","text":"
    with_columns(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Return a copy of self with the given columns added.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef with_columns(self,\n                 *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 **named_exprs: IntoExpr) -> polars.DataFrame:\n    \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n    return self._get_frame().with_columns(*exprs, **named_exprs)\n
    "},{"location":"api/#atomlib.HasAtoms.insert_column","title":"insert_column","text":"
    insert_column(index: int, column: Series) -> DataFrame\n
    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n    return self._get_frame().insert_column(index, column)\n
    "},{"location":"api/#atomlib.HasAtoms.get_column","title":"get_column","text":"
    get_column(name: str) -> Series\n

    Get the specified column from self, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(lambda df, name: df.get_column(name))\ndef get_column(self, name: str) -> polars.Series:\n    \"\"\"\n    Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtoms.get_columns","title":"get_columns","text":"
    get_columns() -> List[Series]\n

    Return all columns from self as a list of Series.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_columns)\ndef get_columns(self) -> t.List[polars.Series]:\n    \"\"\"\n    Return all columns from `self` as a list of [`Series`][polars.Series].\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtoms.get_column_index","title":"get_column_index","text":"
    get_column_index(name: str) -> int\n

    Get the index of a column by name, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_column_index)\ndef get_column_index(self, name: str) -> int:\n    \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtoms.group_by","title":"group_by","text":"
    group_by(\n    *by: Union[IntoExpr, Iterable[IntoExpr]],\n    maintain_order: bool = False,\n    **named_by: IntoExpr\n) -> GroupBy\n

    Start a group by operation. See DataFrame.group_by for more information.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.group_by)\ndef group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]], maintain_order: bool = False,\n             **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n    \"\"\"\n    Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtoms.pipe","title":"pipe","text":"
    pipe(\n    function: Callable[Concatenate[HasAtomsT, P], T],\n    *args: args,\n    **kwargs: kwargs\n) -> T\n

    Apply function to self (in method-call syntax).

    Source code in atomlib/atoms.py
    def pipe(self: HasAtomsT, function: t.Callable[Concatenate[HasAtomsT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n    \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n    return function(self, *args, **kwargs)\n
    "},{"location":"api/#atomlib.HasAtoms.clone","title":"clone","text":"
    clone() -> DataFrame\n

    Return a copy of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef clone(self) -> polars.DataFrame:\n    \"\"\"Return a copy of `self`.\"\"\"\n    return self._get_frame().clone()\n
    "},{"location":"api/#atomlib.HasAtoms.drop","title":"drop","text":"
    drop(\n    *columns: Union[str, Iterable[str]], strict: bool = True\n) -> DataFrame\n

    Return self with the specified columns removed.

    Source code in atomlib/atoms.py
    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n    \"\"\"Return `self` with the specified columns removed.\"\"\"\n    return self._get_frame().drop(*columns, strict=strict)\n
    "},{"location":"api/#atomlib.HasAtoms.filter","title":"filter","text":"
    filter(\n    *predicates: Union[\n        None,\n        IntoExprColumn,\n        Iterable[IntoExprColumn],\n        bool,\n        List[bool],\n        ndarray,\n    ],\n    **constraints: Any\n) -> Self\n

    Filter self, removing rows which evaluate to False.

    Source code in atomlib/atoms.py
    def filter(\n    self,\n    *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n    **constraints: t.Any,\n) -> Self:\n    \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n    # TODO clean up\n    preds_not_none = tuple(filter(lambda p: p is not None, predicates))\n    if not len(preds_not_none) and not len(constraints):\n        return self\n    return self.with_atoms(Atoms(self._get_frame().filter(*preds_not_none, **constraints), _unchecked=True))  # type: ignore\n
    "},{"location":"api/#atomlib.HasAtoms.sort","title":"sort","text":"
    sort(\n    by: Union[IntoExpr, Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: Union[bool, Sequence[bool]] = False,\n    nulls_last: bool = False\n) -> DataFrame\n

    Sort the atoms in self by the given columns/expressions.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef sort(\n    self,\n    by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: t.Union[bool, t.Sequence[bool]] = False,\n    nulls_last: bool = False,\n) -> polars.DataFrame:\n    \"\"\"\n    Sort the atoms in `self` by the given columns/expressions.\n    \"\"\"\n    return self._get_frame().sort(\n        by, *more_by, descending=descending, nulls_last=nulls_last\n    )\n
    "},{"location":"api/#atomlib.HasAtoms.slice","title":"slice","text":"
    slice(\n    offset: int, length: Optional[int] = None\n) -> DataFrame\n

    Return a slice of the rows in self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef slice(self, offset: int, length: t.Optional[int] = None) -> polars.DataFrame:\n    \"\"\"Return a slice of the rows in `self`.\"\"\"\n    return self._get_frame().slice(offset, length)\n
    "},{"location":"api/#atomlib.HasAtoms.head","title":"head","text":"
    head(n: int = 5) -> DataFrame\n

    Return the first n rows of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef head(self, n: int = 5) -> polars.DataFrame:\n    \"\"\"Return the first `n` rows of `self`.\"\"\"\n    return self._get_frame().head(n)\n
    "},{"location":"api/#atomlib.HasAtoms.tail","title":"tail","text":"
    tail(n: int = 5) -> DataFrame\n

    Return the last n rows of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef tail(self, n: int = 5) -> polars.DataFrame:\n    \"\"\"Return the last `n` rows of `self`.\"\"\"\n    return self._get_frame().tail(n)\n
    "},{"location":"api/#atomlib.HasAtoms.drop_nulls","title":"drop_nulls","text":"
    drop_nulls(\n    subset: Union[str, Collection[str], None] = None\n) -> DataFrame\n

    Drop rows that contain nulls in any of columns subset.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n    \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n    return self._get_frame().drop_nulls(subset)\n
    "},{"location":"api/#atomlib.HasAtoms.fill_null","title":"fill_null","text":"
    fill_null(\n    value: Any = None,\n    strategy: Optional[FillNullStrategy] = None,\n    limit: Optional[int] = None,\n    matches_supertype: bool = True,\n) -> DataFrame\n

    Fill null values in self, using the specified value or strategy.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef fill_null(\n    self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n    limit: t.Optional[int] = None, matches_supertype: bool = True,\n) -> polars.DataFrame:\n    \"\"\"Fill null values in `self`, using the specified value or strategy.\"\"\"\n    return self._get_frame().fill_null(value, strategy, limit, matches_supertype=matches_supertype)\n
    "},{"location":"api/#atomlib.HasAtoms.fill_nan","title":"fill_nan","text":"
    fill_nan(value: Union[Expr, int, float, None]) -> DataFrame\n

    Fill floating-point NaN values in self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef fill_nan(self, value: t.Union[polars.Expr, int, float, None]) -> polars.DataFrame:\n    \"\"\"Fill floating-point NaN values in `self`.\"\"\"\n    return self._get_frame().fill_nan(value)\n
    "},{"location":"api/#atomlib.HasAtoms.concat","title":"concat classmethod","text":"
    concat(\n    atoms: Union[\n        HasAtomsT,\n        IntoAtoms,\n        Iterable[Union[HasAtomsT, IntoAtoms]],\n    ],\n    *,\n    rechunk: bool = True,\n    how: ConcatMethod = \"vertical\"\n) -> HasAtomsT\n

    Concatenate multiple Atoms together, handling metadata appropriately.

    Source code in atomlib/atoms.py
    @classmethod\ndef concat(cls: t.Type[HasAtomsT],\n           atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n           rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n    \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n    # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n    # same type as ``cls``.\n    if _is_abstract(cls):\n        raise TypeError(\"concat() must be called on a concrete class.\")\n\n    if isinstance(atoms, HasAtoms):\n        atoms = (atoms,)\n    dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n    representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n    if len(dfs) == 0:\n        return representative.with_atoms(Atoms.empty(), 'local')\n\n    if how in ('vertical', 'vertical_relaxed'):\n        # get order from first member\n        cols = dfs[0].columns\n        dfs = [df.select(cols) for df in dfs]\n    elif how == 'inner':\n        cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n        schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n        if len(schema) == 0:\n            raise ValueError(\"Atoms have no columns in common\")\n\n        dfs = [_select_schema(df, schema) for df in dfs]\n        how = 'vertical'\n\n    return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n
    "},{"location":"api/#atomlib.HasAtoms.partition_by","title":"partition_by","text":"
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: Literal[False] = False\n) -> List[Self]\n
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: Literal[True] = ...\n) -> Dict[Any, Self]\n
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: bool = False\n) -> Union[List[Self], Dict[Any, Self]]\n

    Group by the given columns and partition into separate dataframes.

    Return the partitions as a dictionary by specifying as_dict=True.

    Source code in atomlib/atoms.py
    def partition_by(\n    self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n    maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n    \"\"\"\n    Group by the given columns and partition into separate dataframes.\n\n    Return the partitions as a dictionary by specifying `as_dict=True`.\n    \"\"\"\n    if as_dict:\n        d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n        return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n    return [\n        self.with_atoms(Atoms(df, _unchecked=True))\n        for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n    ]\n
    "},{"location":"api/#atomlib.HasAtoms.select","title":"select","text":"
    select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.select)\ndef select(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr,\n) -> polars.DataFrame:\n    \"\"\"\n    Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtoms.select_schema","title":"select_schema","text":"
    select_schema(schema: SchemaDict) -> DataFrame\n

    Select columns from self and cast to the given schema. Raises TypeError if a column is not found or if it can't be cast.

    Source code in atomlib/atoms.py
    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n    \"\"\"\n    Select columns from `self` and cast to the given schema.\n    Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n    \"\"\"\n    return _select_schema(self, schema)\n
    "},{"location":"api/#atomlib.HasAtoms.select_props","title":"select_props","text":"
    select_props(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Self\n

    Select exprs from self, while keeping required columns.

    RETURNS DESCRIPTION Self

    A HasAtoms filtered to contain the

    Self

    specified properties (as well as required columns).

    Source code in atomlib/atoms.py
    def select_props(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Self:\n    \"\"\"\n    Select `exprs` from `self`, while keeping required columns.\n\n    Returns:\n      A [`HasAtoms`][atomlib.atoms.HasAtoms] filtered to contain the\n      specified properties (as well as required columns).\n    \"\"\"\n    props = self._get_frame().lazy().select(*exprs, **named_exprs).drop(_REQUIRED_COLUMNS, strict=False).collect(_eager=True)\n    return self.with_atoms(\n        Atoms(self._get_frame().select(_REQUIRED_COLUMNS).hstack(props), _unchecked=False)\n    )\n
    "},{"location":"api/#atomlib.HasAtoms.try_select","title":"try_select","text":"
    try_select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Optional[DataFrame]\n

    Try to select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns. Returns None if any columns are missing.

    Source code in atomlib/atoms.py
    def try_select(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr,\n) -> t.Optional[polars.DataFrame]:\n    \"\"\"\n    Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns. Returns `None` if any\n    columns are missing.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    try:\n        return self._get_frame().select(*exprs, **named_exprs)\n    except polars.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/#atomlib.HasAtoms.try_get_column","title":"try_get_column","text":"
    try_get_column(name: str) -> Optional[Series]\n

    Try to get a column from self, returning None if it doesn't exist.

    Source code in atomlib/atoms.py
    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n    \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n    try:\n        return self.get_column(name)\n    except polars.exceptions.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/#atomlib.HasAtoms.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n
    Source code in atomlib/atoms.py
    def assert_equal(self, other: t.Any):\n    assert isinstance(other, HasAtoms)\n    assert dict(self.schema) == dict(other.schema)\n    for col in self.schema.keys():\n        polars.testing.assert_series_equal(self[col], other[col], check_names=False, rtol=1e-3, atol=1e-8)\n
    "},{"location":"api/#atomlib.HasAtoms.bbox_atoms","title":"bbox_atoms","text":"
    bbox_atoms() -> BBox3D\n

    Return the bounding box of all the atoms in self.

    Source code in atomlib/atoms.py
    def bbox_atoms(self) -> BBox3D:\n    \"\"\"Return the bounding box of all the atoms in ``self``.\"\"\"\n    return BBox3D.from_pts(self.coords())\n
    "},{"location":"api/#atomlib.HasAtoms.transform_atoms","title":"transform_atoms","text":"
    transform_atoms(\n    transform: IntoTransform3D,\n    selection: Optional[AtomSelection] = None,\n    *,\n    transform_velocities: bool = False\n) -> Self\n

    Transform the atoms in self by transform. If selection is given, only transform the atoms in selection.

    Source code in atomlib/atoms.py
    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                    transform_velocities: bool = False) -> Self:\n    \"\"\"\n    Transform the atoms in `self` by `transform`.\n    If `selection` is given, only transform the atoms in `selection`.\n    \"\"\"\n    transform = Transform3D.make(transform)\n    selection = _selection_to_numpy(self, selection)\n    transformed = self.with_coords(Transform3D.make(transform) @ self.coords(selection), selection)\n    # try to transform velocities as well\n    if transform_velocities and (velocities := self.velocities(selection)) is not None:\n        return transformed.with_velocity(transform.transform_vec(velocities), selection)\n    return transformed\n
    "},{"location":"api/#atomlib.HasAtoms.round_near_zero","title":"round_near_zero","text":"
    round_near_zero(tol: float = 1e-14) -> Self\n

    Round atom position values near zero to zero.

    Source code in atomlib/atoms.py
    def round_near_zero(self, tol: float = 1e-14) -> Self:\n    \"\"\"\n    Round atom position values near zero to zero.\n    \"\"\"\n    return self.with_columns(coords=polars.concat_list(\n        polars.when(_coord_expr(col).abs() >= tol).then(_coord_expr(col)).otherwise(polars.lit(0.))\n        for col in range(3)\n    ).list.to_array(3))\n
    "},{"location":"api/#atomlib.HasAtoms.crop","title":"crop","text":"
    crop(\n    x_min: float = -inf,\n    x_max: float = inf,\n    y_min: float = -inf,\n    y_max: float = inf,\n    z_min: float = -inf,\n    z_max: float = inf,\n) -> Self\n

    Crop, removing all atoms outside of the specified region, inclusive.

    Source code in atomlib/atoms.py
    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf) -> Self:\n    \"\"\"\n    Crop, removing all atoms outside of the specified region, inclusive.\n    \"\"\"\n\n    return self.filter(\n        self.x().is_between(x_min, x_max, closed='both'),\n        self.y().is_between(y_min, y_max, closed='both'),\n        self.z().is_between(z_min, z_max, closed='both'),\n    )\n
    "},{"location":"api/#atomlib.HasAtoms.deduplicate","title":"deduplicate","text":"
    deduplicate(\n    tol: float = 0.001,\n    subset: Iterable[str] = (\"x\", \"y\", \"z\", \"symbol\"),\n    keep: UniqueKeepStrategy = \"first\",\n    maintain_order: bool = True,\n) -> Self\n

    De-duplicate atoms in self. Atoms of the same symbol that are closer than tolerance to each other (by Euclidian distance) will be removed, leaving only the atom specified by keep (defaults to the first atom).

    If subset is specified, only those columns will be included while assessing duplicates. Floating point columns other than 'x', 'y', and 'z' will not by toleranced.

    Source code in atomlib/atoms.py
    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n    \"\"\"\n    De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n    to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n    `keep` (defaults to the first atom).\n\n    If `subset` is specified, only those columns will be included while assessing duplicates.\n    Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n    \"\"\"\n    import scipy.spatial\n\n    cols = set((subset,) if isinstance(subset, str) else subset)\n\n    indices = numpy.arange(len(self))\n\n    spatial_cols = cols.intersection(('x', 'y', 'z'))\n    cols -= spatial_cols\n    if len(spatial_cols) > 0:\n        coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n        tree = scipy.spatial.KDTree(coords)\n\n        # TODO This is a bad algorithm\n        while True:\n            changed = False\n            for (i, j) in tree.query_pairs(tol, 2.):\n                # whenever we encounter a pair, ensure their index matches\n                i_i, i_j = indices[[i, j]]\n                if i_i != i_j:\n                    indices[i] = indices[j] = min(i_i, i_j)\n                    changed = True\n            if not changed:\n                break\n\n        self = self.with_column(polars.Series('_unique_pts', indices))\n        cols.add('_unique_pts')\n\n    frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n    if len(spatial_cols) > 0:\n        frame = frame.drop('_unique_pts')\n\n    return self.with_atoms(Atoms(frame, _unchecked=True))\n
    "},{"location":"api/#atomlib.HasAtoms.with_bounds","title":"with_bounds","text":"
    with_bounds(\n    cell_size: Optional[VecLike] = None,\n    cell_origin: Optional[VecLike] = None,\n) -> \"AtomCell\"\n

    Return a periodic cell with the given orthogonal cell dimensions.

    If cell_size is not specified, it will be assumed (and may be incorrect).

    Source code in atomlib/atoms.py
    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n    \"\"\"\n    Return a periodic cell with the given orthogonal cell dimensions.\n\n    If cell_size is not specified, it will be assumed (and may be incorrect).\n    \"\"\"\n    # TODO: test this\n    from .atomcell import AtomCell\n\n    if cell_size is None:\n        warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n        cell_size = self.bbox().size\n        cell_origin = self.bbox().min\n\n    # TODO test this origin code\n    cell = Cell.from_unit_cell(cell_size)\n    if cell_origin is not None:\n        cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n    return AtomCell(self.get_atoms(), cell, frame='local')\n
    "},{"location":"api/#atomlib.HasAtoms.coords","title":"coords","text":"
    coords(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Literal[\"local\"] = \"local\"\n) -> NDArray[float64]\n

    Return a (N, 3) ndarray of atom coordinates (dtype numpy.float64).

    Source code in atomlib/atoms.py
    def coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> NDArray[numpy.float64]:\n    \"\"\"Return a `(N, 3)` ndarray of atom coordinates (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n    df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n    return df.get_column('coords').to_numpy().astype(numpy.float64)\n
    "},{"location":"api/#atomlib.HasAtoms.x","title":"x","text":"
    x() -> Expr\n
    Source code in atomlib/atoms.py
    def x(self) -> polars.Expr:\n    return polars.col('coords').arr.get(0).alias('x')\n
    "},{"location":"api/#atomlib.HasAtoms.y","title":"y","text":"
    y() -> Expr\n
    Source code in atomlib/atoms.py
    def y(self) -> polars.Expr:\n    return polars.col('coords').arr.get(1).alias('y')\n
    "},{"location":"api/#atomlib.HasAtoms.z","title":"z","text":"
    z() -> Expr\n
    Source code in atomlib/atoms.py
    def z(self) -> polars.Expr:\n    return polars.col('coords').arr.get(2).alias('z')\n
    "},{"location":"api/#atomlib.HasAtoms.velocities","title":"velocities","text":"
    velocities(\n    selection: Optional[AtomSelection] = None,\n) -> Optional[NDArray[float64]]\n

    Return a (N, 3) ndarray of atom velocities (dtype numpy.float64).

    Source code in atomlib/atoms.py
    def velocities(self, selection: t.Optional[AtomSelection] = None) -> t.Optional[NDArray[numpy.float64]]:\n    \"\"\"Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n    if 'velocity' not in self:\n        return None\n\n    df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n    return df.get_column('velocity').to_numpy().astype(numpy.float64)\n
    "},{"location":"api/#atomlib.HasAtoms.types","title":"types","text":"
    types() -> Optional[Series]\n

    Returns a Series of atom types (dtype polars.Int32).

    Source code in atomlib/atoms.py
    def types(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('type')\n
    "},{"location":"api/#atomlib.HasAtoms.masses","title":"masses","text":"
    masses() -> Optional[Series]\n

    Returns a Series of atom masses (dtype polars.Float32).

    Source code in atomlib/atoms.py
    def masses(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('mass')\n
    "},{"location":"api/#atomlib.HasAtoms.add_atom","title":"add_atom","text":"
    add_atom(\n    elem: Union[int, str],\n    x: ArrayLike,\n    /,\n    *,\n    y: None = None,\n    z: None = None,\n    **kwargs: Any,\n) -> Self\n
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: float,\n    y: float,\n    z: float,\n    **kwargs: Any,\n) -> Self\n
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: Union[ArrayLike, float],\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    **kwargs: Any,\n) -> Self\n

    Return a copy of self with an extra atom.

    By default, all extra columns present in self must be specified as **kwargs.

    Try to avoid calling this in a loop (Use HasAtoms.concat instead).

    Source code in atomlib/atoms.py
    def add_atom(self, elem: t.Union[int, str], /,\n             x: t.Union[ArrayLike, float],\n             y: t.Optional[float] = None,\n             z: t.Optional[float] = None,\n             **kwargs: t.Any) -> Self:\n    \"\"\"\n    Return a copy of `self` with an extra atom.\n\n    By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n    Try to avoid calling this in a loop (Use [`HasAtoms.concat`][atomlib.atoms.HasAtoms.concat] instead).\n    \"\"\"\n    if isinstance(elem, int):\n        kwargs.update(elem=elem)\n    else:\n        kwargs.update(symbol=elem)\n    if hasattr(x, '__len__') and len(x) > 1:  # type: ignore\n        (x, y, z) = to_vec3(x)\n    elif y is None or z is None:\n        raise ValueError(\"Must specify vector of positions or x, y, & z.\")\n\n    sym = get_sym(elem) if isinstance(elem, int) else elem\n    d: t.Dict[str, t.Any] = {'x': x, 'y': y, 'z': z, 'symbol': sym, **kwargs}\n    return self.concat(\n        (self, Atoms(d).select_schema(self.schema)),\n        how='vertical'\n    )\n
    "},{"location":"api/#atomlib.HasAtoms.pos","title":"pos","text":"
    pos(\n    x: Sequence[Optional[float]],\n    /,\n    *,\n    y: None = None,\n    z: None = None,\n    tol: float = 1e-06,\n    **kwargs: Any,\n) -> Expr\n
    pos(\n    x: Optional[float] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n
    pos(\n    x: Union[Sequence[Optional[float]], float, None] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n

    Select all atoms at a given position.

    Formally, returns all atoms within a cube of radius tol centered at (x,y,z), exclusive of the cube's surface.

    Additional parameters given as kwargs will be checked as additional parameters (with strict equality).

    Source code in atomlib/atoms.py
    def pos(self,\n        x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n        y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n        tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n    \"\"\"\n    Select all atoms at a given position.\n\n    Formally, returns all atoms within a cube of radius ``tol``\n    centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n    Additional parameters given as ``kwargs`` will be checked\n    as additional parameters (with strict equality).\n    \"\"\"\n\n    if isinstance(x, t.Sequence):\n        (x, y, z) = x\n\n    tol = abs(float(tol))\n    selection = polars.lit(True)\n    if x is not None:\n        selection &= self.x().is_between(x - tol, x + tol, closed='none')\n    if y is not None:\n        selection &= self.y().is_between(y - tol, y + tol, closed='none')\n    if z is not None:\n        selection &= self.z().is_between(z - tol, z + tol, closed='none')\n    for (col, val) in kwargs.items():\n        selection &= (polars.col(col) == val)\n\n    return selection\n
    "},{"location":"api/#atomlib.HasAtoms.with_index","title":"with_index","text":"
    with_index(index: Optional[AtomValues] = None) -> Self\n

    Returns self with a row index added in column 'i' (dtype polars.Int64). If index is not specified, defaults to an existing index or a new index.

    Source code in atomlib/atoms.py
    def with_index(self, index: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n    If `index` is not specified, defaults to an existing index or a new index.\n    \"\"\"\n    if index is None and 'i' in self.columns:\n        return self\n    if index is None:\n        index = numpy.arange(len(self), dtype=numpy.int64)\n    return self.with_column(_values_to_expr(self, index, polars.Int64).alias('i'))\n
    "},{"location":"api/#atomlib.HasAtoms.with_wobble","title":"with_wobble","text":"
    with_wobble(wobble: Optional[AtomValues] = None) -> Self\n

    Return self with the given displacements in column 'wobble' (dtype polars.Float64). If wobble is not specified, defaults to the already-existing wobbles or 0.

    Source code in atomlib/atoms.py
    def with_wobble(self, wobble: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n    \"\"\"\n    if wobble is None and 'wobble' in self.columns:\n        return self\n    wobble = 0. if wobble is None else wobble\n    return self.with_column(_values_to_expr(self, wobble, polars.Float64).alias('wobble'))\n
    "},{"location":"api/#atomlib.HasAtoms.with_occupancy","title":"with_occupancy","text":"
    with_occupancy(\n    frac_occupancy: Optional[AtomValues] = None,\n) -> Self\n

    Return self with the given fractional occupancies (dtype polars.Float64). If frac_occupancy is not specified, defaults to the already-existing occupancies or 1.

    Source code in atomlib/atoms.py
    def with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n    \"\"\"\n    if frac_occupancy is None and 'frac_occupancy' in self.columns:\n        return self\n    frac_occupancy = 1. if frac_occupancy is None else frac_occupancy\n    return self.with_column(_values_to_expr(self, frac_occupancy, polars.Float64).alias('frac_occupancy'))\n
    "},{"location":"api/#atomlib.HasAtoms.apply_wobble","title":"apply_wobble","text":"
    apply_wobble(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    Displace the atoms in self by the amount in the wobble column. wobble is interpretated as a mean-squared displacement, which is distributed equally over each axis.

    Source code in atomlib/atoms.py
    def apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    Displace the atoms in `self` by the amount in the `wobble` column.\n    `wobble` is interpretated as a mean-squared displacement, which is distributed\n    equally over each axis.\n    \"\"\"\n    if 'wobble' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    stddev = self.select((polars.col('wobble') / 3.).sqrt()).to_series().to_numpy()\n    coords = self.coords()\n    coords += stddev[:, None] * rng.standard_normal(coords.shape)\n    return self.with_coords(coords)\n
    "},{"location":"api/#atomlib.HasAtoms.apply_occupancy","title":"apply_occupancy","text":"
    apply_occupancy(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    For each atom in self, use its frac_occupancy to randomly decide whether to remove it.

    Source code in atomlib/atoms.py
    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n    \"\"\"\n    if 'frac_occupancy' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    frac = self.select('frac_occupancy').to_series().to_numpy()\n    choice = rng.binomial(1, frac).astype(numpy.bool_)\n    return self.filter(polars.lit(choice))\n
    "},{"location":"api/#atomlib.HasAtoms.with_type","title":"with_type","text":"
    with_type(types: Optional[AtomValues] = None) -> Self\n

    Return self with the given atom types in column 'type'. If types is not specified, use the already existing types or auto-assign them.

    When auto-assigning, each symbol is given a unique value, case-sensitive. Values are assigned from lowest atomic number to highest. For instance: [\"Ag+\", \"Na\", \"H\", \"Ag\"] => [3, 11, 1, 2]

    Source code in atomlib/atoms.py
    def with_type(self, types: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom types in column 'type'.\n    If `types` is not specified, use the already existing types or auto-assign them.\n\n    When auto-assigning, each symbol is given a unique value, case-sensitive.\n    Values are assigned from lowest atomic number to highest.\n    For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n    \"\"\"\n    if types is not None:\n        return self.with_columns(type=_values_to_expr(self, types, polars.Int32))\n    if 'type' in self.columns:\n        return self\n\n    unique = Atoms(self._get_frame().unique(maintain_order=False, subset=['elem', 'symbol']).sort(['elem', 'symbol']), _unchecked=True)\n    new = self.with_column(polars.Series('type', values=numpy.zeros(len(self)), dtype=polars.Int32))\n\n    logging.warning(\"Auto-assigning element types\")\n    for (i, (elem, sym)) in enumerate(unique.select(('elem', 'symbol')).rows()):\n        print(f\"Assigning type {i+1} to element '{sym}'\")\n        new = new.with_column(polars.when((polars.col('elem') == elem) & (polars.col('symbol') == sym))\n                                    .then(polars.lit(i+1))\n                                    .otherwise(polars.col('type'))\n                                    .alias('type'))\n\n    assert (new.get_column('type') == 0).sum() == 0\n    return new\n
    "},{"location":"api/#atomlib.HasAtoms.with_mass","title":"with_mass","text":"
    with_mass(mass: Optional[ArrayLike] = None) -> Self\n

    Return self with the given atom masses in column 'mass'. If mass is not specified, use the already existing masses or auto-assign them.

    Source code in atomlib/atoms.py
    def with_mass(self, mass: t.Optional[ArrayLike] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom masses in column `'mass'`.\n    If `mass` is not specified, use the already existing masses or auto-assign them.\n    \"\"\"\n    if mass is not None:\n        return self.with_column(_values_to_expr(self, mass, polars.Float32).alias('mass'))\n    if 'mass' in self.columns:\n        return self\n\n    unique_elems = self.get_column('elem').unique()\n    new = self.with_column(polars.Series('mass', values=numpy.zeros(len(self)), dtype=polars.Float32))\n\n    logging.warning(\"Auto-assigning element masses\")\n    for elem in unique_elems:\n        new = new.with_column(polars.when(polars.col('elem') == elem)\n                                    .then(polars.lit(get_mass(elem)))\n                                    .otherwise(polars.col('mass'))\n                                    .alias('mass'))\n\n    assert (new.get_column('mass').abs() < 1e-10).sum() == 0\n    return new\n
    "},{"location":"api/#atomlib.HasAtoms.with_symbol","title":"with_symbol","text":"
    with_symbol(\n    symbols: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n) -> Self\n

    Return self with the given atomic symbols.

    Source code in atomlib/atoms.py
    def with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atomic symbols.\n    \"\"\"\n    if selection is not None:\n        selection = _selection_to_numpy(self, selection)\n        new_symbols = self.get_column('symbol')\n        new_symbols[selection] = polars.Series(list(numpy.broadcast_to(symbols, len(selection))), dtype=polars.Utf8)\n        symbols = new_symbols\n\n    # TODO better cast here\n    symbols = polars.Series('symbol', list(numpy.broadcast_to(symbols, len(self))), dtype=polars.Utf8)\n    return self.with_columns((symbols, get_elem(symbols)))\n
    "},{"location":"api/#atomlib.HasAtoms.with_coords","title":"with_coords","text":"
    with_coords(\n    pts: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Literal[\"local\"] = \"local\"\n) -> Self\n

    Return self replaced with the given atomic positions.

    Source code in atomlib/atoms.py
    def with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic positions.\n    \"\"\"\n    if selection is not None:\n        selection = _selection_to_numpy(self, selection)\n        new_pts = self.coords()\n        pts = numpy.atleast_2d(pts)\n        assert pts.shape[-1] == 3\n        new_pts[selection] = pts\n        pts = new_pts\n\n    # https://github.com/pola-rs/polars/issues/18369\n    pts = numpy.broadcast_to(pts, (len(self), 3)) if len(self) else []\n    return self.with_columns(polars.Series('coords', pts, polars.Array(polars.Float64, 3)))\n
    "},{"location":"api/#atomlib.HasAtoms.with_velocity","title":"with_velocity","text":"
    with_velocity(\n    pts: Optional[ArrayLike] = None,\n    selection: Optional[AtomSelection] = None,\n) -> Self\n

    Return self replaced with the given atomic velocities. If pts is not specified, use the already existing velocities or zero.

    Source code in atomlib/atoms.py
    def with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                  selection: t.Optional[AtomSelection] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic velocities.\n    If `pts` is not specified, use the already existing velocities or zero.\n    \"\"\"\n    if pts is None:\n        if 'velocity' in self:\n            return self\n        all_pts = numpy.zeros((len(self), 3))\n    else:\n        all_pts = self['velocity'].to_numpy()\n\n    if selection is None:\n        all_pts = pts or all_pts\n    elif pts is not None:\n        selection = _selection_to_numpy(self, selection)\n        all_pts = numpy.require(all_pts, requirements=['WRITEABLE'])\n        pts = numpy.atleast_2d(pts)\n        assert pts.shape[-1] == 3\n        all_pts[selection] = pts\n\n    all_pts = numpy.broadcast_to(all_pts, (len(self), 3))\n    return self.with_columns(polars.Series('velocity', all_pts, polars.Array(polars.Float64, 3)))\n
    "},{"location":"api/#atomlib.Cell","title":"Cell dataclass","text":"

    Bases: HasCell

    Internal class for representing the coordinate systems of a crystal.

    The overall transformation from crystal coordinates to real-space coordinates is is split into four transformations, applied from bottom to top. First is n_cells, which scales from fractions of a unit cell to fractions of a supercell. Next is cell_size, which scales to real-space units. ortho is an orthogonalization matrix, a det = 1 upper-triangular matrix which transforms crystal axes to an orthogonal coordinate system. Finally, affine contains any remaining transformations to the local coordinate system, which atoms are stored in.

    Source code in atomlib/cell.py
    @dataclass(frozen=True, init=False)\nclass Cell(HasCell):\n    \"\"\"\n    Internal class for representing the coordinate systems of a crystal.\n\n    The overall transformation from crystal coordinates to real-space coordinates is\n    is split into four transformations, applied from bottom to top. First is `n_cells`,\n    which scales from fractions of a unit cell to fractions of a supercell. Next is\n    `cell_size`, which scales to real-space units. `ortho` is an orthogonalization\n    matrix, a det = 1 upper-triangular matrix which transforms crystal axes to\n    an orthogonal coordinate system. Finally, `affine` contains any remaining\n    transformations to the local coordinate system, which atoms are stored in.\n    \"\"\"\n\n    def get_cell(self) -> Cell:\n        return self\n\n    def with_cell(self: Cell, cell: Cell) -> Cell:\n        return cell\n\n    _affine: AffineTransform3D = AffineTransform3D()\n    \"\"\"\n    Affine transformation. Holds transformation from `'ortho'` to `'local'` coordinates,\n    including rotation away from the standard crystal orientation.\n    \"\"\"\n\n    _ortho: LinearTransform3D = LinearTransform3D()\n    \"\"\"\n    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.\n    \"\"\"\n\n    _cell_size: NDArray[numpy.float64]\n    \"\"\"Unit cell size.\"\"\"\n    _cell_angle: NDArray[numpy.float64] = field(default_factory=lambda: numpy.full(3, numpy.pi/2.))\n    \"\"\"Unit cell angles, in radians.\"\"\"\n    _n_cells: NDArray[numpy.int64] = field(default_factory=lambda: numpy.ones(3, numpy.int64))\n    \"\"\"Number of unit cells.\"\"\"\n    _pbc: NDArray[numpy.bool_] = field(default_factory=lambda: numpy.ones(3, numpy.bool_))\n    \"\"\"Flags indicating the presence of periodic boundary conditions along each axis.\"\"\"\n\n    def __init__(self, *,\n        affine: t.Optional[AffineTransform3D] = None, ortho: t.Optional[LinearTransform3D] = None,\n        cell_size: VecLike, cell_angle: t.Optional[VecLike] = None,\n        n_cells: t.Optional[VecLike] = None, pbc: t.Optional[VecLike] = None):\n\n        object.__setattr__(self, '_affine', AffineTransform3D() if affine is None else affine)\n        object.__setattr__(self, '_ortho', LinearTransform3D() if ortho is None else ortho)\n        object.__setattr__(self, '_cell_size', to_vec3(cell_size))\n        object.__setattr__(self, '_cell_angle', numpy.full(3, numpy.pi/2.) if cell_angle is None else to_vec3(cell_angle))\n        object.__setattr__(self, '_n_cells', numpy.ones(3, numpy.int_) if n_cells is None else to_vec3(n_cells, numpy.int64))\n        object.__setattr__(self, '_pbc', numpy.ones(3, numpy.bool_) if pbc is None else to_vec3(pbc, numpy.bool_))\n\n    @staticmethod\n    def from_unit_cell(cell_size: VecLike, cell_angle: t.Optional[VecLike] = None, n_cells: t.Optional[VecLike] = None,\n                       pbc: t.Optional[VecLike] = None):\n        return Cell(\n            ortho=cell_to_ortho([1.]*3, cell_angle),\n            n_cells=to_vec3([1]*3 if n_cells is None else n_cells, numpy.int_),\n            cell_size=to_vec3(cell_size),\n            cell_angle=to_vec3([numpy.pi/2.]*3 if cell_angle is None else cell_angle),\n            pbc=pbc\n        )\n\n    @staticmethod\n    def from_ortho(ortho: AffineTransform3D, n_cells: t.Optional[VecLike] = None, pbc: t.Optional[VecLike] = None):\n        lin = ortho.to_linear()\n        # decompose into orthogonal and upper triangular\n        q, r = numpy.linalg.qr(lin.inner)\n\n        # flip QR decomposition so R has positive diagonals\n        signs = numpy.sign(numpy.diagonal(r))\n        # multiply flips to columns of Q, rows of R\n        q = q * signs\n        r = r * signs[:, None]\n        #numpy.testing.assert_allclose(q @ r, lin.inner)\n        if numpy.linalg.det(q) < 0:\n            warn(\"Crystal is left-handed. This is currently unsupported, and may cause errors.\")\n            # currently, behavior is to leave `ortho` proper, and move the inversion into the affine transform\n\n        cell_size, cell_angle = ortho_to_cell(lin)\n        return Cell(\n            affine=LinearTransform3D(q).translate(ortho.translation()),\n            ortho=LinearTransform3D(r / cell_size).round_near_zero(),\n            cell_size=cell_size, cell_angle=cell_angle,\n            n_cells=to_vec3([1]*3 if n_cells is None else n_cells, numpy.int_),\n            pbc=pbc,\n        )\n\n    def __str__(self) -> str:\n        return \"\\n\".join((\n            self.__class__.__name__,\n            f\"Cell size: {self.cell_size!r}\",\n            f\"Cell angle: {self.cell_angle!r}\",\n            f\"# cells: {self.n_cells!r}\",\n            f\"pbc: {self.pbc!r}\",\n        ))\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"ortho={self.ortho}, affine={self.affine}, cell_size={self.cell_size}, \"\n            f\"cell_angle={self.cell_angle}, n_cells={self.n_cells}, pbc={self.pbc})\"\n        )\n\n    def _repr_pretty_(self, p: t.Any, cycle: bool) -> None:\n        p.text(f\"{self.__class__.__name__}(...)\") if cycle else p.text(str(self))\n
    "},{"location":"api/#atomlib.Cell.affine","title":"affine property","text":"
    affine: AffineTransform3D\n

    Affine transformation. Holds transformation from 'ortho' to 'local' coordinates, including rotation away from the standard crystal orientation.

    "},{"location":"api/#atomlib.Cell.ortho","title":"ortho property","text":"
    ortho: LinearTransform3D\n

    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.

    "},{"location":"api/#atomlib.Cell.metric","title":"metric property","text":"
    metric: LinearTransform3D\n

    Cell metric tensor

    Returns the dot product between every combination of basis vectors. :math:\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j

    "},{"location":"api/#atomlib.Cell.cell_size","title":"cell_size property","text":"
    cell_size: NDArray[float64]\n

    Unit cell size.

    "},{"location":"api/#atomlib.Cell.cell_angle","title":"cell_angle property","text":"
    cell_angle: NDArray[float64]\n

    Unit cell angles, in radians.

    "},{"location":"api/#atomlib.Cell.n_cells","title":"n_cells property","text":"
    n_cells: NDArray[int_]\n

    Number of unit cells.

    "},{"location":"api/#atomlib.Cell.pbc","title":"pbc property","text":"
    pbc: NDArray[bool_]\n

    Flags indicating the presence of periodic boundary conditions along each axis.

    "},{"location":"api/#atomlib.Cell.ortho_size","title":"ortho_size property","text":"
    ortho_size: NDArray[float64]\n

    Return size of orthogonal unit cell.

    Equivalent to the diagonal of the orthogonalization matrix.

    "},{"location":"api/#atomlib.Cell.box_size","title":"box_size property","text":"
    box_size: NDArray[float64]\n

    Return size of the cell box.

    Equivalent to self.n_cells * self.cell_size.

    "},{"location":"api/#atomlib.Cell.bbox","title":"bbox class-attribute instance-attribute","text":"
    bbox = bbox_cell\n
    "},{"location":"api/#atomlib.Cell.get_transform","title":"get_transform","text":"
    get_transform(\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n

    In the two-argument form, get the transform to frame_to from frame_from. In the one-argument form, get the transform from local coordinates to 'frame'.

    Source code in atomlib/cell.py
    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n    \"\"\"\n    In the two-argument form, get the transform to `frame_to` from `frame_from`.\n    In the one-argument form, get the transform from local coordinates to 'frame'.\n    \"\"\"\n    transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n    transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n    if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n        return AffineTransform3D()\n    return transform_to.inverse() @ transform_from\n
    "},{"location":"api/#atomlib.Cell.corners","title":"corners","text":"
    corners(frame: CoordinateFrame = 'local') -> ndarray\n
    Source code in atomlib/cell.py
    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n    corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n    return self.get_transform(frame, 'cell_box') @ corners\n
    "},{"location":"api/#atomlib.Cell.bbox_cell","title":"bbox_cell","text":"
    bbox_cell(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the bounding box of the cell box in the given coordinate system.

    Source code in atomlib/cell.py
    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n    return BBox3D.from_pts(self.corners(frame))\n
    "},{"location":"api/#atomlib.Cell.is_orthogonal","title":"is_orthogonal","text":"
    is_orthogonal(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal (axes are at right angles.)

    Source code in atomlib/cell.py
    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n    return self.ortho.is_diagonal(tol=tol)\n
    "},{"location":"api/#atomlib.Cell.is_orthogonal_in_local","title":"is_orthogonal_in_local","text":"
    is_orthogonal_in_local(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal and aligned with the local coordinate system.

    Source code in atomlib/cell.py
    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n    transform = (self.affine @ self.ortho).to_linear()\n    if not transform.is_scaled_orthogonal(tol):\n        return False\n    normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n    # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n    return all(\n        any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n        for row in normed\n    )\n
    "},{"location":"api/#atomlib.Cell.to_ortho","title":"to_ortho","text":"
    to_ortho() -> AffineTransform3D\n
    Source code in atomlib/cell.py
    def to_ortho(self) -> AffineTransform3D:\n    return self.get_transform('local', 'cell_box')\n
    "},{"location":"api/#atomlib.Cell.transform_cell","title":"transform_cell","text":"
    transform_cell(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> HasCellT\n

    Apply the given transform to the unit cell, and return a new Cell. The transform is applied in coordinate frame 'frame'. Orthogonal and affine transformations are applied to the affine matrix component, while skew and scaling is applied to the orthogonalization matrix/cell_size.

    Source code in atomlib/cell.py
    def transform_cell(self: HasCellT, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> HasCellT:\n    \"\"\"\n    Apply the given transform to the unit cell, and return a new `Cell`.\n    The transform is applied in coordinate frame 'frame'.\n    Orthogonal and affine transformations are applied to the affine matrix component,\n    while skew and scaling is applied to the orthogonalization matrix/cell_size.\n    \"\"\"\n    transform = t.cast(AffineTransform3D, self.change_transform(transform, 'local', frame))\n    if not transform.to_linear().is_orthogonal():\n        raise NotImplementedError()\n    return self.with_cell(Cell(\n        affine=transform @ self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size,\n        cell_angle=self.cell_angle,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.Cell.strain_orthogonal","title":"strain_orthogonal","text":"
    strain_orthogonal() -> HasCellT\n

    Orthogonalize using strain.

    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane. For small displacements, no hydrostatic strain is applied (volume is conserved).

    Source code in atomlib/cell.py
    def strain_orthogonal(self: HasCellT) -> HasCellT:\n    \"\"\"\n    Orthogonalize using strain.\n\n    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n    For small displacements, no hydrostatic strain is applied (volume is conserved).\n    \"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=LinearTransform3D(),\n        cell_size=self.cell_size,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.Cell.repeat","title":"repeat","text":"
    repeat(n: Union[int, VecLike]) -> HasCellT\n

    Tile the cell by n in each dimension.

    Source code in atomlib/cell.py
    def repeat(self: HasCellT, n: t.Union[int, VecLike]) -> HasCellT:\n    \"\"\"Tile the cell by `n` in each dimension.\"\"\"\n    ns = numpy.broadcast_to(n, 3)\n    if not numpy.issubdtype(ns.dtype, numpy.integer):\n        raise ValueError(\"repeat() argument must be an integer or integer array.\")\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size,\n        cell_angle=self.cell_angle,\n        n_cells=self.n_cells * numpy.broadcast_to(n, 3),\n        pbc = self.pbc | (ns > 1)  # assume periodic along repeated directions\n    ))\n
    "},{"location":"api/#atomlib.Cell.explode","title":"explode","text":"
    explode() -> HasCellT\n

    Materialize repeated cells as one supercell.

    Source code in atomlib/cell.py
    def explode(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*self.n_cells,\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.Cell.explode_z","title":"explode_z","text":"
    explode_z() -> HasCellT\n

    Materialize repeated cells as one supercell in z.

    Source code in atomlib/cell.py
    def explode_z(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n        n_cells=[*self.n_cells[:2], 1],\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.Cell.crop","title":"crop","text":"
    crop(\n    x_min: float = -inf,\n    x_max: float = inf,\n    y_min: float = -inf,\n    y_max: float = inf,\n    z_min: float = -inf,\n    z_max: float = inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> HasCellT\n

    Crop self to the given extents. For a non-orthogonal cell, this must be specified in cell coordinates. This function implicity explodes the cell as well.

    Source code in atomlib/cell.py
    def crop(self: HasCellT, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n         frame: CoordinateFrame = 'local') -> HasCellT:\n    \"\"\"\n    Crop `self` to the given extents. For a non-orthogonal\n    cell, this must be specified in cell coordinates. This\n    function implicity `explode`s the cell as well.\n    \"\"\"\n\n    if not frame.lower().startswith('cell'):\n        if not self.is_orthogonal():\n            raise ValueError(\"Cannot crop a non-orthogonal cell in orthogonal coordinates. Use crop_atoms instead.\")\n\n    min = to_vec3([x_min, y_min, z_min])\n    max = to_vec3([x_max, y_max, z_max])\n    (min, max) = self.get_transform('cell_box', frame).transform([min, max])\n    new_box = BBox3D(min, max) & BBox3D.unit()\n    cropped = (new_box.min > 0.) | (new_box.max < 1.)\n\n    return self.with_cell(Cell(\n        affine=self.affine @ AffineTransform3D.translate(-new_box.min),\n        ortho=self.ortho,\n        cell_size=new_box.size * self.cell_size * numpy.where(cropped, self.n_cells, 1),\n        n_cells=numpy.where(cropped, 1, self.n_cells),\n        cell_angle=self.cell_angle,\n        pbc=self.pbc & ~cropped  # remove periodicity along cropped directions\n    ))\n
    "},{"location":"api/#atomlib.Cell.change_transform","title":"change_transform","text":"
    change_transform(\n    transform: AffineTransform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n

    Coordinate-change a transformation from frame_from into frame_to.

    Source code in atomlib/cell.py
    def change_transform(self, transform: Transform3D,\n                     frame_to: t.Optional[CoordinateFrame] = None,\n                     frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n    \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n    if frame_to == frame_from and frame_to is not None:\n        return transform\n    coord_change = self.get_transform(frame_to, frame_from)\n    return coord_change @ transform @ coord_change.inverse()\n
    "},{"location":"api/#atomlib.Cell.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n
    Source code in atomlib/cell.py
    def assert_equal(self, other: t.Any):\n    assert isinstance(other, HasCell) and type(self) is type(other)\n    numpy.testing.assert_array_almost_equal(self.affine.inner, other.affine.inner, 6)\n    numpy.testing.assert_array_almost_equal(self.ortho.inner, other.ortho.inner, 6)\n    numpy.testing.assert_array_almost_equal(self.cell_size, other.cell_size, 6)\n    numpy.testing.assert_array_equal(self.n_cells, other.n_cells)\n    numpy.testing.assert_array_equal(self.pbc, other.pbc)\n
    "},{"location":"api/#atomlib.Cell.get_cell","title":"get_cell","text":"
    get_cell() -> Cell\n
    Source code in atomlib/cell.py
    def get_cell(self) -> Cell:\n    return self\n
    "},{"location":"api/#atomlib.Cell.with_cell","title":"with_cell","text":"
    with_cell(cell: Cell) -> Cell\n
    Source code in atomlib/cell.py
    def with_cell(self: Cell, cell: Cell) -> Cell:\n    return cell\n
    "},{"location":"api/#atomlib.Cell.from_unit_cell","title":"from_unit_cell staticmethod","text":"
    from_unit_cell(\n    cell_size: VecLike,\n    cell_angle: Optional[VecLike] = None,\n    n_cells: Optional[VecLike] = None,\n    pbc: Optional[VecLike] = None,\n)\n
    Source code in atomlib/cell.py
    @staticmethod\ndef from_unit_cell(cell_size: VecLike, cell_angle: t.Optional[VecLike] = None, n_cells: t.Optional[VecLike] = None,\n                   pbc: t.Optional[VecLike] = None):\n    return Cell(\n        ortho=cell_to_ortho([1.]*3, cell_angle),\n        n_cells=to_vec3([1]*3 if n_cells is None else n_cells, numpy.int_),\n        cell_size=to_vec3(cell_size),\n        cell_angle=to_vec3([numpy.pi/2.]*3 if cell_angle is None else cell_angle),\n        pbc=pbc\n    )\n
    "},{"location":"api/#atomlib.Cell.from_ortho","title":"from_ortho staticmethod","text":"
    from_ortho(\n    ortho: AffineTransform3D,\n    n_cells: Optional[VecLike] = None,\n    pbc: Optional[VecLike] = None,\n)\n
    Source code in atomlib/cell.py
    @staticmethod\ndef from_ortho(ortho: AffineTransform3D, n_cells: t.Optional[VecLike] = None, pbc: t.Optional[VecLike] = None):\n    lin = ortho.to_linear()\n    # decompose into orthogonal and upper triangular\n    q, r = numpy.linalg.qr(lin.inner)\n\n    # flip QR decomposition so R has positive diagonals\n    signs = numpy.sign(numpy.diagonal(r))\n    # multiply flips to columns of Q, rows of R\n    q = q * signs\n    r = r * signs[:, None]\n    #numpy.testing.assert_allclose(q @ r, lin.inner)\n    if numpy.linalg.det(q) < 0:\n        warn(\"Crystal is left-handed. This is currently unsupported, and may cause errors.\")\n        # currently, behavior is to leave `ortho` proper, and move the inversion into the affine transform\n\n    cell_size, cell_angle = ortho_to_cell(lin)\n    return Cell(\n        affine=LinearTransform3D(q).translate(ortho.translation()),\n        ortho=LinearTransform3D(r / cell_size).round_near_zero(),\n        cell_size=cell_size, cell_angle=cell_angle,\n        n_cells=to_vec3([1]*3 if n_cells is None else n_cells, numpy.int_),\n        pbc=pbc,\n    )\n
    "},{"location":"api/#atomlib.HasCell","title":"HasCell","text":"Source code in atomlib/cell.py
    class HasCell:\n    # abstract methods\n\n    @abc.abstractmethod\n    def get_cell(self) -> Cell:\n        \"\"\"Get the cell contained in ``self``. This should be a low cost method.\"\"\"\n        ...\n\n    @abc.abstractmethod\n    def with_cell(self: HasCellT, cell: Cell) -> HasCellT:\n        \"\"\"Replace the cell in ``self`` with ``cell``.\"\"\"\n        ...\n\n    # getters\n\n    @property\n    def affine(self) -> AffineTransform3D:\n        \"\"\"\n        Affine transformation. Holds transformation from 'ortho' to 'local' coordinates,\n        including rotation away from the standard crystal orientation.\n        \"\"\"\n        return self.get_cell()._affine\n\n    @property\n    def ortho(self) -> LinearTransform3D:\n        \"\"\"\n        Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.\n        \"\"\"\n        return self.get_cell()._ortho\n\n    @property\n    def metric(self) -> LinearTransform3D:\n        r\"\"\"\n        Cell metric tensor\n\n        Returns the dot product between every combination of basis vectors.\n        :math:`\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j`\n        \"\"\"\n        ortho = self.get_cell()._ortho.scale(self.cell_size)\n        return ortho.T @ ortho\n\n    @property\n    def cell_size(self) -> NDArray[numpy.float64]:\n        \"\"\"Unit cell size.\"\"\"\n        return self.get_cell()._cell_size\n\n    @property\n    def cell_angle(self) -> NDArray[numpy.float64]:\n        \"\"\"Unit cell angles, in radians.\"\"\"\n        return self.get_cell()._cell_angle\n\n    @property\n    def n_cells(self) -> NDArray[numpy.int_]:\n        \"\"\"Number of unit cells.\"\"\"\n        return self.get_cell()._n_cells\n\n    @property\n    def pbc(self) -> NDArray[numpy.bool_]:\n        \"\"\"Flags indicating the presence of periodic boundary conditions along each axis.\"\"\"\n        return self.get_cell()._pbc\n\n    @property\n    def ortho_size(self) -> NDArray[numpy.float64]:\n        \"\"\"\n        Return size of orthogonal unit cell.\n\n        Equivalent to the diagonal of the orthogonalization matrix.\n        \"\"\"\n        return self.cell_size * numpy.diag(self.ortho.inner)\n\n    @property\n    def box_size(self) -> NDArray[numpy.float64]:\n        \"\"\"\n        Return size of the cell box.\n\n        Equivalent to ``self.n_cells * self.cell_size``.\n        \"\"\"\n        return self.n_cells * self.cell_size\n\n    # get transforms\n\n    def _get_transform_to_local(self, frame: CoordinateFrame) -> AffineTransform3D:\n        \"\"\"Get the transform from `frame` to local coordinates.\"\"\"\n        frame = t.cast(CoordinateFrame, frame.lower())\n\n        if frame == 'local' or frame == 'global':\n            return LinearTransform3D()\n\n        if frame == 'linear':\n            return self.affine.to_translation()\n\n        if frame.startswith('cell'):\n            transform = self.affine @ self.ortho\n            cell_size = self.cell_size\n        elif frame.startswith('ortho'):\n            transform = self.affine\n            cell_size = self.ortho_size\n        else:\n            raise ValueError(f\"Unknown coordinate frame '{frame}'\")\n\n        if '_' not in frame:\n            return transform\n        end = frame.split('_', 2)[1]\n        if end == 'frac':\n            return transform @ LinearTransform3D.scale(cell_size)\n        if end == 'box':\n            return transform @ LinearTransform3D.scale(cell_size * self.n_cells)\n        raise ValueError(f\"Unknown coordinate frame '{frame}'\")\n\n    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n        \"\"\"\n        In the two-argument form, get the transform to `frame_to` from `frame_from`.\n        In the one-argument form, get the transform from local coordinates to 'frame'.\n        \"\"\"\n        transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n        transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n        if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n            return AffineTransform3D()\n        return transform_to.inverse() @ transform_from\n\n    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n        corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n        return self.get_transform(frame, 'cell_box') @ corners\n\n    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n        \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n        return BBox3D.from_pts(self.corners(frame))\n\n    bbox = bbox_cell\n\n    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n        \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n        return self.ortho.is_diagonal(tol=tol)\n\n    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n        \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n        transform = (self.affine @ self.ortho).to_linear()\n        if not transform.is_scaled_orthogonal(tol):\n            return False\n        normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n        # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n        return all(\n            any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n            for row in normed\n        )\n\n    def _cell_size_in_local(self) -> Vec3:\n        \"\"\"Calculate cell_size in the local coordinate system. Assumes `self.is_orthogonal_in_local()`.\"\"\"\n        return numpy.abs(self.get_transform('local', 'ortho').transform_vec(self.cell_size))\n\n    def _box_size_in_local(self) -> Vec3:\n        \"\"\"Calculate box_size in the local coordinate system. Assumes `self.is_orthogonal_in_local()`.\"\"\"\n        return numpy.abs(self.get_transform('local', 'ortho').transform_vec(self.box_size))\n\n    def _n_cells_in_local(self) -> NDArray[numpy.int_]:\n        \"\"\"Calculate n_cells after any local rotation. Assumes `self.is_orthogonal_in_local()`.\"\"\"\n        return numpy.abs(numpy.round(self.get_transform('local', 'ortho').transform_vec(self.n_cells)).astype(int))\n\n    def to_ortho(self) -> AffineTransform3D:\n        return self.get_transform('local', 'cell_box')\n\n    def transform_cell(self: HasCellT, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> HasCellT:\n        \"\"\"\n        Apply the given transform to the unit cell, and return a new `Cell`.\n        The transform is applied in coordinate frame 'frame'.\n        Orthogonal and affine transformations are applied to the affine matrix component,\n        while skew and scaling is applied to the orthogonalization matrix/cell_size.\n        \"\"\"\n        transform = t.cast(AffineTransform3D, self.change_transform(transform, 'local', frame))\n        if not transform.to_linear().is_orthogonal():\n            raise NotImplementedError()\n        return self.with_cell(Cell(\n            affine=transform @ self.affine,\n            ortho=self.ortho,\n            cell_size=self.cell_size,\n            cell_angle=self.cell_angle,\n            n_cells=self.n_cells,\n            pbc=self.pbc,\n        ))\n\n    def strain_orthogonal(self: HasCellT) -> HasCellT:\n        \"\"\"\n        Orthogonalize using strain.\n\n        Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n        For small displacements, no hydrostatic strain is applied (volume is conserved).\n        \"\"\"\n        return self.with_cell(Cell(\n            affine=self.affine,\n            ortho=LinearTransform3D(),\n            cell_size=self.cell_size,\n            n_cells=self.n_cells,\n            pbc=self.pbc,\n        ))\n\n    def repeat(self: HasCellT, n: t.Union[int, VecLike]) -> HasCellT:\n        \"\"\"Tile the cell by `n` in each dimension.\"\"\"\n        ns = numpy.broadcast_to(n, 3)\n        if not numpy.issubdtype(ns.dtype, numpy.integer):\n            raise ValueError(\"repeat() argument must be an integer or integer array.\")\n        return self.with_cell(Cell(\n            affine=self.affine,\n            ortho=self.ortho,\n            cell_size=self.cell_size,\n            cell_angle=self.cell_angle,\n            n_cells=self.n_cells * numpy.broadcast_to(n, 3),\n            pbc = self.pbc | (ns > 1)  # assume periodic along repeated directions\n        ))\n\n    def explode(self: HasCellT) -> HasCellT:\n        \"\"\"Materialize repeated cells as one supercell.\"\"\"\n        return self.with_cell(Cell(\n            affine=self.affine,\n            ortho=self.ortho,\n            cell_size=self.cell_size*self.n_cells,\n            cell_angle=self.cell_angle,\n            pbc=self.pbc,\n        ))\n\n    def explode_z(self: HasCellT) -> HasCellT:\n        \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n        return self.with_cell(Cell(\n            affine=self.affine,\n            ortho=self.ortho,\n            cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n            n_cells=[*self.n_cells[:2], 1],\n            cell_angle=self.cell_angle,\n            pbc=self.pbc,\n        ))\n\n    def crop(self: HasCellT, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n             y_min: float = -numpy.inf, y_max: float = numpy.inf,\n             z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n             frame: CoordinateFrame = 'local') -> HasCellT:\n        \"\"\"\n        Crop `self` to the given extents. For a non-orthogonal\n        cell, this must be specified in cell coordinates. This\n        function implicity `explode`s the cell as well.\n        \"\"\"\n\n        if not frame.lower().startswith('cell'):\n            if not self.is_orthogonal():\n                raise ValueError(\"Cannot crop a non-orthogonal cell in orthogonal coordinates. Use crop_atoms instead.\")\n\n        min = to_vec3([x_min, y_min, z_min])\n        max = to_vec3([x_max, y_max, z_max])\n        (min, max) = self.get_transform('cell_box', frame).transform([min, max])\n        new_box = BBox3D(min, max) & BBox3D.unit()\n        cropped = (new_box.min > 0.) | (new_box.max < 1.)\n\n        return self.with_cell(Cell(\n            affine=self.affine @ AffineTransform3D.translate(-new_box.min),\n            ortho=self.ortho,\n            cell_size=new_box.size * self.cell_size * numpy.where(cropped, self.n_cells, 1),\n            n_cells=numpy.where(cropped, 1, self.n_cells),\n            cell_angle=self.cell_angle,\n            pbc=self.pbc & ~cropped  # remove periodicity along cropped directions\n        ))\n\n    @t.overload\n    def change_transform(self, transform: AffineTransform3D,\n                         frame_to: t.Optional[CoordinateFrame] = None,\n                         frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n        ...\n\n    @t.overload\n    def change_transform(self, transform: Transform3D,\n                         frame_to: t.Optional[CoordinateFrame] = None,\n                         frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n        ...\n\n    def change_transform(self, transform: Transform3D,\n                         frame_to: t.Optional[CoordinateFrame] = None,\n                         frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n        \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n        if frame_to == frame_from and frame_to is not None:\n            return transform\n        coord_change = self.get_transform(frame_to, frame_from)\n        return coord_change @ transform @ coord_change.inverse()\n\n    def assert_equal(self, other: t.Any):\n        assert isinstance(other, HasCell) and type(self) is type(other)\n        numpy.testing.assert_array_almost_equal(self.affine.inner, other.affine.inner, 6)\n        numpy.testing.assert_array_almost_equal(self.ortho.inner, other.ortho.inner, 6)\n        numpy.testing.assert_array_almost_equal(self.cell_size, other.cell_size, 6)\n        numpy.testing.assert_array_equal(self.n_cells, other.n_cells)\n        numpy.testing.assert_array_equal(self.pbc, other.pbc)\n
    "},{"location":"api/#atomlib.HasCell.affine","title":"affine property","text":"
    affine: AffineTransform3D\n

    Affine transformation. Holds transformation from 'ortho' to 'local' coordinates, including rotation away from the standard crystal orientation.

    "},{"location":"api/#atomlib.HasCell.ortho","title":"ortho property","text":"
    ortho: LinearTransform3D\n

    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.

    "},{"location":"api/#atomlib.HasCell.metric","title":"metric property","text":"
    metric: LinearTransform3D\n

    Cell metric tensor

    Returns the dot product between every combination of basis vectors. :math:\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j

    "},{"location":"api/#atomlib.HasCell.cell_size","title":"cell_size property","text":"
    cell_size: NDArray[float64]\n

    Unit cell size.

    "},{"location":"api/#atomlib.HasCell.cell_angle","title":"cell_angle property","text":"
    cell_angle: NDArray[float64]\n

    Unit cell angles, in radians.

    "},{"location":"api/#atomlib.HasCell.n_cells","title":"n_cells property","text":"
    n_cells: NDArray[int_]\n

    Number of unit cells.

    "},{"location":"api/#atomlib.HasCell.pbc","title":"pbc property","text":"
    pbc: NDArray[bool_]\n

    Flags indicating the presence of periodic boundary conditions along each axis.

    "},{"location":"api/#atomlib.HasCell.ortho_size","title":"ortho_size property","text":"
    ortho_size: NDArray[float64]\n

    Return size of orthogonal unit cell.

    Equivalent to the diagonal of the orthogonalization matrix.

    "},{"location":"api/#atomlib.HasCell.box_size","title":"box_size property","text":"
    box_size: NDArray[float64]\n

    Return size of the cell box.

    Equivalent to self.n_cells * self.cell_size.

    "},{"location":"api/#atomlib.HasCell.bbox","title":"bbox class-attribute instance-attribute","text":"
    bbox = bbox_cell\n
    "},{"location":"api/#atomlib.HasCell.get_cell","title":"get_cell abstractmethod","text":"
    get_cell() -> Cell\n

    Get the cell contained in self. This should be a low cost method.

    Source code in atomlib/cell.py
    @abc.abstractmethod\ndef get_cell(self) -> Cell:\n    \"\"\"Get the cell contained in ``self``. This should be a low cost method.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasCell.with_cell","title":"with_cell abstractmethod","text":"
    with_cell(cell: Cell) -> HasCellT\n

    Replace the cell in self with cell.

    Source code in atomlib/cell.py
    @abc.abstractmethod\ndef with_cell(self: HasCellT, cell: Cell) -> HasCellT:\n    \"\"\"Replace the cell in ``self`` with ``cell``.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasCell.get_transform","title":"get_transform","text":"
    get_transform(\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n

    In the two-argument form, get the transform to frame_to from frame_from. In the one-argument form, get the transform from local coordinates to 'frame'.

    Source code in atomlib/cell.py
    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n    \"\"\"\n    In the two-argument form, get the transform to `frame_to` from `frame_from`.\n    In the one-argument form, get the transform from local coordinates to 'frame'.\n    \"\"\"\n    transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n    transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n    if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n        return AffineTransform3D()\n    return transform_to.inverse() @ transform_from\n
    "},{"location":"api/#atomlib.HasCell.corners","title":"corners","text":"
    corners(frame: CoordinateFrame = 'local') -> ndarray\n
    Source code in atomlib/cell.py
    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n    corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n    return self.get_transform(frame, 'cell_box') @ corners\n
    "},{"location":"api/#atomlib.HasCell.bbox_cell","title":"bbox_cell","text":"
    bbox_cell(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the bounding box of the cell box in the given coordinate system.

    Source code in atomlib/cell.py
    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n    return BBox3D.from_pts(self.corners(frame))\n
    "},{"location":"api/#atomlib.HasCell.is_orthogonal","title":"is_orthogonal","text":"
    is_orthogonal(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal (axes are at right angles.)

    Source code in atomlib/cell.py
    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n    return self.ortho.is_diagonal(tol=tol)\n
    "},{"location":"api/#atomlib.HasCell.is_orthogonal_in_local","title":"is_orthogonal_in_local","text":"
    is_orthogonal_in_local(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal and aligned with the local coordinate system.

    Source code in atomlib/cell.py
    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n    transform = (self.affine @ self.ortho).to_linear()\n    if not transform.is_scaled_orthogonal(tol):\n        return False\n    normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n    # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n    return all(\n        any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n        for row in normed\n    )\n
    "},{"location":"api/#atomlib.HasCell.to_ortho","title":"to_ortho","text":"
    to_ortho() -> AffineTransform3D\n
    Source code in atomlib/cell.py
    def to_ortho(self) -> AffineTransform3D:\n    return self.get_transform('local', 'cell_box')\n
    "},{"location":"api/#atomlib.HasCell.transform_cell","title":"transform_cell","text":"
    transform_cell(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> HasCellT\n

    Apply the given transform to the unit cell, and return a new Cell. The transform is applied in coordinate frame 'frame'. Orthogonal and affine transformations are applied to the affine matrix component, while skew and scaling is applied to the orthogonalization matrix/cell_size.

    Source code in atomlib/cell.py
    def transform_cell(self: HasCellT, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> HasCellT:\n    \"\"\"\n    Apply the given transform to the unit cell, and return a new `Cell`.\n    The transform is applied in coordinate frame 'frame'.\n    Orthogonal and affine transformations are applied to the affine matrix component,\n    while skew and scaling is applied to the orthogonalization matrix/cell_size.\n    \"\"\"\n    transform = t.cast(AffineTransform3D, self.change_transform(transform, 'local', frame))\n    if not transform.to_linear().is_orthogonal():\n        raise NotImplementedError()\n    return self.with_cell(Cell(\n        affine=transform @ self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size,\n        cell_angle=self.cell_angle,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.HasCell.strain_orthogonal","title":"strain_orthogonal","text":"
    strain_orthogonal() -> HasCellT\n

    Orthogonalize using strain.

    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane. For small displacements, no hydrostatic strain is applied (volume is conserved).

    Source code in atomlib/cell.py
    def strain_orthogonal(self: HasCellT) -> HasCellT:\n    \"\"\"\n    Orthogonalize using strain.\n\n    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n    For small displacements, no hydrostatic strain is applied (volume is conserved).\n    \"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=LinearTransform3D(),\n        cell_size=self.cell_size,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.HasCell.repeat","title":"repeat","text":"
    repeat(n: Union[int, VecLike]) -> HasCellT\n

    Tile the cell by n in each dimension.

    Source code in atomlib/cell.py
    def repeat(self: HasCellT, n: t.Union[int, VecLike]) -> HasCellT:\n    \"\"\"Tile the cell by `n` in each dimension.\"\"\"\n    ns = numpy.broadcast_to(n, 3)\n    if not numpy.issubdtype(ns.dtype, numpy.integer):\n        raise ValueError(\"repeat() argument must be an integer or integer array.\")\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size,\n        cell_angle=self.cell_angle,\n        n_cells=self.n_cells * numpy.broadcast_to(n, 3),\n        pbc = self.pbc | (ns > 1)  # assume periodic along repeated directions\n    ))\n
    "},{"location":"api/#atomlib.HasCell.explode","title":"explode","text":"
    explode() -> HasCellT\n

    Materialize repeated cells as one supercell.

    Source code in atomlib/cell.py
    def explode(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*self.n_cells,\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.HasCell.explode_z","title":"explode_z","text":"
    explode_z() -> HasCellT\n

    Materialize repeated cells as one supercell in z.

    Source code in atomlib/cell.py
    def explode_z(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n        n_cells=[*self.n_cells[:2], 1],\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.HasCell.crop","title":"crop","text":"
    crop(\n    x_min: float = -inf,\n    x_max: float = inf,\n    y_min: float = -inf,\n    y_max: float = inf,\n    z_min: float = -inf,\n    z_max: float = inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> HasCellT\n

    Crop self to the given extents. For a non-orthogonal cell, this must be specified in cell coordinates. This function implicity explodes the cell as well.

    Source code in atomlib/cell.py
    def crop(self: HasCellT, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n         frame: CoordinateFrame = 'local') -> HasCellT:\n    \"\"\"\n    Crop `self` to the given extents. For a non-orthogonal\n    cell, this must be specified in cell coordinates. This\n    function implicity `explode`s the cell as well.\n    \"\"\"\n\n    if not frame.lower().startswith('cell'):\n        if not self.is_orthogonal():\n            raise ValueError(\"Cannot crop a non-orthogonal cell in orthogonal coordinates. Use crop_atoms instead.\")\n\n    min = to_vec3([x_min, y_min, z_min])\n    max = to_vec3([x_max, y_max, z_max])\n    (min, max) = self.get_transform('cell_box', frame).transform([min, max])\n    new_box = BBox3D(min, max) & BBox3D.unit()\n    cropped = (new_box.min > 0.) | (new_box.max < 1.)\n\n    return self.with_cell(Cell(\n        affine=self.affine @ AffineTransform3D.translate(-new_box.min),\n        ortho=self.ortho,\n        cell_size=new_box.size * self.cell_size * numpy.where(cropped, self.n_cells, 1),\n        n_cells=numpy.where(cropped, 1, self.n_cells),\n        cell_angle=self.cell_angle,\n        pbc=self.pbc & ~cropped  # remove periodicity along cropped directions\n    ))\n
    "},{"location":"api/#atomlib.HasCell.change_transform","title":"change_transform","text":"
    change_transform(\n    transform: AffineTransform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n

    Coordinate-change a transformation from frame_from into frame_to.

    Source code in atomlib/cell.py
    def change_transform(self, transform: Transform3D,\n                     frame_to: t.Optional[CoordinateFrame] = None,\n                     frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n    \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n    if frame_to == frame_from and frame_to is not None:\n        return transform\n    coord_change = self.get_transform(frame_to, frame_from)\n    return coord_change @ transform @ coord_change.inverse()\n
    "},{"location":"api/#atomlib.HasCell.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n
    Source code in atomlib/cell.py
    def assert_equal(self, other: t.Any):\n    assert isinstance(other, HasCell) and type(self) is type(other)\n    numpy.testing.assert_array_almost_equal(self.affine.inner, other.affine.inner, 6)\n    numpy.testing.assert_array_almost_equal(self.ortho.inner, other.ortho.inner, 6)\n    numpy.testing.assert_array_almost_equal(self.cell_size, other.cell_size, 6)\n    numpy.testing.assert_array_equal(self.n_cells, other.n_cells)\n    numpy.testing.assert_array_equal(self.pbc, other.pbc)\n
    "},{"location":"api/#atomlib.AtomCell","title":"AtomCell dataclass","text":"

    Bases: AtomCellIOMixin, HasAtomCell

    Cell of atoms with known size and periodic boundary conditions.

    Source code in atomlib/atomcell.py
    @dataclass(init=False, repr=False, frozen=True)\nclass AtomCell(AtomCellIOMixin, HasAtomCell):\n    \"\"\"\n    Cell of atoms with known size and periodic boundary conditions.\n    \"\"\"\n\n    atoms: Atoms\n    \"\"\"Atoms in the cell. Stored in 'local' coordinates (i.e. relative to the enclosing group but not relative to box dimensions).\"\"\"\n\n    cell: Cell\n    \"\"\"Cell coordinate system.\"\"\"\n\n    frame: CoordinateFrame = 'local'\n    \"\"\"Coordinate frame 'atoms' are stored in.\"\"\"\n\n    def get_cell(self) -> Cell:\n        return self.cell\n\n    def with_cell(self, cell: Cell) -> Self:\n        return self.__class__(self.atoms, cell, frame=self.frame, keep_frame=True)\n\n    def get_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> Atoms:\n        \"\"\"Get atoms contained in ``self``, in the given coordinate frame.\"\"\"\n\n        if frame is None or frame == self.get_frame():\n            return self.atoms\n        return self.atoms.transform(self.get_transform(frame, self.get_frame()))\n\n    def with_atoms(self, atoms: HasAtoms, frame: t.Optional[CoordinateFrame] = None) -> Self:\n        frame = frame if frame is not None else self.frame\n        return self.__class__(atoms.get_atoms(), cell=self.cell, frame=frame, keep_frame=True)\n        #return replace(self, atoms=atoms, frame = frame if frame is not None else self.frame, keep_frame=True)\n\n    def get_frame(self) -> CoordinateFrame:\n        \"\"\"Get the coordinate frame atoms are stored in.\"\"\"\n        return self.frame\n\n    @classmethod\n    def _combine_metadata(cls: t.Type[AtomCellT], *atoms: HasAtoms, n: t.Optional[int] = None) -> AtomCellT:\n        \"\"\"\n        When combining multiple [`HasAtoms`][atomlib.atoms.HasAtoms], check that they are compatible with each other,\n        and return a 'representative' which best represents the combined metadata.\n        Implementors should treat [`Atoms`][atomlib.atoms.Atoms] as acceptable, but having no metadata.\n        \"\"\"\n        if n is not None:\n            rep = atoms[n]\n            if not isinstance(rep, AtomCell):\n                raise ValueError(f\"Atoms #{n} has no cell\")\n        else:\n            atom_cells = [a for a in atoms if isinstance(a, AtomCell)]\n            if len(atom_cells) == 0:\n                raise TypeError(\"No AtomCells to combine\")\n            rep = atom_cells[0]\n            if not all(a.cell == rep.cell for a in atom_cells[1:]):\n                raise TypeError(\"Can't combine AtomCells with different cells\")\n\n        return cls(Atoms.empty(), frame=rep.frame, cell=rep.cell)\n\n    @classmethod\n    def from_ortho(cls, atoms: IntoAtoms, ortho: LinearTransform3D, *,\n                   n_cells: t.Optional[VecLike] = None,\n                   frame: CoordinateFrame = 'local',\n                   keep_frame: bool = False):\n        \"\"\"\n        Make an atom cell given a list of atoms and an orthogonalization matrix.\n        Atoms are assumed to be in the coordinate system `frame`.\n        \"\"\"\n        cell = Cell.from_ortho(ortho, n_cells)\n        return cls(atoms, cell, frame=frame, keep_frame=keep_frame)\n\n    @classmethod\n    def from_unit_cell(cls, atoms: IntoAtoms, cell_size: VecLike,\n                       cell_angle: t.Optional[VecLike] = None, *,\n                       n_cells: t.Optional[VecLike] = None,\n                       frame: CoordinateFrame = 'local',\n                       keep_frame: bool = False):\n        \"\"\"\n        Make a cell given a list of atoms and unit cell parameters.\n        Atoms are assumed to be in the coordinate system `frame`.\n        \"\"\"\n        cell = Cell.from_unit_cell(cell_size, cell_angle, n_cells=n_cells)\n        return cls(atoms, cell, frame=frame, keep_frame=keep_frame)\n\n    def __init__(self, atoms: IntoAtoms, cell: Cell, *,\n                 frame: CoordinateFrame = 'local',\n                 keep_frame: bool = False):\n        atoms = Atoms(atoms)\n        # by default, store in local coordinates\n        if not keep_frame and frame != 'local':\n            atoms = atoms.transform(cell.get_transform('local', frame))\n            frame = 'local'\n\n        object.__setattr__(self, 'atoms', atoms)\n        object.__setattr__(self, 'cell', cell)\n        object.__setattr__(self, 'frame', frame)\n\n        self.__post_init__()\n\n    def __post_init__(self):\n        pass\n\n    def orthogonalize(self) -> OrthoCell:\n        if self.is_orthogonal():\n            return OrthoCell(self.atoms, self.cell, frame=self.frame)\n        raise NotImplementedError()\n\n    def clone(self: AtomCellT) -> AtomCellT:\n        \"\"\"Make a deep copy of `self`.\"\"\"\n        return self.__class__(**{field.name: copy.deepcopy(getattr(self, field.name)) for field in fields(self)})\n\n    def assert_equal(self, other: t.Any):\n        \"\"\"Assert this structure is equal to `other`\"\"\"\n        assert isinstance(other, AtomCell)\n        self.cell.assert_equal(other.cell)\n        self.get_atoms('local').assert_equal(other.get_atoms('local'))\n\n    def _str_parts(self) -> t.Iterable[t.Any]:\n        return (\n            f\"Cell size:  {self.cell.cell_size!s}\",\n            f\"Cell angle: {self.cell.cell_angle!s}\",\n            f\"# Cells: {self.cell.n_cells!s}\",\n            f\"Frame: {self.frame}\",\n            self.atoms,\n        )\n\n    def __str__(self) -> str:\n        return \"\\n\".join(map(str, self._str_parts()))\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}({self.atoms!r}, cell={self.cell!r}, frame={self.frame})\"\n\n    def _repr_pretty_(self, p: t.Any, cycle: bool) -> None:\n        p.text(f'{self.__class__.__name__}(...)') if cycle else p.text(str(self))\n
    "},{"location":"api/#atomlib.AtomCell.affine","title":"affine property","text":"
    affine: AffineTransform3D\n

    Affine transformation. Holds transformation from 'ortho' to 'local' coordinates, including rotation away from the standard crystal orientation.

    "},{"location":"api/#atomlib.AtomCell.ortho","title":"ortho property","text":"
    ortho: LinearTransform3D\n

    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.

    "},{"location":"api/#atomlib.AtomCell.metric","title":"metric property","text":"
    metric: LinearTransform3D\n

    Cell metric tensor

    Returns the dot product between every combination of basis vectors. :math:\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j

    "},{"location":"api/#atomlib.AtomCell.cell_size","title":"cell_size property","text":"
    cell_size: NDArray[float64]\n

    Unit cell size.

    "},{"location":"api/#atomlib.AtomCell.cell_angle","title":"cell_angle property","text":"
    cell_angle: NDArray[float64]\n

    Unit cell angles, in radians.

    "},{"location":"api/#atomlib.AtomCell.n_cells","title":"n_cells property","text":"
    n_cells: NDArray[int_]\n

    Number of unit cells.

    "},{"location":"api/#atomlib.AtomCell.pbc","title":"pbc property","text":"
    pbc: NDArray[bool_]\n

    Flags indicating the presence of periodic boundary conditions along each axis.

    "},{"location":"api/#atomlib.AtomCell.ortho_size","title":"ortho_size property","text":"
    ortho_size: NDArray[float64]\n

    Return size of orthogonal unit cell.

    Equivalent to the diagonal of the orthogonalization matrix.

    "},{"location":"api/#atomlib.AtomCell.box_size","title":"box_size property","text":"
    box_size: NDArray[float64]\n

    Return size of the cell box.

    Equivalent to self.n_cells * self.cell_size.

    "},{"location":"api/#atomlib.AtomCell.columns","title":"columns property","text":"
    columns: List[str]\n

    Return the column names in self.

    RETURNS DESCRIPTION List[str]

    A sequence of column names

    "},{"location":"api/#atomlib.AtomCell.dtypes","title":"dtypes property","text":"
    dtypes: List[DataType]\n

    Return the datatypes in self.

    RETURNS DESCRIPTION List[DataType]

    A sequence of column DataTypes

    "},{"location":"api/#atomlib.AtomCell.schema","title":"schema property","text":"
    schema: Schema\n

    Return the schema of self.

    RETURNS DESCRIPTION Schema

    A dictionary of column names and DataTypes

    "},{"location":"api/#atomlib.AtomCell.with_column","title":"with_column class-attribute instance-attribute","text":"
    with_column = with_columns\n
    "},{"location":"api/#atomlib.AtomCell.unique","title":"unique class-attribute instance-attribute","text":"
    unique = deduplicate\n
    "},{"location":"api/#atomlib.AtomCell.atoms","title":"atoms instance-attribute","text":"
    atoms: Atoms\n

    Atoms in the cell. Stored in 'local' coordinates (i.e. relative to the enclosing group but not relative to box dimensions).

    "},{"location":"api/#atomlib.AtomCell.cell","title":"cell instance-attribute","text":"
    cell: Cell\n

    Cell coordinate system.

    "},{"location":"api/#atomlib.AtomCell.frame","title":"frame class-attribute instance-attribute","text":"
    frame: CoordinateFrame = 'local'\n

    Coordinate frame 'atoms' are stored in.

    "},{"location":"api/#atomlib.AtomCell.get_transform","title":"get_transform","text":"
    get_transform(\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n

    In the two-argument form, get the transform to frame_to from frame_from. In the one-argument form, get the transform from local coordinates to 'frame'.

    Source code in atomlib/cell.py
    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n    \"\"\"\n    In the two-argument form, get the transform to `frame_to` from `frame_from`.\n    In the one-argument form, get the transform from local coordinates to 'frame'.\n    \"\"\"\n    transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n    transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n    if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n        return AffineTransform3D()\n    return transform_to.inverse() @ transform_from\n
    "},{"location":"api/#atomlib.AtomCell.corners","title":"corners","text":"
    corners(frame: CoordinateFrame = 'local') -> ndarray\n
    Source code in atomlib/cell.py
    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n    corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n    return self.get_transform(frame, 'cell_box') @ corners\n
    "},{"location":"api/#atomlib.AtomCell.bbox_cell","title":"bbox_cell","text":"
    bbox_cell(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the bounding box of the cell box in the given coordinate system.

    Source code in atomlib/cell.py
    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n    return BBox3D.from_pts(self.corners(frame))\n
    "},{"location":"api/#atomlib.AtomCell.bbox","title":"bbox","text":"
    bbox(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the combined bounding box of the cell and atoms in the given coordinate system. To get the cell or atoms bounding box only, use bbox_cell or bbox_atoms.

    Source code in atomlib/atomcell.py
    def bbox(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"\n    Return the combined bounding box of the cell and atoms in the given coordinate system.\n    To get the cell or atoms bounding box only, use [`bbox_cell`][atomlib.atomcell.HasAtomCell.bbox_cell] or [`bbox_atoms`][atomlib.atomcell.HasAtomCell.bbox_atoms].\n    \"\"\"\n    return self.bbox_atoms(frame) | self.bbox_cell(frame)\n
    "},{"location":"api/#atomlib.AtomCell.is_orthogonal","title":"is_orthogonal","text":"
    is_orthogonal(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal (axes are at right angles.)

    Source code in atomlib/cell.py
    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n    return self.ortho.is_diagonal(tol=tol)\n
    "},{"location":"api/#atomlib.AtomCell.is_orthogonal_in_local","title":"is_orthogonal_in_local","text":"
    is_orthogonal_in_local(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal and aligned with the local coordinate system.

    Source code in atomlib/cell.py
    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n    transform = (self.affine @ self.ortho).to_linear()\n    if not transform.is_scaled_orthogonal(tol):\n        return False\n    normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n    # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n    return all(\n        any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n        for row in normed\n    )\n
    "},{"location":"api/#atomlib.AtomCell.to_ortho","title":"to_ortho","text":"
    to_ortho() -> AffineTransform3D\n
    Source code in atomlib/cell.py
    def to_ortho(self) -> AffineTransform3D:\n    return self.get_transform('local', 'cell_box')\n
    "},{"location":"api/#atomlib.AtomCell.transform_cell","title":"transform_cell","text":"
    transform_cell(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> Self\n

    Apply the given transform to the unit cell, without changing atom positions. The transform is applied in coordinate frame 'frame'.

    Source code in atomlib/atomcell.py
    def transform_cell(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n    \"\"\"\n    Apply the given transform to the unit cell, without changing atom positions.\n    The transform is applied in coordinate frame 'frame'.\n    \"\"\"\n    return self.with_cell(self.get_cell().transform_cell(transform, frame=frame))\n
    "},{"location":"api/#atomlib.AtomCell.strain_orthogonal","title":"strain_orthogonal","text":"
    strain_orthogonal() -> HasCellT\n

    Orthogonalize using strain.

    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane. For small displacements, no hydrostatic strain is applied (volume is conserved).

    Source code in atomlib/cell.py
    def strain_orthogonal(self: HasCellT) -> HasCellT:\n    \"\"\"\n    Orthogonalize using strain.\n\n    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n    For small displacements, no hydrostatic strain is applied (volume is conserved).\n    \"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=LinearTransform3D(),\n        cell_size=self.cell_size,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.AtomCell.repeat","title":"repeat","text":"
    repeat(n: Union[int, VecLike]) -> Self\n

    Tile the cell

    Source code in atomlib/atomcell.py
    def repeat(self, n: t.Union[int, VecLike]) -> Self:\n    \"\"\"Tile the cell\"\"\"\n    ns = numpy.broadcast_to(n, 3)\n    if not numpy.issubdtype(ns.dtype, numpy.integer):\n        raise ValueError(\"repeat() argument must be an integer or integer array.\")\n\n    cells = numpy.stack(numpy.meshgrid(*map(numpy.arange, ns))) \\\n        .reshape(3, -1).T.astype(float)\n    cells = cells * self.box_size\n\n    atoms = self.get_atoms('cell')\n    atoms = Atoms.concat([\n        atoms.transform(AffineTransform3D.translate(cell))\n        for cell in cells\n    ]) #.transform(self.cell.get_transform('local', 'cell_frac'))\n    return self.with_atoms(atoms, 'cell').with_cell(self.get_cell().repeat(ns))\n
    "},{"location":"api/#atomlib.AtomCell.explode","title":"explode","text":"
    explode() -> Self\n

    Materialize repeated cells as one supercell.

    Source code in atomlib/atomcell.py
    def explode(self) -> Self:\n    \"\"\"Materialize repeated cells as one supercell.\"\"\"\n    frame = self.get_frame()\n\n    return self.with_atoms(self.get_atoms('local'), 'local') \\\n        .with_cell(self.get_cell().explode()) \\\n        .to_frame(frame)\n
    "},{"location":"api/#atomlib.AtomCell.explode_z","title":"explode_z","text":"
    explode_z() -> HasCellT\n

    Materialize repeated cells as one supercell in z.

    Source code in atomlib/cell.py
    def explode_z(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n        n_cells=[*self.n_cells[:2], 1],\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.AtomCell.crop","title":"crop","text":"
    crop(\n    x_min: float = -inf,\n    x_max: float = inf,\n    y_min: float = -inf,\n    y_max: float = inf,\n    z_min: float = -inf,\n    z_max: float = inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> Self\n

    Crop atoms and cell to the given extents. For a non-orthogonal cell, this must be specified in cell coordinates. This function implicity explodes the cell as well.

    To crop atoms only, use crop_atoms instead.

    Source code in atomlib/atomcell.py
    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n         frame: CoordinateFrame = 'local') -> Self:\n    \"\"\"\n    Crop atoms and cell to the given extents. For a non-orthogonal\n    cell, this must be specified in cell coordinates. This\n    function implicity `explode`s the cell as well.\n\n    To crop atoms only, use `crop_atoms` instead.\n    \"\"\"\n\n    cell = self.get_cell().crop(x_min, x_max, y_min, y_max, z_min, z_max, frame=frame)\n    atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n    return self.with_cell(cell).with_atoms(atoms)\n
    "},{"location":"api/#atomlib.AtomCell.change_transform","title":"change_transform","text":"
    change_transform(\n    transform: AffineTransform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n

    Coordinate-change a transformation from frame_from into frame_to.

    Source code in atomlib/cell.py
    def change_transform(self, transform: Transform3D,\n                     frame_to: t.Optional[CoordinateFrame] = None,\n                     frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n    \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n    if frame_to == frame_from and frame_to is not None:\n        return transform\n    coord_change = self.get_transform(frame_to, frame_from)\n    return coord_change @ transform @ coord_change.inverse()\n
    "},{"location":"api/#atomlib.AtomCell.describe","title":"describe","text":"
    describe(\n    percentiles: Union[Sequence[float], float, None] = (\n        0.25,\n        0.5,\n        0.75,\n    ),\n    *,\n    interpolation: RollingInterpolationMethod = \"nearest\",\n    frame: Optional[CoordinateFrame] = None\n) -> DataFrame\n

    Return summary statistics for self. See DataFrame.describe for more information.

    PARAMETER DESCRIPTION percentiles

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), 50% (median), and 75% (third quartile).

    TYPE: Union[Sequence[float], float, None] DEFAULT: (0.25, 0.5, 0.75)

    RETURNS DESCRIPTION DataFrame

    A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n             interpolation: RollingInterpolationMethod = 'nearest',\n             frame: t.Optional[CoordinateFrame] = None) -> polars.DataFrame:\n    \"\"\"\n    Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n    Args:\n      percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                   50% (median), and 75% (third quartile).\n\n    Returns:\n      A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.with_columns","title":"with_columns","text":"
    with_columns(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self\n

    Return a copy of self with the given columns added.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_columns(self,\n                 *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 frame: t.Optional[CoordinateFrame] = None,\n                 **named_exprs: IntoExpr) -> Self:\n    \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.insert_column","title":"insert_column","text":"
    insert_column(index: int, column: Series) -> DataFrame\n
    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n    return self._get_frame().insert_column(index, column)\n
    "},{"location":"api/#atomlib.AtomCell.get_column","title":"get_column","text":"
    get_column(\n    name: str, *, frame: Optional[CoordinateFrame] = None\n) -> Series\n

    Get the specified column from self, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef get_column(self, name: str, *, frame: t.Optional[CoordinateFrame] = None) -> polars.Series:\n    \"\"\"\n    Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.get_columns","title":"get_columns","text":"
    get_columns(\n    *, frame: Optional[CoordinateFrame] = None\n) -> List[Series]\n

    Return all columns from self as a list of Series.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef get_columns(self, *, frame: t.Optional[CoordinateFrame] = None) -> t.List[polars.Series]:\n    \"\"\"\n    Return all columns from `self` as a list of [`Series`][polars.Series].\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.get_column_index","title":"get_column_index","text":"
    get_column_index(name: str) -> int\n

    Get the index of a column by name, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_column_index)\ndef get_column_index(self, name: str) -> int:\n    \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.group_by","title":"group_by","text":"
    group_by(\n    *by: Union[IntoExpr, Iterable[IntoExpr]],\n    maintain_order: bool = False,\n    frame: Optional[CoordinateFrame] = None,\n    **named_by: IntoExpr\n) -> GroupBy\n

    Start a group by operation. See DataFrame.group_by for more information.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n             maintain_order: bool = False, frame: t.Optional[CoordinateFrame] = None,\n             **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n    \"\"\"\n    Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.pipe","title":"pipe","text":"
    pipe(\n    function: Callable[Concatenate[HasAtomCellT, P], T],\n    *args: args,\n    **kwargs: kwargs\n) -> T\n

    Apply function to self (in method-call syntax).

    Source code in atomlib/atomcell.py
    def pipe(self: HasAtomCellT, function: t.Callable[Concatenate[HasAtomCellT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n    \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n    return function(self, *args, **kwargs)\n
    "},{"location":"api/#atomlib.AtomCell.drop","title":"drop","text":"
    drop(\n    *columns: Union[str, Iterable[str]], strict: bool = True\n) -> DataFrame\n

    Return self with the specified columns removed.

    Source code in atomlib/atoms.py
    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n    \"\"\"Return `self` with the specified columns removed.\"\"\"\n    return self._get_frame().drop(*columns, strict=strict)\n
    "},{"location":"api/#atomlib.AtomCell.filter","title":"filter","text":"
    filter(\n    *predicates: Union[\n        None,\n        IntoExprColumn,\n        Iterable[IntoExprColumn],\n        bool,\n        List[bool],\n        ndarray,\n    ],\n    frame: Optional[CoordinateFrame] = None,\n    **constraints: Any\n) -> Self\n

    Filter self, removing rows which evaluate to False.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef filter(\n    self,\n    *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n    frame: t.Optional[CoordinateFrame] = None,\n    **constraints: t.Any,\n) -> Self:\n    \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.sort","title":"sort","text":"
    sort(\n    by: Union[IntoExpr, Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: Union[bool, Sequence[bool]] = False,\n    nulls_last: bool = False\n) -> Self\n

    Sort the atoms in self by the given columns/expressions.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef sort(\n    self,\n    by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: t.Union[bool, t.Sequence[bool]] = False,\n    nulls_last: bool = False,\n) -> Self:\n    \"\"\"\n    Sort the atoms in `self` by the given columns/expressions.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.slice","title":"slice","text":"
    slice(\n    offset: int,\n    length: Optional[int] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return a slice of the rows in self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef slice(self, offset: int, length: t.Optional[int] = None, *,\n          frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return a slice of the rows in `self`.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.head","title":"head","text":"
    head(\n    n: int = 5, *, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return the first n rows of self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef head(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return the first `n` rows of `self`.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.tail","title":"tail","text":"
    tail(\n    n: int = 5, *, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return the last n rows of self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef tail(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return the last `n` rows of `self`.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.drop_nulls","title":"drop_nulls","text":"
    drop_nulls(\n    subset: Union[str, Collection[str], None] = None\n) -> DataFrame\n

    Drop rows that contain nulls in any of columns subset.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n    \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n    return self._get_frame().drop_nulls(subset)\n
    "},{"location":"api/#atomlib.AtomCell.fill_null","title":"fill_null","text":"
    fill_null(\n    value: Any = None,\n    strategy: Optional[FillNullStrategy] = None,\n    limit: Optional[int] = None,\n    matches_supertype: bool = True,\n) -> Self\n
    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef fill_null(\n    self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n    limit: t.Optional[int] = None, matches_supertype: bool = True,\n) -> Self:\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.fill_nan","title":"fill_nan","text":"
    fill_nan(\n    value: Union[Expr, int, float, None],\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n
    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef fill_nan(self, value: t.Union[polars.Expr, int, float, None], *,\n             frame: t.Optional[CoordinateFrame] = None) -> Self:\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.concat","title":"concat classmethod","text":"
    concat(\n    atoms: Union[\n        HasAtomsT,\n        IntoAtoms,\n        Iterable[Union[HasAtomsT, IntoAtoms]],\n    ],\n    *,\n    rechunk: bool = True,\n    how: ConcatMethod = \"vertical\"\n) -> HasAtomsT\n

    Concatenate multiple Atoms together, handling metadata appropriately.

    Source code in atomlib/atoms.py
    @classmethod\ndef concat(cls: t.Type[HasAtomsT],\n           atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n           rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n    \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n    # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n    # same type as ``cls``.\n    if _is_abstract(cls):\n        raise TypeError(\"concat() must be called on a concrete class.\")\n\n    if isinstance(atoms, HasAtoms):\n        atoms = (atoms,)\n    dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n    representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n    if len(dfs) == 0:\n        return representative.with_atoms(Atoms.empty(), 'local')\n\n    if how in ('vertical', 'vertical_relaxed'):\n        # get order from first member\n        cols = dfs[0].columns\n        dfs = [df.select(cols) for df in dfs]\n    elif how == 'inner':\n        cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n        schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n        if len(schema) == 0:\n            raise ValueError(\"Atoms have no columns in common\")\n\n        dfs = [_select_schema(df, schema) for df in dfs]\n        how = 'vertical'\n\n    return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n
    "},{"location":"api/#atomlib.AtomCell.partition_by","title":"partition_by","text":"
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: Literal[False] = False\n) -> List[Self]\n
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: Literal[True] = ...\n) -> Dict[Any, Self]\n
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: bool = False\n) -> Union[List[Self], Dict[Any, Self]]\n

    Group by the given columns and partition into separate dataframes.

    Return the partitions as a dictionary by specifying as_dict=True.

    Source code in atomlib/atoms.py
    def partition_by(\n    self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n    maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n    \"\"\"\n    Group by the given columns and partition into separate dataframes.\n\n    Return the partitions as a dictionary by specifying `as_dict=True`.\n    \"\"\"\n    if as_dict:\n        d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n        return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n    return [\n        self.with_atoms(Atoms(df, _unchecked=True))\n        for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n    ]\n
    "},{"location":"api/#atomlib.AtomCell.select","title":"select","text":"
    select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef select(\n    self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> polars.DataFrame:\n    \"\"\"\n    Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.select_schema","title":"select_schema","text":"
    select_schema(schema: SchemaDict) -> DataFrame\n

    Select columns from self and cast to the given schema. Raises TypeError if a column is not found or if it can't be cast.

    Source code in atomlib/atoms.py
    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n    \"\"\"\n    Select columns from `self` and cast to the given schema.\n    Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n    \"\"\"\n    return _select_schema(self, schema)\n
    "},{"location":"api/#atomlib.AtomCell.select_props","title":"select_props","text":"
    select_props(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self\n

    Select exprs from self, while keeping required columns. Doesn't affect the cell.

    RETURNS DESCRIPTION Self

    A HasAtomCell filtered to contain

    Self

    the specified properties (as well as required columns).

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef select_props(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self:\n    \"\"\"\n    Select `exprs` from `self`, while keeping required columns.\n    Doesn't affect the cell.\n\n    Returns:\n      A [`HasAtomCell`][atomlib.atomcell.HasAtomCell] filtered to contain\n      the specified properties (as well as required columns).\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.try_select","title":"try_select","text":"
    try_select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Optional[DataFrame]\n

    Try to select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns. Returns None if any columns are missing.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef try_select(\n    self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> t.Optional[polars.DataFrame]:\n    \"\"\"\n    Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns. Returns `None` if any\n    columns are missing.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.try_get_column","title":"try_get_column","text":"
    try_get_column(name: str) -> Optional[Series]\n

    Try to get a column from self, returning None if it doesn't exist.

    Source code in atomlib/atoms.py
    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n    \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n    try:\n        return self.get_column(name)\n    except polars.exceptions.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/#atomlib.AtomCell.bbox_atoms","title":"bbox_atoms","text":"
    bbox_atoms(\n    frame: Optional[CoordinateFrame] = None,\n) -> BBox3D\n

    Return the bounding box of all the atoms in self, in the given coordinate frame.

    Source code in atomlib/atomcell.py
    def bbox_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> BBox3D:\n    \"\"\"Return the bounding box of all the atoms in `self`, in the given coordinate frame.\"\"\"\n    return self.get_atoms(frame).bbox()\n
    "},{"location":"api/#atomlib.AtomCell.transform_atoms","title":"transform_atoms","text":"
    transform_atoms(\n    transform: IntoTransform3D,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: CoordinateFrame = \"local\",\n    transform_velocities: bool = False\n) -> Self\n

    Transform the atoms in self by transform. If selection is given, only transform the atoms in selection.

    Source code in atomlib/atomcell.py
    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                    frame: CoordinateFrame = 'local', transform_velocities: bool = False) -> Self:\n    \"\"\"\n    Transform the atoms in `self` by `transform`.\n    If `selection` is given, only transform the atoms in `selection`.\n    \"\"\"\n    transform = self.change_transform(Transform3D.make(transform), self.get_frame(), frame)\n    return self.with_atoms(self.get_atoms(self.get_frame()).transform(transform, selection, transform_velocities=transform_velocities))\n
    "},{"location":"api/#atomlib.AtomCell.transform","title":"transform","text":"
    transform(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> Self\n
    Source code in atomlib/atomcell.py
    def transform(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n    if isinstance(transform, Transform3D) and not isinstance(transform, AffineTransform3D):\n        raise ValueError(\"Non-affine transforms cannot change the box dimensions. Use 'transform_atoms' instead.\")\n    # TODO: cleanup once tests pass\n    # coordinate change the transform into atomic coordinates\n    new_cell = self.get_cell().transform_cell(transform, frame)\n    transform = self.get_cell().change_transform(transform, self.get_frame(), frame)\n    return self.with_atoms(self.get_atoms().transform(transform), self.get_frame()).with_cell(new_cell)\n
    "},{"location":"api/#atomlib.AtomCell.round_near_zero","title":"round_near_zero","text":"
    round_near_zero(\n    tol: float = 1e-14,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Round atom position values near zero to zero.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef round_near_zero(self, tol: float = 1e-14, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Round atom position values near zero to zero.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.crop_atoms","title":"crop_atoms","text":"
    crop_atoms(\n    x_min: float = -inf,\n    x_max: float = inf,\n    y_min: float = -inf,\n    y_max: float = inf,\n    z_min: float = -inf,\n    z_max: float = inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> Self\n
    Source code in atomlib/atomcell.py
    def crop_atoms(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n               y_min: float = -numpy.inf, y_max: float = numpy.inf,\n               z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n               frame: CoordinateFrame = 'local') -> Self:\n    atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n    return self.with_atoms(atoms)\n
    "},{"location":"api/#atomlib.AtomCell.deduplicate","title":"deduplicate","text":"
    deduplicate(\n    tol: float = 0.001,\n    subset: Iterable[str] = (\"x\", \"y\", \"z\", \"symbol\"),\n    keep: UniqueKeepStrategy = \"first\",\n    maintain_order: bool = True,\n) -> Self\n

    De-duplicate atoms in self. Atoms of the same symbol that are closer than tolerance to each other (by Euclidian distance) will be removed, leaving only the atom specified by keep (defaults to the first atom).

    If subset is specified, only those columns will be included while assessing duplicates. Floating point columns other than 'x', 'y', and 'z' will not by toleranced.

    Source code in atomlib/atoms.py
    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n    \"\"\"\n    De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n    to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n    `keep` (defaults to the first atom).\n\n    If `subset` is specified, only those columns will be included while assessing duplicates.\n    Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n    \"\"\"\n    import scipy.spatial\n\n    cols = set((subset,) if isinstance(subset, str) else subset)\n\n    indices = numpy.arange(len(self))\n\n    spatial_cols = cols.intersection(('x', 'y', 'z'))\n    cols -= spatial_cols\n    if len(spatial_cols) > 0:\n        coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n        tree = scipy.spatial.KDTree(coords)\n\n        # TODO This is a bad algorithm\n        while True:\n            changed = False\n            for (i, j) in tree.query_pairs(tol, 2.):\n                # whenever we encounter a pair, ensure their index matches\n                i_i, i_j = indices[[i, j]]\n                if i_i != i_j:\n                    indices[i] = indices[j] = min(i_i, i_j)\n                    changed = True\n            if not changed:\n                break\n\n        self = self.with_column(polars.Series('_unique_pts', indices))\n        cols.add('_unique_pts')\n\n    frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n    if len(spatial_cols) > 0:\n        frame = frame.drop('_unique_pts')\n\n    return self.with_atoms(Atoms(frame, _unchecked=True))\n
    "},{"location":"api/#atomlib.AtomCell.with_bounds","title":"with_bounds","text":"
    with_bounds(\n    cell_size: Optional[VecLike] = None,\n    cell_origin: Optional[VecLike] = None,\n) -> \"AtomCell\"\n

    Return a periodic cell with the given orthogonal cell dimensions.

    If cell_size is not specified, it will be assumed (and may be incorrect).

    Source code in atomlib/atoms.py
    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n    \"\"\"\n    Return a periodic cell with the given orthogonal cell dimensions.\n\n    If cell_size is not specified, it will be assumed (and may be incorrect).\n    \"\"\"\n    # TODO: test this\n    from .atomcell import AtomCell\n\n    if cell_size is None:\n        warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n        cell_size = self.bbox().size\n        cell_origin = self.bbox().min\n\n    # TODO test this origin code\n    cell = Cell.from_unit_cell(cell_size)\n    if cell_origin is not None:\n        cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n    return AtomCell(self.get_atoms(), cell, frame='local')\n
    "},{"location":"api/#atomlib.AtomCell.coords","title":"coords","text":"
    coords(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> NDArray[float64]\n

    Return a (N, 3) ndarray of atom positions (dtype numpy.float64) in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> NDArray[numpy.float64]:\n    \"\"\"\n    Return a `(N, 3)` ndarray of atom positions (dtype [`numpy.float64`][numpy.float64])\n    in the given coordinate frame.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.x","title":"x","text":"
    x() -> Expr\n
    Source code in atomlib/atoms.py
    def x(self) -> polars.Expr:\n    return polars.col('coords').arr.get(0).alias('x')\n
    "},{"location":"api/#atomlib.AtomCell.y","title":"y","text":"
    y() -> Expr\n
    Source code in atomlib/atoms.py
    def y(self) -> polars.Expr:\n    return polars.col('coords').arr.get(1).alias('y')\n
    "},{"location":"api/#atomlib.AtomCell.z","title":"z","text":"
    z() -> Expr\n
    Source code in atomlib/atoms.py
    def z(self) -> polars.Expr:\n    return polars.col('coords').arr.get(2).alias('z')\n
    "},{"location":"api/#atomlib.AtomCell.velocities","title":"velocities","text":"
    velocities(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Optional[NDArray[float64]]\n

    Return a (N, 3) ndarray of atom velocities (dtype numpy.float64) in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef velocities(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> t.Optional[NDArray[numpy.float64]]:\n    \"\"\"\n    Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64])\n    in the given coordinate frame.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.types","title":"types","text":"
    types() -> Optional[Series]\n

    Returns a Series of atom types (dtype polars.Int32).

    Source code in atomlib/atoms.py
    def types(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('type')\n
    "},{"location":"api/#atomlib.AtomCell.masses","title":"masses","text":"
    masses() -> Optional[Series]\n

    Returns a Series of atom masses (dtype polars.Float32).

    Source code in atomlib/atoms.py
    def masses(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('mass')\n
    "},{"location":"api/#atomlib.AtomCell.add_atom","title":"add_atom","text":"
    add_atom(\n    elem: Union[int, str],\n    x: ArrayLike,\n    /,\n    *,\n    y: None = None,\n    z: None = None,\n    frame: Optional[CoordinateFrame] = None,\n    **kwargs: Any,\n) -> Self\n
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: float,\n    y: float,\n    z: float,\n    *,\n    frame: Optional[CoordinateFrame] = None,\n    **kwargs: Any,\n) -> Self\n
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: Union[ArrayLike, float],\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None,\n    **kwargs: Any,\n) -> Self\n

    Return a copy of self with an extra atom.

    By default, all extra columns present in self must be specified as **kwargs.

    Try to avoid calling this in a loop (Use concat instead).

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef add_atom(self, elem: t.Union[int, str], /,  # type: ignore (spurious)\n             x: t.Union[ArrayLike, float],\n             y: t.Optional[float] = None,\n             z: t.Optional[float] = None, *,\n             frame: t.Optional[CoordinateFrame] = None,\n             **kwargs: t.Any) -> Self:\n    \"\"\"\n    Return a copy of `self` with an extra atom.\n\n    By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n    Try to avoid calling this in a loop (Use [`concat`][atomlib.atomcell.HasAtomCell.concat] instead).\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.pos","title":"pos","text":"
    pos(\n    x: Sequence[Optional[float]],\n    /,\n    *,\n    y: None = None,\n    z: None = None,\n    tol: float = 1e-06,\n    **kwargs: Any,\n) -> Expr\n
    pos(\n    x: Optional[float] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n
    pos(\n    x: Union[Sequence[Optional[float]], float, None] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n

    Select all atoms at a given position.

    Formally, returns all atoms within a cube of radius tol centered at (x,y,z), exclusive of the cube's surface.

    Additional parameters given as kwargs will be checked as additional parameters (with strict equality).

    Source code in atomlib/atoms.py
    def pos(self,\n        x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n        y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n        tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n    \"\"\"\n    Select all atoms at a given position.\n\n    Formally, returns all atoms within a cube of radius ``tol``\n    centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n    Additional parameters given as ``kwargs`` will be checked\n    as additional parameters (with strict equality).\n    \"\"\"\n\n    if isinstance(x, t.Sequence):\n        (x, y, z) = x\n\n    tol = abs(float(tol))\n    selection = polars.lit(True)\n    if x is not None:\n        selection &= self.x().is_between(x - tol, x + tol, closed='none')\n    if y is not None:\n        selection &= self.y().is_between(y - tol, y + tol, closed='none')\n    if z is not None:\n        selection &= self.z().is_between(z - tol, z + tol, closed='none')\n    for (col, val) in kwargs.items():\n        selection &= (polars.col(col) == val)\n\n    return selection\n
    "},{"location":"api/#atomlib.AtomCell.with_index","title":"with_index","text":"
    with_index(\n    index: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Returns self with a row index added in column 'i' (dtype polars.Int64). If index is not specified, defaults to an existing index or a new index.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_index(self, index: t.Optional[AtomValues] = None, *,\n               frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n    If `index` is not specified, defaults to an existing index or a new index.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.with_wobble","title":"with_wobble","text":"
    with_wobble(\n    wobble: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given displacements in column 'wobble' (dtype polars.Float64). If wobble is not specified, defaults to the already-existing wobbles or 0.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_wobble(self, wobble: t.Optional[AtomValues] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.with_occupancy","title":"with_occupancy","text":"
    with_occupancy(\n    frac_occupancy: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given fractional occupancies (dtype polars.Float64). If frac_occupancy is not specified, defaults to the already-existing occupancies or 1.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None, *,\n                   frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.apply_wobble","title":"apply_wobble","text":"
    apply_wobble(\n    rng: Union[Generator, int, None] = None,\n    frame: Optional[CoordinateFrame] = None,\n) -> Self\n

    Displace the atoms in self by the amount in the wobble column. wobble is interpretated as a mean-squared displacement, which is distributed equally over each axis.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None,\n                 frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Displace the atoms in `self` by the amount in the `wobble` column.\n    `wobble` is interpretated as a mean-squared displacement, which is distributed\n    equally over each axis.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.apply_occupancy","title":"apply_occupancy","text":"
    apply_occupancy(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    For each atom in self, use its frac_occupancy to randomly decide whether to remove it.

    Source code in atomlib/atoms.py
    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n    \"\"\"\n    if 'frac_occupancy' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    frac = self.select('frac_occupancy').to_series().to_numpy()\n    choice = rng.binomial(1, frac).astype(numpy.bool_)\n    return self.filter(polars.lit(choice))\n
    "},{"location":"api/#atomlib.AtomCell.with_type","title":"with_type","text":"
    with_type(\n    types: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atom types in column 'type'. If types is not specified, use the already existing types or auto-assign them.

    When auto-assigning, each symbol is given a unique value, case-sensitive. Values are assigned from lowest atomic number to highest. For instance: [\"Ag+\", \"Na\", \"H\", \"Ag\"] => [3, 11, 1, 2]

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_type(self, types: t.Optional[AtomValues] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom types in column 'type'.\n    If `types` is not specified, use the already existing types or auto-assign them.\n\n    When auto-assigning, each symbol is given a unique value, case-sensitive.\n    Values are assigned from lowest atomic number to highest.\n    For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.with_mass","title":"with_mass","text":"
    with_mass(\n    mass: Optional[ArrayLike] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atom masses in column 'mass'. If mass is not specified, use the already existing masses or auto-assign them.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_mass(self, mass: t.Optional[ArrayLike] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom masses in column `'mass'`.\n    If `mass` is not specified, use the already existing masses or auto-assign them.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.with_symbol","title":"with_symbol","text":"
    with_symbol(\n    symbols: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atomic symbols.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atomic symbols.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.with_coords","title":"with_coords","text":"
    with_coords(\n    pts: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self replaced with the given atomic positions.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic positions.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.with_velocity","title":"with_velocity","text":"
    with_velocity(\n    pts: Optional[ArrayLike] = None,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self replaced with the given atomic velocities. If pts is not specified, use the already existing velocities or zero.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                  selection: t.Optional[AtomSelection] = None, *,\n                  frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic velocities.\n    If `pts` is not specified, use the already existing velocities or zero.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.AtomCell.get_atomcell","title":"get_atomcell","text":"
    get_atomcell() -> AtomCell\n
    Source code in atomlib/atomcell.py
    def get_atomcell(self) -> AtomCell:\n    frame = self.get_frame()\n    return AtomCell(self.get_atoms(frame), self.get_cell(), frame=frame, keep_frame=True)\n
    "},{"location":"api/#atomlib.AtomCell.to_frame","title":"to_frame","text":"
    to_frame(frame: CoordinateFrame) -> Self\n

    Convert the stored Atoms to the given coordinate frame.

    Source code in atomlib/atomcell.py
    def to_frame(self, frame: CoordinateFrame) -> Self:\n    \"\"\"Convert the stored Atoms to the given coordinate frame.\"\"\"\n    return self.with_atoms(self.get_atoms(frame), frame)\n
    "},{"location":"api/#atomlib.AtomCell.crop_to_box","title":"crop_to_box","text":"
    crop_to_box(eps: float = 1e-05) -> Self\n
    Source code in atomlib/atomcell.py
    def crop_to_box(self, eps: float = 1e-5) -> Self:\n    atoms = self._transform_atoms_in_frame('cell_box', lambda atoms: atoms.crop_atoms(*([-eps, 1-eps]*3)))\n    return self.with_atoms(atoms)\n
    "},{"location":"api/#atomlib.AtomCell.wrap","title":"wrap","text":"
    wrap(eps: float = 1e-05) -> Self\n

    Wrap atoms around the cell boundaries.

    Source code in atomlib/atomcell.py
    def wrap(self, eps: float = 1e-5) -> Self:\n    \"\"\"Wrap atoms around the cell boundaries.\"\"\"\n    return self.with_atoms(self._transform_atoms_in_frame('cell_box', lambda a: a._wrap(eps)))\n
    "},{"location":"api/#atomlib.AtomCell.repeat_to","title":"repeat_to","text":"
    repeat_to(\n    size: VecLike, crop: Union[bool, Sequence[bool]] = False\n) -> Self\n

    Repeat the cell so it is at least size along the crystal's axes.

    If crop, then crop the cell to exactly size. This may break periodicity. crop may be a vector, in which case you can specify cropping only along some axes.

    Source code in atomlib/atomcell.py
    def repeat_to(self, size: VecLike, crop: t.Union[bool, t.Sequence[bool]] = False) -> Self:\n    \"\"\"\n    Repeat the cell so it is at least `size` along the crystal's axes.\n\n    If `crop`, then crop the cell to exactly `size`. This may break periodicity.\n    `crop` may be a vector, in which case you can specify cropping only along some axes.\n    \"\"\"\n    size = to_vec3(size)\n    cell_size = self.cell_size * self.n_cells\n    repeat = numpy.maximum(numpy.ceil(size / cell_size).astype(int), 1)\n    atom_cell = self.repeat(repeat)\n\n    crop_v = to_vec3(crop, dtype=numpy.bool_)\n    if numpy.any(crop_v):\n        crop_x, crop_y, crop_z = crop_v\n        return atom_cell.crop(\n            x_max = size[0] if crop_x else numpy.inf,\n            y_max = size[1] if crop_y else numpy.inf,\n            z_max = size[2] if crop_z else numpy.inf,\n            frame='cell'\n        )\n\n    return atom_cell\n
    "},{"location":"api/#atomlib.AtomCell.repeat_x","title":"repeat_x","text":"
    repeat_x(n: int) -> Self\n

    Tile the cell in the x axis.

    Source code in atomlib/atomcell.py
    def repeat_x(self, n: int) -> Self:\n    \"\"\"Tile the cell in the x axis.\"\"\"\n    return self.repeat((n, 1, 1))\n
    "},{"location":"api/#atomlib.AtomCell.repeat_y","title":"repeat_y","text":"
    repeat_y(n: int) -> Self\n

    Tile the cell in the y axis.

    Source code in atomlib/atomcell.py
    def repeat_y(self, n: int) -> Self:\n    \"\"\"Tile the cell in the y axis.\"\"\"\n    return self.repeat((1, n, 1))\n
    "},{"location":"api/#atomlib.AtomCell.repeat_z","title":"repeat_z","text":"
    repeat_z(n: int) -> Self\n

    Tile the cell in the z axis.

    Source code in atomlib/atomcell.py
    def repeat_z(self, n: int) -> Self:\n    \"\"\"Tile the cell in the z axis.\"\"\"\n    return self.repeat((1, 1, n))\n
    "},{"location":"api/#atomlib.AtomCell.repeat_to_x","title":"repeat_to_x","text":"
    repeat_to_x(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the x axis.

    Source code in atomlib/atomcell.py
    def repeat_to_x(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the x axis.\"\"\"\n    return self.repeat_to([size, 0., 0.], [crop, False, False])\n
    "},{"location":"api/#atomlib.AtomCell.repeat_to_y","title":"repeat_to_y","text":"
    repeat_to_y(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the y axis.

    Source code in atomlib/atomcell.py
    def repeat_to_y(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the y axis.\"\"\"\n    return self.repeat_to([0., size, 0.], [False, crop, False])\n
    "},{"location":"api/#atomlib.AtomCell.repeat_to_z","title":"repeat_to_z","text":"
    repeat_to_z(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the z axis.

    Source code in atomlib/atomcell.py
    def repeat_to_z(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the z axis.\"\"\"\n    return self.repeat_to([0., 0., size], [False, False, crop])\n
    "},{"location":"api/#atomlib.AtomCell.repeat_to_aspect","title":"repeat_to_aspect","text":"
    repeat_to_aspect(\n    plane: Literal[\"xy\", \"xz\", \"yz\"] = \"xy\",\n    *,\n    aspect: float = 1.0,\n    min_size: Optional[VecLike] = None,\n    max_size: Optional[VecLike] = None\n) -> Self\n

    Repeat to optimize the aspect ratio in plane, while staying above min_size and under max_size.

    Source code in atomlib/atomcell.py
    def repeat_to_aspect(self, plane: t.Literal['xy', 'xz', 'yz'] = 'xy', *,\n                     aspect: float = 1., min_size: t.Optional[VecLike] = None,\n                     max_size: t.Optional[VecLike] = None) -> Self:\n    \"\"\"\n    Repeat to optimize the aspect ratio in `plane`,\n    while staying above `min_size` and under `max_size`.\n    \"\"\"\n    if min_size is None:\n        min_n = numpy.array([1, 1, 1], numpy.int_)\n    else:\n        min_n = numpy.maximum(numpy.ceil(to_vec3(min_size) / self.box_size), 1).astype(numpy.int_)\n\n    if max_size is None:\n        max_n = 3 * min_n\n    else:\n        max_n = numpy.maximum(numpy.floor(to_vec3(max_size) / self.box_size), 1).astype(numpy.int_)\n\n    if plane == 'xy':\n        indices = [0, 1]\n    elif plane == 'xz':\n        indices = [0, 2]\n    elif plane == 'yz':\n        indices = [1, 2]\n    else:\n        raise ValueError(f\"Invalid plane '{plane}'. Exepcted 'xy', 'xz', 'or 'yz'.\")\n\n    na = numpy.arange(min_n[indices[0]], max_n[indices[0]])\n    nb = numpy.arange(min_n[indices[1]], max_n[indices[1]])\n    (na, nb) = numpy.meshgrid(na, nb)\n\n    aspects = na * self.box_size[indices[0]] / (nb * self.box_size[indices[1]])\n    # cost function: log(aspect)^2  (so cost(0.5) == cost(2))\n    min_i = numpy.argmin(numpy.log(aspects / aspect)**2)\n    repeat = numpy.array([1, 1, 1], numpy.int_)\n    repeat[indices] = na.flatten()[min_i], nb.flatten()[min_i]\n    return self.repeat(repeat)\n
    "},{"location":"api/#atomlib.AtomCell.periodic_duplicate","title":"periodic_duplicate","text":"
    periodic_duplicate(eps: float = 1e-05) -> Self\n

    Add duplicate copies of atoms near periodic boundaries.

    For instance, an atom at a corner will be duplicated into 8 copies. This is mostly only useful for visualization.

    Source code in atomlib/atomcell.py
    def periodic_duplicate(self, eps: float = 1e-5) -> Self:\n    \"\"\"\n    Add duplicate copies of atoms near periodic boundaries.\n\n    For instance, an atom at a corner will be duplicated into 8 copies.\n    This is mostly only useful for visualization.\n    \"\"\"\n    frame_save = self.get_frame()\n    self = self.to_frame('cell_box').wrap(eps=eps)\n\n    for i in range(3):\n        self = self.concat((self,\n            self.filter(polars.col('coords').arr.get(i).abs() <= eps, frame='cell_box')\n                .transform_atoms(AffineTransform3D.translate([1. if i == j else 0. for j in range(3)]), frame='cell_box')\n        ))\n\n    return self.to_frame(frame_save)\n
    "},{"location":"api/#atomlib.AtomCell.read","title":"read classmethod","text":"
    read(path: FileOrPath, ty: FileType) -> HasAtomsT\n
    read(\n    path: Union[str, Path, TextIO], ty: Literal[None] = None\n) -> HasAtomsT\n
    read(\n    path: FileOrPath, ty: Optional[FileType] = None\n) -> HasAtomsT\n

    Read a structure from a file.

    Supported types can be found in the io module. If no ty is specified, it is inferred from the file's extension.

    Source code in atomlib/mixins.py
    @classmethod\ndef read(cls: t.Type[HasAtomsT], path: FileOrPath, ty: t.Optional[FileType] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a file.\n\n    Supported types can be found in the [io][atomlib.io] module.\n    If no `ty` is specified, it is inferred from the file's extension.\n    \"\"\"\n    from .io import read\n    return _cast_atoms(read(path, ty), cls)  # type: ignore\n
    "},{"location":"api/#atomlib.AtomCell.read_cif","title":"read_cif classmethod","text":"
    read_cif(\n    f: Union[FileOrPath, CIF, CIFDataBlock],\n    block: Union[int, str, None] = None,\n) -> HasAtomsT\n

    Read a structure from a CIF file.

    If block is specified, read data from the given block of the CIF file (index or name).

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cif(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CIF, CIFDataBlock], block: t.Union[int, str, None] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a CIF file.\n\n    If `block` is specified, read data from the given block of the CIF file (index or name).\n    \"\"\"\n    from .io import read_cif\n    return _cast_atoms(read_cif(f, block), cls)\n
    "},{"location":"api/#atomlib.AtomCell.read_xyz","title":"read_xyz classmethod","text":"
    read_xyz(f: Union[FileOrPath, XYZ]) -> HasAtomsT\n

    Read a structure from an XYZ file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xyz(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XYZ]) -> HasAtomsT:\n    \"\"\"Read a structure from an XYZ file.\"\"\"\n    from .io import read_xyz\n    return _cast_atoms(read_xyz(f), cls)\n
    "},{"location":"api/#atomlib.AtomCell.read_xsf","title":"read_xsf classmethod","text":"
    read_xsf(f: Union[FileOrPath, XSF]) -> HasAtomsT\n

    Read a structure from an XSF file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xsf(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XSF]) -> HasAtomsT:\n    \"\"\"Read a structure from an XSF file.\"\"\"\n    from .io import read_xsf\n    return _cast_atoms(read_xsf(f), cls)\n
    "},{"location":"api/#atomlib.AtomCell.read_cfg","title":"read_cfg classmethod","text":"
    read_cfg(f: Union[FileOrPath, CFG]) -> HasAtomsT\n

    Read a structure from a CFG file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cfg(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CFG]) -> HasAtomsT:\n    \"\"\"Read a structure from a CFG file.\"\"\"\n    from .io import read_cfg\n    return _cast_atoms(read_cfg(f), cls)\n
    "},{"location":"api/#atomlib.AtomCell.read_lmp","title":"read_lmp classmethod","text":"
    read_lmp(\n    f: Union[FileOrPath, LMP],\n    type_map: Optional[Dict[int, Union[str, int]]] = None,\n) -> HasAtomsT\n

    Read a structure from a LAAMPS data file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_lmp(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, LMP], type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> HasAtomsT:\n    \"\"\"Read a structure from a LAAMPS data file.\"\"\"\n    from .io import read_lmp\n    return _cast_atoms(read_lmp(f, type_map=type_map), cls)\n
    "},{"location":"api/#atomlib.AtomCell.write_cif","title":"write_cif","text":"
    write_cif(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cif(self, f: FileOrPath):\n    from .io import write_cif\n    write_cif(self, f)\n
    "},{"location":"api/#atomlib.AtomCell.write_xyz","title":"write_xyz","text":"
    write_xyz(f: FileOrPath, fmt: XYZFormat = 'exyz')\n
    Source code in atomlib/mixins.py
    def write_xyz(self, f: FileOrPath, fmt: XYZFormat = 'exyz'):\n    from .io import write_xyz\n    write_xyz(self, f, fmt)\n
    "},{"location":"api/#atomlib.AtomCell.write_xsf","title":"write_xsf","text":"
    write_xsf(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_xsf(self, f: FileOrPath):\n    from .io import write_xsf\n    write_xsf(self, f)\n
    "},{"location":"api/#atomlib.AtomCell.write_cfg","title":"write_cfg","text":"
    write_cfg(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cfg(self, f: FileOrPath):\n    from .io import write_cfg\n    write_cfg(self, f)\n
    "},{"location":"api/#atomlib.AtomCell.write_lmp","title":"write_lmp","text":"
    write_lmp(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_lmp(self, f: FileOrPath):\n    from .io import write_lmp\n    write_lmp(self, f)\n
    "},{"location":"api/#atomlib.AtomCell.write","title":"write","text":"
    write(path: FileOrPath, ty: FileType)\n
    write(\n    path: Union[str, Path, TextIO], ty: Literal[None] = None\n)\n
    write(path: FileOrPath, ty: Optional[FileType] = None)\n

    Write this structure to a file.

    A file type may be specified using ty. If no ty is specified, it is inferred from the path's extension.

    Source code in atomlib/mixins.py
    def write(self, path: FileOrPath, ty: t.Optional[FileType] = None):\n    \"\"\"\n    Write this structure to a file.\n\n    A file type may be specified using `ty`.\n    If no `ty` is specified, it is inferred from the path's extension.\n    \"\"\"\n    from .io import write\n    write(self, path, ty)  # type: ignore\n
    "},{"location":"api/#atomlib.AtomCell.write_mslice","title":"write_mslice","text":"
    write_mslice(\n    f: BinaryFileOrPath,\n    template: Optional[MSliceFile] = None,\n    *,\n    slice_thickness: Optional[float] = None,\n    scan_points: Optional[ArrayLike] = None,\n    scan_extent: Optional[ArrayLike] = None,\n    noise_sigma: Optional[float] = None,\n    conv_angle: Optional[float] = None,\n    energy: Optional[float] = None,\n    defocus: Optional[float] = None,\n    tilt: Optional[Tuple[float, float]] = None,\n    tds: Optional[bool] = None,\n    n_cells: Optional[ArrayLike] = None\n)\n

    Write a structure to an mslice file.

    template may be a file, path, or ElementTree containing an existing mslice file. Its structure will be modified to make the final output. If not specified, a default template will be used.

    Additional options modify simulation properties. If an option is not specified, the template's properties are used.

    Source code in atomlib/mixins.py
    def write_mslice(self, f: BinaryFileOrPath, template: t.Optional[MSliceFile] = None, *,\n             slice_thickness: t.Optional[float] = None,  # angstrom\n             scan_points: t.Optional[ArrayLike] = None,\n             scan_extent: t.Optional[ArrayLike] = None,\n             noise_sigma: t.Optional[float] = None,  # angstrom\n             conv_angle: t.Optional[float] = None,  # mrad\n             energy: t.Optional[float] = None,  # keV\n             defocus: t.Optional[float] = None,  # angstrom\n             tilt: t.Optional[t.Tuple[float, float]] = None,  # (mrad, mrad)\n             tds: t.Optional[bool] = None,\n             n_cells: t.Optional[ArrayLike] = None):\n    \"\"\"\n    Write a structure to an mslice file.\n\n    `template` may be a file, path, or `ElementTree` containing an existing mslice file.\n    Its structure will be modified to make the final output. If not specified, a default\n    template will be used.\n\n    Additional options modify simulation properties. If an option is not specified, the\n    template's properties are used.\n    \"\"\"\n    from .io import write_mslice\n    return write_mslice(self, f, template, slice_thickness=slice_thickness,\n                        scan_points=scan_points, scan_extent=scan_extent,\n                        conv_angle=conv_angle, energy=energy, defocus=defocus,\n                        noise_sigma=noise_sigma, tilt=tilt, tds=tds, n_cells=n_cells)\n
    "},{"location":"api/#atomlib.AtomCell.write_qe","title":"write_qe","text":"
    write_qe(\n    f: FileOrPath,\n    pseudo: Optional[Mapping[str, str]] = None,\n)\n

    Write a structure to a Quantum Espresso pw.x file.

    PARAMETER DESCRIPTION f

    File or path to write to

    TYPE: FileOrPath

    pseudo

    Mapping from atom symbol

    TYPE: Optional[Mapping[str, str]] DEFAULT: None

    Source code in atomlib/mixins.py
    def write_qe(self, f: FileOrPath, pseudo: t.Optional[t.Mapping[str, str]] = None):\n    \"\"\"\n    Write a structure to a Quantum Espresso pw.x file.\n\n    Args:\n      f: File or path to write to\n      pseudo: Mapping from atom symbol\n    \"\"\"\n    from .io import write_qe\n    write_qe(self, f, pseudo)\n
    "},{"location":"api/#atomlib.AtomCell.get_cell","title":"get_cell","text":"
    get_cell() -> Cell\n
    Source code in atomlib/atomcell.py
    def get_cell(self) -> Cell:\n    return self.cell\n
    "},{"location":"api/#atomlib.AtomCell.with_cell","title":"with_cell","text":"
    with_cell(cell: Cell) -> Self\n
    Source code in atomlib/atomcell.py
    def with_cell(self, cell: Cell) -> Self:\n    return self.__class__(self.atoms, cell, frame=self.frame, keep_frame=True)\n
    "},{"location":"api/#atomlib.AtomCell.get_atoms","title":"get_atoms","text":"
    get_atoms(frame: Optional[CoordinateFrame] = None) -> Atoms\n

    Get atoms contained in self, in the given coordinate frame.

    Source code in atomlib/atomcell.py
    def get_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> Atoms:\n    \"\"\"Get atoms contained in ``self``, in the given coordinate frame.\"\"\"\n\n    if frame is None or frame == self.get_frame():\n        return self.atoms\n    return self.atoms.transform(self.get_transform(frame, self.get_frame()))\n
    "},{"location":"api/#atomlib.AtomCell.with_atoms","title":"with_atoms","text":"
    with_atoms(\n    atoms: HasAtoms, frame: Optional[CoordinateFrame] = None\n) -> Self\n
    Source code in atomlib/atomcell.py
    def with_atoms(self, atoms: HasAtoms, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    frame = frame if frame is not None else self.frame\n    return self.__class__(atoms.get_atoms(), cell=self.cell, frame=frame, keep_frame=True)\n
    "},{"location":"api/#atomlib.AtomCell.get_frame","title":"get_frame","text":"
    get_frame() -> CoordinateFrame\n

    Get the coordinate frame atoms are stored in.

    Source code in atomlib/atomcell.py
    def get_frame(self) -> CoordinateFrame:\n    \"\"\"Get the coordinate frame atoms are stored in.\"\"\"\n    return self.frame\n
    "},{"location":"api/#atomlib.AtomCell.from_ortho","title":"from_ortho classmethod","text":"
    from_ortho(\n    atoms: IntoAtoms,\n    ortho: LinearTransform3D,\n    *,\n    n_cells: Optional[VecLike] = None,\n    frame: CoordinateFrame = \"local\",\n    keep_frame: bool = False\n)\n

    Make an atom cell given a list of atoms and an orthogonalization matrix. Atoms are assumed to be in the coordinate system frame.

    Source code in atomlib/atomcell.py
    @classmethod\ndef from_ortho(cls, atoms: IntoAtoms, ortho: LinearTransform3D, *,\n               n_cells: t.Optional[VecLike] = None,\n               frame: CoordinateFrame = 'local',\n               keep_frame: bool = False):\n    \"\"\"\n    Make an atom cell given a list of atoms and an orthogonalization matrix.\n    Atoms are assumed to be in the coordinate system `frame`.\n    \"\"\"\n    cell = Cell.from_ortho(ortho, n_cells)\n    return cls(atoms, cell, frame=frame, keep_frame=keep_frame)\n
    "},{"location":"api/#atomlib.AtomCell.from_unit_cell","title":"from_unit_cell classmethod","text":"
    from_unit_cell(\n    atoms: IntoAtoms,\n    cell_size: VecLike,\n    cell_angle: Optional[VecLike] = None,\n    *,\n    n_cells: Optional[VecLike] = None,\n    frame: CoordinateFrame = \"local\",\n    keep_frame: bool = False\n)\n

    Make a cell given a list of atoms and unit cell parameters. Atoms are assumed to be in the coordinate system frame.

    Source code in atomlib/atomcell.py
    @classmethod\ndef from_unit_cell(cls, atoms: IntoAtoms, cell_size: VecLike,\n                   cell_angle: t.Optional[VecLike] = None, *,\n                   n_cells: t.Optional[VecLike] = None,\n                   frame: CoordinateFrame = 'local',\n                   keep_frame: bool = False):\n    \"\"\"\n    Make a cell given a list of atoms and unit cell parameters.\n    Atoms are assumed to be in the coordinate system `frame`.\n    \"\"\"\n    cell = Cell.from_unit_cell(cell_size, cell_angle, n_cells=n_cells)\n    return cls(atoms, cell, frame=frame, keep_frame=keep_frame)\n
    "},{"location":"api/#atomlib.AtomCell.orthogonalize","title":"orthogonalize","text":"
    orthogonalize() -> OrthoCell\n
    Source code in atomlib/atomcell.py
    def orthogonalize(self) -> OrthoCell:\n    if self.is_orthogonal():\n        return OrthoCell(self.atoms, self.cell, frame=self.frame)\n    raise NotImplementedError()\n
    "},{"location":"api/#atomlib.AtomCell.clone","title":"clone","text":"
    clone() -> AtomCellT\n

    Make a deep copy of self.

    Source code in atomlib/atomcell.py
    def clone(self: AtomCellT) -> AtomCellT:\n    \"\"\"Make a deep copy of `self`.\"\"\"\n    return self.__class__(**{field.name: copy.deepcopy(getattr(self, field.name)) for field in fields(self)})\n
    "},{"location":"api/#atomlib.AtomCell.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n

    Assert this structure is equal to other

    Source code in atomlib/atomcell.py
    def assert_equal(self, other: t.Any):\n    \"\"\"Assert this structure is equal to `other`\"\"\"\n    assert isinstance(other, AtomCell)\n    self.cell.assert_equal(other.cell)\n    self.get_atoms('local').assert_equal(other.get_atoms('local'))\n
    "},{"location":"api/#atomlib.HasAtomCell","title":"HasAtomCell","text":"

    Bases: HasAtoms, HasCell, ABC

    Source code in atomlib/atomcell.py
    class HasAtomCell(HasAtoms, HasCell, abc.ABC):\n    @abc.abstractmethod\n    def get_frame(self) -> CoordinateFrame:\n        \"\"\"Get the coordinate frame atoms are stored in.\"\"\"\n        ...\n\n    @abc.abstractmethod\n    def with_atoms(self, atoms: HasAtoms, frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Replace the atoms in `self`. If no coordinate frame is specified, keep the coordinate frame unchanged.\n        \"\"\"\n        ...\n\n    def with_cell(self, cell: Cell) -> Self:\n        \"\"\"\n        Replace the cell in `self`, without touching the atomic coordinates.\n        \"\"\"\n        return self.to_frame('local').with_cell(cell)\n\n    def get_atomcell(self) -> AtomCell:\n        frame = self.get_frame()\n        return AtomCell(self.get_atoms(frame), self.get_cell(), frame=frame, keep_frame=True)\n\n    @abc.abstractmethod\n    def get_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> Atoms:\n        \"\"\"Get atoms contained in `self`, in the given coordinate frame.\"\"\"\n        ...\n\n    def bbox_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> BBox3D:\n        \"\"\"Return the bounding box of all the atoms in `self`, in the given coordinate frame.\"\"\"\n        return self.get_atoms(frame).bbox()\n\n    def bbox(self, frame: CoordinateFrame = 'local') -> BBox3D:\n        \"\"\"\n        Return the combined bounding box of the cell and atoms in the given coordinate system.\n        To get the cell or atoms bounding box only, use [`bbox_cell`][atomlib.atomcell.HasAtomCell.bbox_cell] or [`bbox_atoms`][atomlib.atomcell.HasAtomCell.bbox_atoms].\n        \"\"\"\n        return self.bbox_atoms(frame) | self.bbox_cell(frame)\n\n    # transformation\n\n    def _transform_atoms_in_frame(self, frame: t.Optional[CoordinateFrame], f: t.Callable[[Atoms], Atoms]) -> Atoms:\n        # ugly code\n        if frame is None or frame == self.get_frame():\n            return f(self.get_atoms())\n        return f(self.get_atoms(frame)).transform(self.get_transform(self.get_frame(), frame))\n\n    def to_frame(self, frame: CoordinateFrame) -> Self:\n        \"\"\"Convert the stored Atoms to the given coordinate frame.\"\"\"\n        return self.with_atoms(self.get_atoms(frame), frame)\n\n    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                        frame: CoordinateFrame = 'local', transform_velocities: bool = False) -> Self:\n        \"\"\"\n        Transform the atoms in `self` by `transform`.\n        If `selection` is given, only transform the atoms in `selection`.\n        \"\"\"\n        transform = self.change_transform(Transform3D.make(transform), self.get_frame(), frame)\n        return self.with_atoms(self.get_atoms(self.get_frame()).transform(transform, selection, transform_velocities=transform_velocities))\n\n    def transform_cell(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n        \"\"\"\n        Apply the given transform to the unit cell, without changing atom positions.\n        The transform is applied in coordinate frame 'frame'.\n        \"\"\"\n        return self.with_cell(self.get_cell().transform_cell(transform, frame=frame))\n\n    def transform(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n        if isinstance(transform, Transform3D) and not isinstance(transform, AffineTransform3D):\n            raise ValueError(\"Non-affine transforms cannot change the box dimensions. Use 'transform_atoms' instead.\")\n        # TODO: cleanup once tests pass\n        # coordinate change the transform into atomic coordinates\n        new_cell = self.get_cell().transform_cell(transform, frame)\n        transform = self.get_cell().change_transform(transform, self.get_frame(), frame)\n        return self.with_atoms(self.get_atoms().transform(transform), self.get_frame()).with_cell(new_cell)\n\n    # crop methods\n\n    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n             y_min: float = -numpy.inf, y_max: float = numpy.inf,\n             z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n             frame: CoordinateFrame = 'local') -> Self:\n        \"\"\"\n        Crop atoms and cell to the given extents. For a non-orthogonal\n        cell, this must be specified in cell coordinates. This\n        function implicity `explode`s the cell as well.\n\n        To crop atoms only, use `crop_atoms` instead.\n        \"\"\"\n\n        cell = self.get_cell().crop(x_min, x_max, y_min, y_max, z_min, z_max, frame=frame)\n        atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n        return self.with_cell(cell).with_atoms(atoms)\n\n    def crop_atoms(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n                   y_min: float = -numpy.inf, y_max: float = numpy.inf,\n                   z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n                   frame: CoordinateFrame = 'local') -> Self:\n        atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n        return self.with_atoms(atoms)\n\n    def crop_to_box(self, eps: float = 1e-5) -> Self:\n        atoms = self._transform_atoms_in_frame('cell_box', lambda atoms: atoms.crop_atoms(*([-eps, 1-eps]*3)))\n        return self.with_atoms(atoms)\n\n    def wrap(self, eps: float = 1e-5) -> Self:\n        \"\"\"Wrap atoms around the cell boundaries.\"\"\"\n        return self.with_atoms(self._transform_atoms_in_frame('cell_box', lambda a: a._wrap(eps)))\n\n    def _repeat_to_contain(self, pts: numpy.ndarray, pad: int = 0, frame: CoordinateFrame = 'cell_frac') -> Self:\n        #print(f\"pts: {pts} in frame {frame}\")\n        pts = self.get_transform('cell_frac', frame) @ pts\n\n        bbox = BBox3D.unit() | BBox3D.from_pts(pts)\n        min_bounds = numpy.floor(bbox.min).astype(int) - pad\n        max_bounds = numpy.ceil(bbox.max).astype(int) + pad\n        #print(f\"tiling to {min_bounds}, {max_bounds}\")\n        repeat = max_bounds - min_bounds\n        cells = numpy.stack(numpy.meshgrid(*map(numpy.arange, repeat))).reshape(3, -1).T.astype(float)\n\n        atoms = self.get_atoms('cell_frac')\n        atoms = Atoms.concat([\n            atoms.transform(AffineTransform3D.translate(cell))\n            for cell in cells\n        ])\n        #print(f\"atoms:\\n{atoms}\")\n        cell = self.get_cell().repeat(repeat) \\\n            .transform_cell(AffineTransform3D.translate(min_bounds), 'cell_frac')\n        return self.with_cell(cell).with_atoms(atoms, 'cell_frac')\n\n    def repeat(self, n: t.Union[int, VecLike]) -> Self:\n        \"\"\"Tile the cell\"\"\"\n        ns = numpy.broadcast_to(n, 3)\n        if not numpy.issubdtype(ns.dtype, numpy.integer):\n            raise ValueError(\"repeat() argument must be an integer or integer array.\")\n\n        cells = numpy.stack(numpy.meshgrid(*map(numpy.arange, ns))) \\\n            .reshape(3, -1).T.astype(float)\n        cells = cells * self.box_size\n\n        atoms = self.get_atoms('cell')\n        atoms = Atoms.concat([\n            atoms.transform(AffineTransform3D.translate(cell))\n            for cell in cells\n        ]) #.transform(self.cell.get_transform('local', 'cell_frac'))\n        return self.with_atoms(atoms, 'cell').with_cell(self.get_cell().repeat(ns))\n\n    def repeat_to(self, size: VecLike, crop: t.Union[bool, t.Sequence[bool]] = False) -> Self:\n        \"\"\"\n        Repeat the cell so it is at least `size` along the crystal's axes.\n\n        If `crop`, then crop the cell to exactly `size`. This may break periodicity.\n        `crop` may be a vector, in which case you can specify cropping only along some axes.\n        \"\"\"\n        size = to_vec3(size)\n        cell_size = self.cell_size * self.n_cells\n        repeat = numpy.maximum(numpy.ceil(size / cell_size).astype(int), 1)\n        atom_cell = self.repeat(repeat)\n\n        crop_v = to_vec3(crop, dtype=numpy.bool_)\n        if numpy.any(crop_v):\n            crop_x, crop_y, crop_z = crop_v\n            return atom_cell.crop(\n                x_max = size[0] if crop_x else numpy.inf,\n                y_max = size[1] if crop_y else numpy.inf,\n                z_max = size[2] if crop_z else numpy.inf,\n                frame='cell'\n            )\n\n        return atom_cell\n\n    def repeat_x(self, n: int) -> Self:\n        \"\"\"Tile the cell in the x axis.\"\"\"\n        return self.repeat((n, 1, 1))\n\n    def repeat_y(self, n: int) -> Self:\n        \"\"\"Tile the cell in the y axis.\"\"\"\n        return self.repeat((1, n, 1))\n\n    def repeat_z(self, n: int) -> Self:\n        \"\"\"Tile the cell in the z axis.\"\"\"\n        return self.repeat((1, 1, n))\n\n    def repeat_to_x(self, size: float, crop: bool = False) -> Self:\n        \"\"\"Repeat the cell so it is at least size `size` along the x axis.\"\"\"\n        return self.repeat_to([size, 0., 0.], [crop, False, False])\n\n    def repeat_to_y(self, size: float, crop: bool = False) -> Self:\n        \"\"\"Repeat the cell so it is at least size `size` along the y axis.\"\"\"\n        return self.repeat_to([0., size, 0.], [False, crop, False])\n\n    def repeat_to_z(self, size: float, crop: bool = False) -> Self:\n        \"\"\"Repeat the cell so it is at least size `size` along the z axis.\"\"\"\n        return self.repeat_to([0., 0., size], [False, False, crop])\n\n    def repeat_to_aspect(self, plane: t.Literal['xy', 'xz', 'yz'] = 'xy', *,\n                         aspect: float = 1., min_size: t.Optional[VecLike] = None,\n                         max_size: t.Optional[VecLike] = None) -> Self:\n        \"\"\"\n        Repeat to optimize the aspect ratio in `plane`,\n        while staying above `min_size` and under `max_size`.\n        \"\"\"\n        if min_size is None:\n            min_n = numpy.array([1, 1, 1], numpy.int_)\n        else:\n            min_n = numpy.maximum(numpy.ceil(to_vec3(min_size) / self.box_size), 1).astype(numpy.int_)\n\n        if max_size is None:\n            max_n = 3 * min_n\n        else:\n            max_n = numpy.maximum(numpy.floor(to_vec3(max_size) / self.box_size), 1).astype(numpy.int_)\n\n        if plane == 'xy':\n            indices = [0, 1]\n        elif plane == 'xz':\n            indices = [0, 2]\n        elif plane == 'yz':\n            indices = [1, 2]\n        else:\n            raise ValueError(f\"Invalid plane '{plane}'. Exepcted 'xy', 'xz', 'or 'yz'.\")\n\n        na = numpy.arange(min_n[indices[0]], max_n[indices[0]])\n        nb = numpy.arange(min_n[indices[1]], max_n[indices[1]])\n        (na, nb) = numpy.meshgrid(na, nb)\n\n        aspects = na * self.box_size[indices[0]] / (nb * self.box_size[indices[1]])\n        # cost function: log(aspect)^2  (so cost(0.5) == cost(2))\n        min_i = numpy.argmin(numpy.log(aspects / aspect)**2)\n        repeat = numpy.array([1, 1, 1], numpy.int_)\n        repeat[indices] = na.flatten()[min_i], nb.flatten()[min_i]\n        return self.repeat(repeat)\n\n    def explode(self) -> Self:\n        \"\"\"Materialize repeated cells as one supercell.\"\"\"\n        frame = self.get_frame()\n\n        return self.with_atoms(self.get_atoms('local'), 'local') \\\n            .with_cell(self.get_cell().explode()) \\\n            .to_frame(frame)\n\n    def periodic_duplicate(self, eps: float = 1e-5) -> Self:\n        \"\"\"\n        Add duplicate copies of atoms near periodic boundaries.\n\n        For instance, an atom at a corner will be duplicated into 8 copies.\n        This is mostly only useful for visualization.\n        \"\"\"\n        frame_save = self.get_frame()\n        self = self.to_frame('cell_box').wrap(eps=eps)\n\n        for i in range(3):\n            self = self.concat((self,\n                self.filter(polars.col('coords').arr.get(i).abs() <= eps, frame='cell_box')\n                    .transform_atoms(AffineTransform3D.translate([1. if i == j else 0. for j in range(3)]), frame='cell_box')\n            ))\n\n        return self.to_frame(frame_save)\n\n    # add frame to some HasAtoms methods\n\n    @_fwd_atoms_get\n    def describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n                 interpolation: RollingInterpolationMethod = 'nearest',\n                 frame: t.Optional[CoordinateFrame] = None) -> polars.DataFrame:\n        \"\"\"\n        Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n        Args:\n          percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                       50% (median), and 75% (third quartile).\n\n        Returns:\n          A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_columns(self,\n                     *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                     frame: t.Optional[CoordinateFrame] = None,\n                     **named_exprs: IntoExpr) -> Self:\n        \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n        ...\n\n    with_column = with_columns\n\n    @_fwd_atoms_get\n    def get_column(self, name: str, *, frame: t.Optional[CoordinateFrame] = None) -> polars.Series:\n        \"\"\"\n        Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        ...\n\n    @_fwd_atoms_get\n    def get_columns(self, *, frame: t.Optional[CoordinateFrame] = None) -> t.List[polars.Series]:\n        \"\"\"\n        Return all columns from `self` as a list of [`Series`][polars.Series].\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        ...\n\n    @_fwd_atoms_get\n    def group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 maintain_order: bool = False, frame: t.Optional[CoordinateFrame] = None,\n                 **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n        \"\"\"\n        Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n        \"\"\"\n        ...\n\n    def pipe(self: HasAtomCellT, function: t.Callable[Concatenate[HasAtomCellT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n        \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n        return function(self, *args, **kwargs)\n\n    @_fwd_atoms_transform\n    def filter(\n        self,\n        *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n        frame: t.Optional[CoordinateFrame] = None,\n        **constraints: t.Any,\n    ) -> Self:\n        \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def sort(\n        self,\n        by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        *more_by: IntoExpr,\n        descending: t.Union[bool, t.Sequence[bool]] = False,\n        nulls_last: bool = False,\n    ) -> Self:\n        \"\"\"\n        Sort the atoms in `self` by the given columns/expressions.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def slice(self, offset: int, length: t.Optional[int] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"Return a slice of the rows in `self`.\"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def head(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"Return the first `n` rows of `self`.\"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def tail(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"Return the last `n` rows of `self`.\"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def fill_null(\n        self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n        limit: t.Optional[int] = None, matches_supertype: bool = True,\n    ) -> Self:\n        ...\n\n    @_fwd_atoms_transform\n    def fill_nan(self, value: t.Union[polars.Expr, int, float, None], *,\n                 frame: t.Optional[CoordinateFrame] = None) -> Self:\n        ...\n\n    # TODO: partition_by\n\n    @_fwd_atoms_get\n    def select(\n        self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        frame: t.Optional[CoordinateFrame] = None,\n        **named_exprs: IntoExpr\n    ) -> polars.DataFrame:\n        \"\"\"\n        Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n        Expressions may either be columns or expressions of columns.\n\n        [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def select_props(\n        self,\n        *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        frame: t.Optional[CoordinateFrame] = None,\n        **named_exprs: IntoExpr\n    ) -> Self:\n        \"\"\"\n        Select `exprs` from `self`, while keeping required columns.\n        Doesn't affect the cell.\n\n        Returns:\n          A [`HasAtomCell`][atomlib.atomcell.HasAtomCell] filtered to contain\n          the specified properties (as well as required columns).\n        \"\"\"\n        ...\n\n    @_fwd_atoms_get\n    def try_select(\n        self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        frame: t.Optional[CoordinateFrame] = None,\n        **named_exprs: IntoExpr\n    ) -> t.Optional[polars.DataFrame]:\n        \"\"\"\n        Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n        Expressions may either be columns or expressions of columns. Returns `None` if any\n        columns are missing.\n\n        [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def round_near_zero(self, tol: float = 1e-14, *,\n                        frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Round atom position values near zero to zero.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_get\n    def coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> NDArray[numpy.float64]:\n        \"\"\"\n        Return a `(N, 3)` ndarray of atom positions (dtype [`numpy.float64`][numpy.float64])\n        in the given coordinate frame.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_get\n    def velocities(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> t.Optional[NDArray[numpy.float64]]:\n        \"\"\"\n        Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64])\n        in the given coordinate frame.\n        \"\"\"\n        ...\n\n    @t.overload\n    def add_atom(self, elem: t.Union[int, str], x: ArrayLike, /, *,\n                 y: None = None, z: None = None, frame: t.Optional[CoordinateFrame] = None,\n                 **kwargs: t.Any) -> Self:\n        ...\n\n    @t.overload\n    def add_atom(self, elem: t.Union[int, str], /,\n                 x: float, y: float, z: float, *,\n                 frame: t.Optional[CoordinateFrame] = None,\n                 **kwargs: t.Any) -> Self:\n        ...\n\n    @_fwd_atoms_transform\n    def add_atom(self, elem: t.Union[int, str], /,  # type: ignore (spurious)\n                 x: t.Union[ArrayLike, float],\n                 y: t.Optional[float] = None,\n                 z: t.Optional[float] = None, *,\n                 frame: t.Optional[CoordinateFrame] = None,\n                 **kwargs: t.Any) -> Self:\n        \"\"\"\n        Return a copy of `self` with an extra atom.\n\n        By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n        Try to avoid calling this in a loop (Use [`concat`][atomlib.atomcell.HasAtomCell.concat] instead).\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_index(self, index: t.Optional[AtomValues] = None, *,\n                   frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n        If `index` is not specified, defaults to an existing index or a new index.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_wobble(self, wobble: t.Optional[AtomValues] = None, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n        If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None, *,\n                       frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n        If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None,\n                     frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Displace the atoms in `self` by the amount in the `wobble` column.\n        `wobble` is interpretated as a mean-squared displacement, which is distributed\n        equally over each axis.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_type(self, types: t.Optional[AtomValues] = None, *,\n                  frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atom types in column 'type'.\n        If `types` is not specified, use the already existing types or auto-assign them.\n\n        When auto-assigning, each symbol is given a unique value, case-sensitive.\n        Values are assigned from lowest atomic number to highest.\n        For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_mass(self, mass: t.Optional[ArrayLike] = None, *,\n                  frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atom masses in column `'mass'`.\n        If `mass` is not specified, use the already existing masses or auto-assign them.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atomic symbols.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` replaced with the given atomic positions.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                      selection: t.Optional[AtomSelection] = None, *,\n                      frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` replaced with the given atomic velocities.\n        If `pts` is not specified, use the already existing velocities or zero.\n        \"\"\"\n        ...\n
    "},{"location":"api/#atomlib.HasAtomCell.affine","title":"affine property","text":"
    affine: AffineTransform3D\n

    Affine transformation. Holds transformation from 'ortho' to 'local' coordinates, including rotation away from the standard crystal orientation.

    "},{"location":"api/#atomlib.HasAtomCell.ortho","title":"ortho property","text":"
    ortho: LinearTransform3D\n

    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.

    "},{"location":"api/#atomlib.HasAtomCell.metric","title":"metric property","text":"
    metric: LinearTransform3D\n

    Cell metric tensor

    Returns the dot product between every combination of basis vectors. :math:\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j

    "},{"location":"api/#atomlib.HasAtomCell.cell_size","title":"cell_size property","text":"
    cell_size: NDArray[float64]\n

    Unit cell size.

    "},{"location":"api/#atomlib.HasAtomCell.cell_angle","title":"cell_angle property","text":"
    cell_angle: NDArray[float64]\n

    Unit cell angles, in radians.

    "},{"location":"api/#atomlib.HasAtomCell.n_cells","title":"n_cells property","text":"
    n_cells: NDArray[int_]\n

    Number of unit cells.

    "},{"location":"api/#atomlib.HasAtomCell.pbc","title":"pbc property","text":"
    pbc: NDArray[bool_]\n

    Flags indicating the presence of periodic boundary conditions along each axis.

    "},{"location":"api/#atomlib.HasAtomCell.ortho_size","title":"ortho_size property","text":"
    ortho_size: NDArray[float64]\n

    Return size of orthogonal unit cell.

    Equivalent to the diagonal of the orthogonalization matrix.

    "},{"location":"api/#atomlib.HasAtomCell.box_size","title":"box_size property","text":"
    box_size: NDArray[float64]\n

    Return size of the cell box.

    Equivalent to self.n_cells * self.cell_size.

    "},{"location":"api/#atomlib.HasAtomCell.columns","title":"columns property","text":"
    columns: List[str]\n

    Return the column names in self.

    RETURNS DESCRIPTION List[str]

    A sequence of column names

    "},{"location":"api/#atomlib.HasAtomCell.dtypes","title":"dtypes property","text":"
    dtypes: List[DataType]\n

    Return the datatypes in self.

    RETURNS DESCRIPTION List[DataType]

    A sequence of column DataTypes

    "},{"location":"api/#atomlib.HasAtomCell.schema","title":"schema property","text":"
    schema: Schema\n

    Return the schema of self.

    RETURNS DESCRIPTION Schema

    A dictionary of column names and DataTypes

    "},{"location":"api/#atomlib.HasAtomCell.unique","title":"unique class-attribute instance-attribute","text":"
    unique = deduplicate\n
    "},{"location":"api/#atomlib.HasAtomCell.with_column","title":"with_column class-attribute instance-attribute","text":"
    with_column = with_columns\n
    "},{"location":"api/#atomlib.HasAtomCell.get_cell","title":"get_cell abstractmethod","text":"
    get_cell() -> Cell\n

    Get the cell contained in self. This should be a low cost method.

    Source code in atomlib/cell.py
    @abc.abstractmethod\ndef get_cell(self) -> Cell:\n    \"\"\"Get the cell contained in ``self``. This should be a low cost method.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.get_transform","title":"get_transform","text":"
    get_transform(\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n

    In the two-argument form, get the transform to frame_to from frame_from. In the one-argument form, get the transform from local coordinates to 'frame'.

    Source code in atomlib/cell.py
    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n    \"\"\"\n    In the two-argument form, get the transform to `frame_to` from `frame_from`.\n    In the one-argument form, get the transform from local coordinates to 'frame'.\n    \"\"\"\n    transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n    transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n    if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n        return AffineTransform3D()\n    return transform_to.inverse() @ transform_from\n
    "},{"location":"api/#atomlib.HasAtomCell.corners","title":"corners","text":"
    corners(frame: CoordinateFrame = 'local') -> ndarray\n
    Source code in atomlib/cell.py
    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n    corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n    return self.get_transform(frame, 'cell_box') @ corners\n
    "},{"location":"api/#atomlib.HasAtomCell.bbox_cell","title":"bbox_cell","text":"
    bbox_cell(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the bounding box of the cell box in the given coordinate system.

    Source code in atomlib/cell.py
    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n    return BBox3D.from_pts(self.corners(frame))\n
    "},{"location":"api/#atomlib.HasAtomCell.is_orthogonal","title":"is_orthogonal","text":"
    is_orthogonal(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal (axes are at right angles.)

    Source code in atomlib/cell.py
    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n    return self.ortho.is_diagonal(tol=tol)\n
    "},{"location":"api/#atomlib.HasAtomCell.is_orthogonal_in_local","title":"is_orthogonal_in_local","text":"
    is_orthogonal_in_local(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal and aligned with the local coordinate system.

    Source code in atomlib/cell.py
    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n    transform = (self.affine @ self.ortho).to_linear()\n    if not transform.is_scaled_orthogonal(tol):\n        return False\n    normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n    # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n    return all(\n        any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n        for row in normed\n    )\n
    "},{"location":"api/#atomlib.HasAtomCell.to_ortho","title":"to_ortho","text":"
    to_ortho() -> AffineTransform3D\n
    Source code in atomlib/cell.py
    def to_ortho(self) -> AffineTransform3D:\n    return self.get_transform('local', 'cell_box')\n
    "},{"location":"api/#atomlib.HasAtomCell.strain_orthogonal","title":"strain_orthogonal","text":"
    strain_orthogonal() -> HasCellT\n

    Orthogonalize using strain.

    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane. For small displacements, no hydrostatic strain is applied (volume is conserved).

    Source code in atomlib/cell.py
    def strain_orthogonal(self: HasCellT) -> HasCellT:\n    \"\"\"\n    Orthogonalize using strain.\n\n    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n    For small displacements, no hydrostatic strain is applied (volume is conserved).\n    \"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=LinearTransform3D(),\n        cell_size=self.cell_size,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.HasAtomCell.explode_z","title":"explode_z","text":"
    explode_z() -> HasCellT\n

    Materialize repeated cells as one supercell in z.

    Source code in atomlib/cell.py
    def explode_z(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n        n_cells=[*self.n_cells[:2], 1],\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/#atomlib.HasAtomCell.change_transform","title":"change_transform","text":"
    change_transform(\n    transform: AffineTransform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n

    Coordinate-change a transformation from frame_from into frame_to.

    Source code in atomlib/cell.py
    def change_transform(self, transform: Transform3D,\n                     frame_to: t.Optional[CoordinateFrame] = None,\n                     frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n    \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n    if frame_to == frame_from and frame_to is not None:\n        return transform\n    coord_change = self.get_transform(frame_to, frame_from)\n    return coord_change @ transform @ coord_change.inverse()\n
    "},{"location":"api/#atomlib.HasAtomCell.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n
    Source code in atomlib/atoms.py
    def assert_equal(self, other: t.Any):\n    assert isinstance(other, HasAtoms)\n    assert dict(self.schema) == dict(other.schema)\n    for col in self.schema.keys():\n        polars.testing.assert_series_equal(self[col], other[col], check_names=False, rtol=1e-3, atol=1e-8)\n
    "},{"location":"api/#atomlib.HasAtomCell.insert_column","title":"insert_column","text":"
    insert_column(index: int, column: Series) -> DataFrame\n
    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n    return self._get_frame().insert_column(index, column)\n
    "},{"location":"api/#atomlib.HasAtomCell.get_column_index","title":"get_column_index","text":"
    get_column_index(name: str) -> int\n

    Get the index of a column by name, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_column_index)\ndef get_column_index(self, name: str) -> int:\n    \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.clone","title":"clone","text":"
    clone() -> DataFrame\n

    Return a copy of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef clone(self) -> polars.DataFrame:\n    \"\"\"Return a copy of `self`.\"\"\"\n    return self._get_frame().clone()\n
    "},{"location":"api/#atomlib.HasAtomCell.drop","title":"drop","text":"
    drop(\n    *columns: Union[str, Iterable[str]], strict: bool = True\n) -> DataFrame\n

    Return self with the specified columns removed.

    Source code in atomlib/atoms.py
    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n    \"\"\"Return `self` with the specified columns removed.\"\"\"\n    return self._get_frame().drop(*columns, strict=strict)\n
    "},{"location":"api/#atomlib.HasAtomCell.drop_nulls","title":"drop_nulls","text":"
    drop_nulls(\n    subset: Union[str, Collection[str], None] = None\n) -> DataFrame\n

    Drop rows that contain nulls in any of columns subset.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n    \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n    return self._get_frame().drop_nulls(subset)\n
    "},{"location":"api/#atomlib.HasAtomCell.concat","title":"concat classmethod","text":"
    concat(\n    atoms: Union[\n        HasAtomsT,\n        IntoAtoms,\n        Iterable[Union[HasAtomsT, IntoAtoms]],\n    ],\n    *,\n    rechunk: bool = True,\n    how: ConcatMethod = \"vertical\"\n) -> HasAtomsT\n

    Concatenate multiple Atoms together, handling metadata appropriately.

    Source code in atomlib/atoms.py
    @classmethod\ndef concat(cls: t.Type[HasAtomsT],\n           atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n           rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n    \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n    # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n    # same type as ``cls``.\n    if _is_abstract(cls):\n        raise TypeError(\"concat() must be called on a concrete class.\")\n\n    if isinstance(atoms, HasAtoms):\n        atoms = (atoms,)\n    dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n    representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n    if len(dfs) == 0:\n        return representative.with_atoms(Atoms.empty(), 'local')\n\n    if how in ('vertical', 'vertical_relaxed'):\n        # get order from first member\n        cols = dfs[0].columns\n        dfs = [df.select(cols) for df in dfs]\n    elif how == 'inner':\n        cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n        schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n        if len(schema) == 0:\n            raise ValueError(\"Atoms have no columns in common\")\n\n        dfs = [_select_schema(df, schema) for df in dfs]\n        how = 'vertical'\n\n    return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n
    "},{"location":"api/#atomlib.HasAtomCell.partition_by","title":"partition_by","text":"
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: Literal[False] = False\n) -> List[Self]\n
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: Literal[True] = ...\n) -> Dict[Any, Self]\n
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: bool = False\n) -> Union[List[Self], Dict[Any, Self]]\n

    Group by the given columns and partition into separate dataframes.

    Return the partitions as a dictionary by specifying as_dict=True.

    Source code in atomlib/atoms.py
    def partition_by(\n    self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n    maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n    \"\"\"\n    Group by the given columns and partition into separate dataframes.\n\n    Return the partitions as a dictionary by specifying `as_dict=True`.\n    \"\"\"\n    if as_dict:\n        d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n        return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n    return [\n        self.with_atoms(Atoms(df, _unchecked=True))\n        for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n    ]\n
    "},{"location":"api/#atomlib.HasAtomCell.select_schema","title":"select_schema","text":"
    select_schema(schema: SchemaDict) -> DataFrame\n

    Select columns from self and cast to the given schema. Raises TypeError if a column is not found or if it can't be cast.

    Source code in atomlib/atoms.py
    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n    \"\"\"\n    Select columns from `self` and cast to the given schema.\n    Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n    \"\"\"\n    return _select_schema(self, schema)\n
    "},{"location":"api/#atomlib.HasAtomCell.try_get_column","title":"try_get_column","text":"
    try_get_column(name: str) -> Optional[Series]\n

    Try to get a column from self, returning None if it doesn't exist.

    Source code in atomlib/atoms.py
    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n    \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n    try:\n        return self.get_column(name)\n    except polars.exceptions.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/#atomlib.HasAtomCell.deduplicate","title":"deduplicate","text":"
    deduplicate(\n    tol: float = 0.001,\n    subset: Iterable[str] = (\"x\", \"y\", \"z\", \"symbol\"),\n    keep: UniqueKeepStrategy = \"first\",\n    maintain_order: bool = True,\n) -> Self\n

    De-duplicate atoms in self. Atoms of the same symbol that are closer than tolerance to each other (by Euclidian distance) will be removed, leaving only the atom specified by keep (defaults to the first atom).

    If subset is specified, only those columns will be included while assessing duplicates. Floating point columns other than 'x', 'y', and 'z' will not by toleranced.

    Source code in atomlib/atoms.py
    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n    \"\"\"\n    De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n    to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n    `keep` (defaults to the first atom).\n\n    If `subset` is specified, only those columns will be included while assessing duplicates.\n    Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n    \"\"\"\n    import scipy.spatial\n\n    cols = set((subset,) if isinstance(subset, str) else subset)\n\n    indices = numpy.arange(len(self))\n\n    spatial_cols = cols.intersection(('x', 'y', 'z'))\n    cols -= spatial_cols\n    if len(spatial_cols) > 0:\n        coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n        tree = scipy.spatial.KDTree(coords)\n\n        # TODO This is a bad algorithm\n        while True:\n            changed = False\n            for (i, j) in tree.query_pairs(tol, 2.):\n                # whenever we encounter a pair, ensure their index matches\n                i_i, i_j = indices[[i, j]]\n                if i_i != i_j:\n                    indices[i] = indices[j] = min(i_i, i_j)\n                    changed = True\n            if not changed:\n                break\n\n        self = self.with_column(polars.Series('_unique_pts', indices))\n        cols.add('_unique_pts')\n\n    frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n    if len(spatial_cols) > 0:\n        frame = frame.drop('_unique_pts')\n\n    return self.with_atoms(Atoms(frame, _unchecked=True))\n
    "},{"location":"api/#atomlib.HasAtomCell.with_bounds","title":"with_bounds","text":"
    with_bounds(\n    cell_size: Optional[VecLike] = None,\n    cell_origin: Optional[VecLike] = None,\n) -> \"AtomCell\"\n

    Return a periodic cell with the given orthogonal cell dimensions.

    If cell_size is not specified, it will be assumed (and may be incorrect).

    Source code in atomlib/atoms.py
    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n    \"\"\"\n    Return a periodic cell with the given orthogonal cell dimensions.\n\n    If cell_size is not specified, it will be assumed (and may be incorrect).\n    \"\"\"\n    # TODO: test this\n    from .atomcell import AtomCell\n\n    if cell_size is None:\n        warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n        cell_size = self.bbox().size\n        cell_origin = self.bbox().min\n\n    # TODO test this origin code\n    cell = Cell.from_unit_cell(cell_size)\n    if cell_origin is not None:\n        cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n    return AtomCell(self.get_atoms(), cell, frame='local')\n
    "},{"location":"api/#atomlib.HasAtomCell.x","title":"x","text":"
    x() -> Expr\n
    Source code in atomlib/atoms.py
    def x(self) -> polars.Expr:\n    return polars.col('coords').arr.get(0).alias('x')\n
    "},{"location":"api/#atomlib.HasAtomCell.y","title":"y","text":"
    y() -> Expr\n
    Source code in atomlib/atoms.py
    def y(self) -> polars.Expr:\n    return polars.col('coords').arr.get(1).alias('y')\n
    "},{"location":"api/#atomlib.HasAtomCell.z","title":"z","text":"
    z() -> Expr\n
    Source code in atomlib/atoms.py
    def z(self) -> polars.Expr:\n    return polars.col('coords').arr.get(2).alias('z')\n
    "},{"location":"api/#atomlib.HasAtomCell.types","title":"types","text":"
    types() -> Optional[Series]\n

    Returns a Series of atom types (dtype polars.Int32).

    Source code in atomlib/atoms.py
    def types(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('type')\n
    "},{"location":"api/#atomlib.HasAtomCell.masses","title":"masses","text":"
    masses() -> Optional[Series]\n

    Returns a Series of atom masses (dtype polars.Float32).

    Source code in atomlib/atoms.py
    def masses(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('mass')\n
    "},{"location":"api/#atomlib.HasAtomCell.pos","title":"pos","text":"
    pos(\n    x: Sequence[Optional[float]],\n    /,\n    *,\n    y: None = None,\n    z: None = None,\n    tol: float = 1e-06,\n    **kwargs: Any,\n) -> Expr\n
    pos(\n    x: Optional[float] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n
    pos(\n    x: Union[Sequence[Optional[float]], float, None] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n

    Select all atoms at a given position.

    Formally, returns all atoms within a cube of radius tol centered at (x,y,z), exclusive of the cube's surface.

    Additional parameters given as kwargs will be checked as additional parameters (with strict equality).

    Source code in atomlib/atoms.py
    def pos(self,\n        x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n        y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n        tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n    \"\"\"\n    Select all atoms at a given position.\n\n    Formally, returns all atoms within a cube of radius ``tol``\n    centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n    Additional parameters given as ``kwargs`` will be checked\n    as additional parameters (with strict equality).\n    \"\"\"\n\n    if isinstance(x, t.Sequence):\n        (x, y, z) = x\n\n    tol = abs(float(tol))\n    selection = polars.lit(True)\n    if x is not None:\n        selection &= self.x().is_between(x - tol, x + tol, closed='none')\n    if y is not None:\n        selection &= self.y().is_between(y - tol, y + tol, closed='none')\n    if z is not None:\n        selection &= self.z().is_between(z - tol, z + tol, closed='none')\n    for (col, val) in kwargs.items():\n        selection &= (polars.col(col) == val)\n\n    return selection\n
    "},{"location":"api/#atomlib.HasAtomCell.apply_occupancy","title":"apply_occupancy","text":"
    apply_occupancy(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    For each atom in self, use its frac_occupancy to randomly decide whether to remove it.

    Source code in atomlib/atoms.py
    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n    \"\"\"\n    if 'frac_occupancy' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    frac = self.select('frac_occupancy').to_series().to_numpy()\n    choice = rng.binomial(1, frac).astype(numpy.bool_)\n    return self.filter(polars.lit(choice))\n
    "},{"location":"api/#atomlib.HasAtomCell.get_frame","title":"get_frame abstractmethod","text":"
    get_frame() -> CoordinateFrame\n

    Get the coordinate frame atoms are stored in.

    Source code in atomlib/atomcell.py
    @abc.abstractmethod\ndef get_frame(self) -> CoordinateFrame:\n    \"\"\"Get the coordinate frame atoms are stored in.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_atoms","title":"with_atoms abstractmethod","text":"
    with_atoms(\n    atoms: HasAtoms, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Replace the atoms in self. If no coordinate frame is specified, keep the coordinate frame unchanged.

    Source code in atomlib/atomcell.py
    @abc.abstractmethod\ndef with_atoms(self, atoms: HasAtoms, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Replace the atoms in `self`. If no coordinate frame is specified, keep the coordinate frame unchanged.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_cell","title":"with_cell","text":"
    with_cell(cell: Cell) -> Self\n

    Replace the cell in self, without touching the atomic coordinates.

    Source code in atomlib/atomcell.py
    def with_cell(self, cell: Cell) -> Self:\n    \"\"\"\n    Replace the cell in `self`, without touching the atomic coordinates.\n    \"\"\"\n    return self.to_frame('local').with_cell(cell)\n
    "},{"location":"api/#atomlib.HasAtomCell.get_atomcell","title":"get_atomcell","text":"
    get_atomcell() -> AtomCell\n
    Source code in atomlib/atomcell.py
    def get_atomcell(self) -> AtomCell:\n    frame = self.get_frame()\n    return AtomCell(self.get_atoms(frame), self.get_cell(), frame=frame, keep_frame=True)\n
    "},{"location":"api/#atomlib.HasAtomCell.get_atoms","title":"get_atoms abstractmethod","text":"
    get_atoms(frame: Optional[CoordinateFrame] = None) -> Atoms\n

    Get atoms contained in self, in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @abc.abstractmethod\ndef get_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> Atoms:\n    \"\"\"Get atoms contained in `self`, in the given coordinate frame.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.bbox_atoms","title":"bbox_atoms","text":"
    bbox_atoms(\n    frame: Optional[CoordinateFrame] = None,\n) -> BBox3D\n

    Return the bounding box of all the atoms in self, in the given coordinate frame.

    Source code in atomlib/atomcell.py
    def bbox_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> BBox3D:\n    \"\"\"Return the bounding box of all the atoms in `self`, in the given coordinate frame.\"\"\"\n    return self.get_atoms(frame).bbox()\n
    "},{"location":"api/#atomlib.HasAtomCell.bbox","title":"bbox","text":"
    bbox(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the combined bounding box of the cell and atoms in the given coordinate system. To get the cell or atoms bounding box only, use bbox_cell or bbox_atoms.

    Source code in atomlib/atomcell.py
    def bbox(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"\n    Return the combined bounding box of the cell and atoms in the given coordinate system.\n    To get the cell or atoms bounding box only, use [`bbox_cell`][atomlib.atomcell.HasAtomCell.bbox_cell] or [`bbox_atoms`][atomlib.atomcell.HasAtomCell.bbox_atoms].\n    \"\"\"\n    return self.bbox_atoms(frame) | self.bbox_cell(frame)\n
    "},{"location":"api/#atomlib.HasAtomCell.to_frame","title":"to_frame","text":"
    to_frame(frame: CoordinateFrame) -> Self\n

    Convert the stored Atoms to the given coordinate frame.

    Source code in atomlib/atomcell.py
    def to_frame(self, frame: CoordinateFrame) -> Self:\n    \"\"\"Convert the stored Atoms to the given coordinate frame.\"\"\"\n    return self.with_atoms(self.get_atoms(frame), frame)\n
    "},{"location":"api/#atomlib.HasAtomCell.transform_atoms","title":"transform_atoms","text":"
    transform_atoms(\n    transform: IntoTransform3D,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: CoordinateFrame = \"local\",\n    transform_velocities: bool = False\n) -> Self\n

    Transform the atoms in self by transform. If selection is given, only transform the atoms in selection.

    Source code in atomlib/atomcell.py
    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                    frame: CoordinateFrame = 'local', transform_velocities: bool = False) -> Self:\n    \"\"\"\n    Transform the atoms in `self` by `transform`.\n    If `selection` is given, only transform the atoms in `selection`.\n    \"\"\"\n    transform = self.change_transform(Transform3D.make(transform), self.get_frame(), frame)\n    return self.with_atoms(self.get_atoms(self.get_frame()).transform(transform, selection, transform_velocities=transform_velocities))\n
    "},{"location":"api/#atomlib.HasAtomCell.transform_cell","title":"transform_cell","text":"
    transform_cell(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> Self\n

    Apply the given transform to the unit cell, without changing atom positions. The transform is applied in coordinate frame 'frame'.

    Source code in atomlib/atomcell.py
    def transform_cell(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n    \"\"\"\n    Apply the given transform to the unit cell, without changing atom positions.\n    The transform is applied in coordinate frame 'frame'.\n    \"\"\"\n    return self.with_cell(self.get_cell().transform_cell(transform, frame=frame))\n
    "},{"location":"api/#atomlib.HasAtomCell.transform","title":"transform","text":"
    transform(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> Self\n
    Source code in atomlib/atomcell.py
    def transform(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n    if isinstance(transform, Transform3D) and not isinstance(transform, AffineTransform3D):\n        raise ValueError(\"Non-affine transforms cannot change the box dimensions. Use 'transform_atoms' instead.\")\n    # TODO: cleanup once tests pass\n    # coordinate change the transform into atomic coordinates\n    new_cell = self.get_cell().transform_cell(transform, frame)\n    transform = self.get_cell().change_transform(transform, self.get_frame(), frame)\n    return self.with_atoms(self.get_atoms().transform(transform), self.get_frame()).with_cell(new_cell)\n
    "},{"location":"api/#atomlib.HasAtomCell.crop","title":"crop","text":"
    crop(\n    x_min: float = -inf,\n    x_max: float = inf,\n    y_min: float = -inf,\n    y_max: float = inf,\n    z_min: float = -inf,\n    z_max: float = inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> Self\n

    Crop atoms and cell to the given extents. For a non-orthogonal cell, this must be specified in cell coordinates. This function implicity explodes the cell as well.

    To crop atoms only, use crop_atoms instead.

    Source code in atomlib/atomcell.py
    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n         frame: CoordinateFrame = 'local') -> Self:\n    \"\"\"\n    Crop atoms and cell to the given extents. For a non-orthogonal\n    cell, this must be specified in cell coordinates. This\n    function implicity `explode`s the cell as well.\n\n    To crop atoms only, use `crop_atoms` instead.\n    \"\"\"\n\n    cell = self.get_cell().crop(x_min, x_max, y_min, y_max, z_min, z_max, frame=frame)\n    atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n    return self.with_cell(cell).with_atoms(atoms)\n
    "},{"location":"api/#atomlib.HasAtomCell.crop_atoms","title":"crop_atoms","text":"
    crop_atoms(\n    x_min: float = -inf,\n    x_max: float = inf,\n    y_min: float = -inf,\n    y_max: float = inf,\n    z_min: float = -inf,\n    z_max: float = inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> Self\n
    Source code in atomlib/atomcell.py
    def crop_atoms(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n               y_min: float = -numpy.inf, y_max: float = numpy.inf,\n               z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n               frame: CoordinateFrame = 'local') -> Self:\n    atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n    return self.with_atoms(atoms)\n
    "},{"location":"api/#atomlib.HasAtomCell.crop_to_box","title":"crop_to_box","text":"
    crop_to_box(eps: float = 1e-05) -> Self\n
    Source code in atomlib/atomcell.py
    def crop_to_box(self, eps: float = 1e-5) -> Self:\n    atoms = self._transform_atoms_in_frame('cell_box', lambda atoms: atoms.crop_atoms(*([-eps, 1-eps]*3)))\n    return self.with_atoms(atoms)\n
    "},{"location":"api/#atomlib.HasAtomCell.wrap","title":"wrap","text":"
    wrap(eps: float = 1e-05) -> Self\n

    Wrap atoms around the cell boundaries.

    Source code in atomlib/atomcell.py
    def wrap(self, eps: float = 1e-5) -> Self:\n    \"\"\"Wrap atoms around the cell boundaries.\"\"\"\n    return self.with_atoms(self._transform_atoms_in_frame('cell_box', lambda a: a._wrap(eps)))\n
    "},{"location":"api/#atomlib.HasAtomCell.repeat","title":"repeat","text":"
    repeat(n: Union[int, VecLike]) -> Self\n

    Tile the cell

    Source code in atomlib/atomcell.py
    def repeat(self, n: t.Union[int, VecLike]) -> Self:\n    \"\"\"Tile the cell\"\"\"\n    ns = numpy.broadcast_to(n, 3)\n    if not numpy.issubdtype(ns.dtype, numpy.integer):\n        raise ValueError(\"repeat() argument must be an integer or integer array.\")\n\n    cells = numpy.stack(numpy.meshgrid(*map(numpy.arange, ns))) \\\n        .reshape(3, -1).T.astype(float)\n    cells = cells * self.box_size\n\n    atoms = self.get_atoms('cell')\n    atoms = Atoms.concat([\n        atoms.transform(AffineTransform3D.translate(cell))\n        for cell in cells\n    ]) #.transform(self.cell.get_transform('local', 'cell_frac'))\n    return self.with_atoms(atoms, 'cell').with_cell(self.get_cell().repeat(ns))\n
    "},{"location":"api/#atomlib.HasAtomCell.repeat_to","title":"repeat_to","text":"
    repeat_to(\n    size: VecLike, crop: Union[bool, Sequence[bool]] = False\n) -> Self\n

    Repeat the cell so it is at least size along the crystal's axes.

    If crop, then crop the cell to exactly size. This may break periodicity. crop may be a vector, in which case you can specify cropping only along some axes.

    Source code in atomlib/atomcell.py
    def repeat_to(self, size: VecLike, crop: t.Union[bool, t.Sequence[bool]] = False) -> Self:\n    \"\"\"\n    Repeat the cell so it is at least `size` along the crystal's axes.\n\n    If `crop`, then crop the cell to exactly `size`. This may break periodicity.\n    `crop` may be a vector, in which case you can specify cropping only along some axes.\n    \"\"\"\n    size = to_vec3(size)\n    cell_size = self.cell_size * self.n_cells\n    repeat = numpy.maximum(numpy.ceil(size / cell_size).astype(int), 1)\n    atom_cell = self.repeat(repeat)\n\n    crop_v = to_vec3(crop, dtype=numpy.bool_)\n    if numpy.any(crop_v):\n        crop_x, crop_y, crop_z = crop_v\n        return atom_cell.crop(\n            x_max = size[0] if crop_x else numpy.inf,\n            y_max = size[1] if crop_y else numpy.inf,\n            z_max = size[2] if crop_z else numpy.inf,\n            frame='cell'\n        )\n\n    return atom_cell\n
    "},{"location":"api/#atomlib.HasAtomCell.repeat_x","title":"repeat_x","text":"
    repeat_x(n: int) -> Self\n

    Tile the cell in the x axis.

    Source code in atomlib/atomcell.py
    def repeat_x(self, n: int) -> Self:\n    \"\"\"Tile the cell in the x axis.\"\"\"\n    return self.repeat((n, 1, 1))\n
    "},{"location":"api/#atomlib.HasAtomCell.repeat_y","title":"repeat_y","text":"
    repeat_y(n: int) -> Self\n

    Tile the cell in the y axis.

    Source code in atomlib/atomcell.py
    def repeat_y(self, n: int) -> Self:\n    \"\"\"Tile the cell in the y axis.\"\"\"\n    return self.repeat((1, n, 1))\n
    "},{"location":"api/#atomlib.HasAtomCell.repeat_z","title":"repeat_z","text":"
    repeat_z(n: int) -> Self\n

    Tile the cell in the z axis.

    Source code in atomlib/atomcell.py
    def repeat_z(self, n: int) -> Self:\n    \"\"\"Tile the cell in the z axis.\"\"\"\n    return self.repeat((1, 1, n))\n
    "},{"location":"api/#atomlib.HasAtomCell.repeat_to_x","title":"repeat_to_x","text":"
    repeat_to_x(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the x axis.

    Source code in atomlib/atomcell.py
    def repeat_to_x(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the x axis.\"\"\"\n    return self.repeat_to([size, 0., 0.], [crop, False, False])\n
    "},{"location":"api/#atomlib.HasAtomCell.repeat_to_y","title":"repeat_to_y","text":"
    repeat_to_y(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the y axis.

    Source code in atomlib/atomcell.py
    def repeat_to_y(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the y axis.\"\"\"\n    return self.repeat_to([0., size, 0.], [False, crop, False])\n
    "},{"location":"api/#atomlib.HasAtomCell.repeat_to_z","title":"repeat_to_z","text":"
    repeat_to_z(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the z axis.

    Source code in atomlib/atomcell.py
    def repeat_to_z(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the z axis.\"\"\"\n    return self.repeat_to([0., 0., size], [False, False, crop])\n
    "},{"location":"api/#atomlib.HasAtomCell.repeat_to_aspect","title":"repeat_to_aspect","text":"
    repeat_to_aspect(\n    plane: Literal[\"xy\", \"xz\", \"yz\"] = \"xy\",\n    *,\n    aspect: float = 1.0,\n    min_size: Optional[VecLike] = None,\n    max_size: Optional[VecLike] = None\n) -> Self\n

    Repeat to optimize the aspect ratio in plane, while staying above min_size and under max_size.

    Source code in atomlib/atomcell.py
    def repeat_to_aspect(self, plane: t.Literal['xy', 'xz', 'yz'] = 'xy', *,\n                     aspect: float = 1., min_size: t.Optional[VecLike] = None,\n                     max_size: t.Optional[VecLike] = None) -> Self:\n    \"\"\"\n    Repeat to optimize the aspect ratio in `plane`,\n    while staying above `min_size` and under `max_size`.\n    \"\"\"\n    if min_size is None:\n        min_n = numpy.array([1, 1, 1], numpy.int_)\n    else:\n        min_n = numpy.maximum(numpy.ceil(to_vec3(min_size) / self.box_size), 1).astype(numpy.int_)\n\n    if max_size is None:\n        max_n = 3 * min_n\n    else:\n        max_n = numpy.maximum(numpy.floor(to_vec3(max_size) / self.box_size), 1).astype(numpy.int_)\n\n    if plane == 'xy':\n        indices = [0, 1]\n    elif plane == 'xz':\n        indices = [0, 2]\n    elif plane == 'yz':\n        indices = [1, 2]\n    else:\n        raise ValueError(f\"Invalid plane '{plane}'. Exepcted 'xy', 'xz', 'or 'yz'.\")\n\n    na = numpy.arange(min_n[indices[0]], max_n[indices[0]])\n    nb = numpy.arange(min_n[indices[1]], max_n[indices[1]])\n    (na, nb) = numpy.meshgrid(na, nb)\n\n    aspects = na * self.box_size[indices[0]] / (nb * self.box_size[indices[1]])\n    # cost function: log(aspect)^2  (so cost(0.5) == cost(2))\n    min_i = numpy.argmin(numpy.log(aspects / aspect)**2)\n    repeat = numpy.array([1, 1, 1], numpy.int_)\n    repeat[indices] = na.flatten()[min_i], nb.flatten()[min_i]\n    return self.repeat(repeat)\n
    "},{"location":"api/#atomlib.HasAtomCell.explode","title":"explode","text":"
    explode() -> Self\n

    Materialize repeated cells as one supercell.

    Source code in atomlib/atomcell.py
    def explode(self) -> Self:\n    \"\"\"Materialize repeated cells as one supercell.\"\"\"\n    frame = self.get_frame()\n\n    return self.with_atoms(self.get_atoms('local'), 'local') \\\n        .with_cell(self.get_cell().explode()) \\\n        .to_frame(frame)\n
    "},{"location":"api/#atomlib.HasAtomCell.periodic_duplicate","title":"periodic_duplicate","text":"
    periodic_duplicate(eps: float = 1e-05) -> Self\n

    Add duplicate copies of atoms near periodic boundaries.

    For instance, an atom at a corner will be duplicated into 8 copies. This is mostly only useful for visualization.

    Source code in atomlib/atomcell.py
    def periodic_duplicate(self, eps: float = 1e-5) -> Self:\n    \"\"\"\n    Add duplicate copies of atoms near periodic boundaries.\n\n    For instance, an atom at a corner will be duplicated into 8 copies.\n    This is mostly only useful for visualization.\n    \"\"\"\n    frame_save = self.get_frame()\n    self = self.to_frame('cell_box').wrap(eps=eps)\n\n    for i in range(3):\n        self = self.concat((self,\n            self.filter(polars.col('coords').arr.get(i).abs() <= eps, frame='cell_box')\n                .transform_atoms(AffineTransform3D.translate([1. if i == j else 0. for j in range(3)]), frame='cell_box')\n        ))\n\n    return self.to_frame(frame_save)\n
    "},{"location":"api/#atomlib.HasAtomCell.describe","title":"describe","text":"
    describe(\n    percentiles: Union[Sequence[float], float, None] = (\n        0.25,\n        0.5,\n        0.75,\n    ),\n    *,\n    interpolation: RollingInterpolationMethod = \"nearest\",\n    frame: Optional[CoordinateFrame] = None\n) -> DataFrame\n

    Return summary statistics for self. See DataFrame.describe for more information.

    PARAMETER DESCRIPTION percentiles

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), 50% (median), and 75% (third quartile).

    TYPE: Union[Sequence[float], float, None] DEFAULT: (0.25, 0.5, 0.75)

    RETURNS DESCRIPTION DataFrame

    A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n             interpolation: RollingInterpolationMethod = 'nearest',\n             frame: t.Optional[CoordinateFrame] = None) -> polars.DataFrame:\n    \"\"\"\n    Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n    Args:\n      percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                   50% (median), and 75% (third quartile).\n\n    Returns:\n      A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_columns","title":"with_columns","text":"
    with_columns(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self\n

    Return a copy of self with the given columns added.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_columns(self,\n                 *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 frame: t.Optional[CoordinateFrame] = None,\n                 **named_exprs: IntoExpr) -> Self:\n    \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.get_column","title":"get_column","text":"
    get_column(\n    name: str, *, frame: Optional[CoordinateFrame] = None\n) -> Series\n

    Get the specified column from self, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef get_column(self, name: str, *, frame: t.Optional[CoordinateFrame] = None) -> polars.Series:\n    \"\"\"\n    Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.get_columns","title":"get_columns","text":"
    get_columns(\n    *, frame: Optional[CoordinateFrame] = None\n) -> List[Series]\n

    Return all columns from self as a list of Series.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef get_columns(self, *, frame: t.Optional[CoordinateFrame] = None) -> t.List[polars.Series]:\n    \"\"\"\n    Return all columns from `self` as a list of [`Series`][polars.Series].\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.group_by","title":"group_by","text":"
    group_by(\n    *by: Union[IntoExpr, Iterable[IntoExpr]],\n    maintain_order: bool = False,\n    frame: Optional[CoordinateFrame] = None,\n    **named_by: IntoExpr\n) -> GroupBy\n

    Start a group by operation. See DataFrame.group_by for more information.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n             maintain_order: bool = False, frame: t.Optional[CoordinateFrame] = None,\n             **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n    \"\"\"\n    Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.pipe","title":"pipe","text":"
    pipe(\n    function: Callable[Concatenate[HasAtomCellT, P], T],\n    *args: args,\n    **kwargs: kwargs\n) -> T\n

    Apply function to self (in method-call syntax).

    Source code in atomlib/atomcell.py
    def pipe(self: HasAtomCellT, function: t.Callable[Concatenate[HasAtomCellT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n    \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n    return function(self, *args, **kwargs)\n
    "},{"location":"api/#atomlib.HasAtomCell.filter","title":"filter","text":"
    filter(\n    *predicates: Union[\n        None,\n        IntoExprColumn,\n        Iterable[IntoExprColumn],\n        bool,\n        List[bool],\n        ndarray,\n    ],\n    frame: Optional[CoordinateFrame] = None,\n    **constraints: Any\n) -> Self\n

    Filter self, removing rows which evaluate to False.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef filter(\n    self,\n    *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n    frame: t.Optional[CoordinateFrame] = None,\n    **constraints: t.Any,\n) -> Self:\n    \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.sort","title":"sort","text":"
    sort(\n    by: Union[IntoExpr, Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: Union[bool, Sequence[bool]] = False,\n    nulls_last: bool = False\n) -> Self\n

    Sort the atoms in self by the given columns/expressions.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef sort(\n    self,\n    by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: t.Union[bool, t.Sequence[bool]] = False,\n    nulls_last: bool = False,\n) -> Self:\n    \"\"\"\n    Sort the atoms in `self` by the given columns/expressions.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.slice","title":"slice","text":"
    slice(\n    offset: int,\n    length: Optional[int] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return a slice of the rows in self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef slice(self, offset: int, length: t.Optional[int] = None, *,\n          frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return a slice of the rows in `self`.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.head","title":"head","text":"
    head(\n    n: int = 5, *, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return the first n rows of self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef head(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return the first `n` rows of `self`.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.tail","title":"tail","text":"
    tail(\n    n: int = 5, *, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return the last n rows of self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef tail(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return the last `n` rows of `self`.\"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.fill_null","title":"fill_null","text":"
    fill_null(\n    value: Any = None,\n    strategy: Optional[FillNullStrategy] = None,\n    limit: Optional[int] = None,\n    matches_supertype: bool = True,\n) -> Self\n
    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef fill_null(\n    self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n    limit: t.Optional[int] = None, matches_supertype: bool = True,\n) -> Self:\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.fill_nan","title":"fill_nan","text":"
    fill_nan(\n    value: Union[Expr, int, float, None],\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n
    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef fill_nan(self, value: t.Union[polars.Expr, int, float, None], *,\n             frame: t.Optional[CoordinateFrame] = None) -> Self:\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.select","title":"select","text":"
    select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef select(\n    self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> polars.DataFrame:\n    \"\"\"\n    Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.select_props","title":"select_props","text":"
    select_props(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self\n

    Select exprs from self, while keeping required columns. Doesn't affect the cell.

    RETURNS DESCRIPTION Self

    A HasAtomCell filtered to contain

    Self

    the specified properties (as well as required columns).

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef select_props(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self:\n    \"\"\"\n    Select `exprs` from `self`, while keeping required columns.\n    Doesn't affect the cell.\n\n    Returns:\n      A [`HasAtomCell`][atomlib.atomcell.HasAtomCell] filtered to contain\n      the specified properties (as well as required columns).\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.try_select","title":"try_select","text":"
    try_select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Optional[DataFrame]\n

    Try to select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns. Returns None if any columns are missing.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef try_select(\n    self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> t.Optional[polars.DataFrame]:\n    \"\"\"\n    Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns. Returns `None` if any\n    columns are missing.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.round_near_zero","title":"round_near_zero","text":"
    round_near_zero(\n    tol: float = 1e-14,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Round atom position values near zero to zero.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef round_near_zero(self, tol: float = 1e-14, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Round atom position values near zero to zero.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.coords","title":"coords","text":"
    coords(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> NDArray[float64]\n

    Return a (N, 3) ndarray of atom positions (dtype numpy.float64) in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> NDArray[numpy.float64]:\n    \"\"\"\n    Return a `(N, 3)` ndarray of atom positions (dtype [`numpy.float64`][numpy.float64])\n    in the given coordinate frame.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.velocities","title":"velocities","text":"
    velocities(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Optional[NDArray[float64]]\n

    Return a (N, 3) ndarray of atom velocities (dtype numpy.float64) in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef velocities(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> t.Optional[NDArray[numpy.float64]]:\n    \"\"\"\n    Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64])\n    in the given coordinate frame.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.add_atom","title":"add_atom","text":"
    add_atom(\n    elem: Union[int, str],\n    x: ArrayLike,\n    /,\n    *,\n    y: None = None,\n    z: None = None,\n    frame: Optional[CoordinateFrame] = None,\n    **kwargs: Any,\n) -> Self\n
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: float,\n    y: float,\n    z: float,\n    *,\n    frame: Optional[CoordinateFrame] = None,\n    **kwargs: Any,\n) -> Self\n
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: Union[ArrayLike, float],\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None,\n    **kwargs: Any,\n) -> Self\n

    Return a copy of self with an extra atom.

    By default, all extra columns present in self must be specified as **kwargs.

    Try to avoid calling this in a loop (Use concat instead).

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef add_atom(self, elem: t.Union[int, str], /,  # type: ignore (spurious)\n             x: t.Union[ArrayLike, float],\n             y: t.Optional[float] = None,\n             z: t.Optional[float] = None, *,\n             frame: t.Optional[CoordinateFrame] = None,\n             **kwargs: t.Any) -> Self:\n    \"\"\"\n    Return a copy of `self` with an extra atom.\n\n    By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n    Try to avoid calling this in a loop (Use [`concat`][atomlib.atomcell.HasAtomCell.concat] instead).\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_index","title":"with_index","text":"
    with_index(\n    index: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Returns self with a row index added in column 'i' (dtype polars.Int64). If index is not specified, defaults to an existing index or a new index.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_index(self, index: t.Optional[AtomValues] = None, *,\n               frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n    If `index` is not specified, defaults to an existing index or a new index.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_wobble","title":"with_wobble","text":"
    with_wobble(\n    wobble: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given displacements in column 'wobble' (dtype polars.Float64). If wobble is not specified, defaults to the already-existing wobbles or 0.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_wobble(self, wobble: t.Optional[AtomValues] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_occupancy","title":"with_occupancy","text":"
    with_occupancy(\n    frac_occupancy: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given fractional occupancies (dtype polars.Float64). If frac_occupancy is not specified, defaults to the already-existing occupancies or 1.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None, *,\n                   frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.apply_wobble","title":"apply_wobble","text":"
    apply_wobble(\n    rng: Union[Generator, int, None] = None,\n    frame: Optional[CoordinateFrame] = None,\n) -> Self\n

    Displace the atoms in self by the amount in the wobble column. wobble is interpretated as a mean-squared displacement, which is distributed equally over each axis.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None,\n                 frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Displace the atoms in `self` by the amount in the `wobble` column.\n    `wobble` is interpretated as a mean-squared displacement, which is distributed\n    equally over each axis.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_type","title":"with_type","text":"
    with_type(\n    types: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atom types in column 'type'. If types is not specified, use the already existing types or auto-assign them.

    When auto-assigning, each symbol is given a unique value, case-sensitive. Values are assigned from lowest atomic number to highest. For instance: [\"Ag+\", \"Na\", \"H\", \"Ag\"] => [3, 11, 1, 2]

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_type(self, types: t.Optional[AtomValues] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom types in column 'type'.\n    If `types` is not specified, use the already existing types or auto-assign them.\n\n    When auto-assigning, each symbol is given a unique value, case-sensitive.\n    Values are assigned from lowest atomic number to highest.\n    For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_mass","title":"with_mass","text":"
    with_mass(\n    mass: Optional[ArrayLike] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atom masses in column 'mass'. If mass is not specified, use the already existing masses or auto-assign them.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_mass(self, mass: t.Optional[ArrayLike] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom masses in column `'mass'`.\n    If `mass` is not specified, use the already existing masses or auto-assign them.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_symbol","title":"with_symbol","text":"
    with_symbol(\n    symbols: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atomic symbols.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atomic symbols.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_coords","title":"with_coords","text":"
    with_coords(\n    pts: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self replaced with the given atomic positions.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic positions.\n    \"\"\"\n    ...\n
    "},{"location":"api/#atomlib.HasAtomCell.with_velocity","title":"with_velocity","text":"
    with_velocity(\n    pts: Optional[ArrayLike] = None,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self replaced with the given atomic velocities. If pts is not specified, use the already existing velocities or zero.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                  selection: t.Optional[AtomSelection] = None, *,\n                  frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic velocities.\n    If `pts` is not specified, use the already existing velocities or zero.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/","title":"atomlib.atomcell","text":"

    Atoms with an associated Cell.

    This module defines HasAtomCell and the concrete AtomCell, which combines the functionality of HasAtoms and HasCell.

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell","title":"HasAtomCell","text":"

    Bases: HasAtoms, HasCell, ABC

    Source code in atomlib/atomcell.py
    class HasAtomCell(HasAtoms, HasCell, abc.ABC):\n    @abc.abstractmethod\n    def get_frame(self) -> CoordinateFrame:\n        \"\"\"Get the coordinate frame atoms are stored in.\"\"\"\n        ...\n\n    @abc.abstractmethod\n    def with_atoms(self, atoms: HasAtoms, frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Replace the atoms in `self`. If no coordinate frame is specified, keep the coordinate frame unchanged.\n        \"\"\"\n        ...\n\n    def with_cell(self, cell: Cell) -> Self:\n        \"\"\"\n        Replace the cell in `self`, without touching the atomic coordinates.\n        \"\"\"\n        return self.to_frame('local').with_cell(cell)\n\n    def get_atomcell(self) -> AtomCell:\n        frame = self.get_frame()\n        return AtomCell(self.get_atoms(frame), self.get_cell(), frame=frame, keep_frame=True)\n\n    @abc.abstractmethod\n    def get_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> Atoms:\n        \"\"\"Get atoms contained in `self`, in the given coordinate frame.\"\"\"\n        ...\n\n    def bbox_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> BBox3D:\n        \"\"\"Return the bounding box of all the atoms in `self`, in the given coordinate frame.\"\"\"\n        return self.get_atoms(frame).bbox()\n\n    def bbox(self, frame: CoordinateFrame = 'local') -> BBox3D:\n        \"\"\"\n        Return the combined bounding box of the cell and atoms in the given coordinate system.\n        To get the cell or atoms bounding box only, use [`bbox_cell`][atomlib.atomcell.HasAtomCell.bbox_cell] or [`bbox_atoms`][atomlib.atomcell.HasAtomCell.bbox_atoms].\n        \"\"\"\n        return self.bbox_atoms(frame) | self.bbox_cell(frame)\n\n    # transformation\n\n    def _transform_atoms_in_frame(self, frame: t.Optional[CoordinateFrame], f: t.Callable[[Atoms], Atoms]) -> Atoms:\n        # ugly code\n        if frame is None or frame == self.get_frame():\n            return f(self.get_atoms())\n        return f(self.get_atoms(frame)).transform(self.get_transform(self.get_frame(), frame))\n\n    def to_frame(self, frame: CoordinateFrame) -> Self:\n        \"\"\"Convert the stored Atoms to the given coordinate frame.\"\"\"\n        return self.with_atoms(self.get_atoms(frame), frame)\n\n    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                        frame: CoordinateFrame = 'local', transform_velocities: bool = False) -> Self:\n        \"\"\"\n        Transform the atoms in `self` by `transform`.\n        If `selection` is given, only transform the atoms in `selection`.\n        \"\"\"\n        transform = self.change_transform(Transform3D.make(transform), self.get_frame(), frame)\n        return self.with_atoms(self.get_atoms(self.get_frame()).transform(transform, selection, transform_velocities=transform_velocities))\n\n    def transform_cell(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n        \"\"\"\n        Apply the given transform to the unit cell, without changing atom positions.\n        The transform is applied in coordinate frame 'frame'.\n        \"\"\"\n        return self.with_cell(self.get_cell().transform_cell(transform, frame=frame))\n\n    def transform(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n        if isinstance(transform, Transform3D) and not isinstance(transform, AffineTransform3D):\n            raise ValueError(\"Non-affine transforms cannot change the box dimensions. Use 'transform_atoms' instead.\")\n        # TODO: cleanup once tests pass\n        # coordinate change the transform into atomic coordinates\n        new_cell = self.get_cell().transform_cell(transform, frame)\n        transform = self.get_cell().change_transform(transform, self.get_frame(), frame)\n        return self.with_atoms(self.get_atoms().transform(transform), self.get_frame()).with_cell(new_cell)\n\n    # crop methods\n\n    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n             y_min: float = -numpy.inf, y_max: float = numpy.inf,\n             z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n             frame: CoordinateFrame = 'local') -> Self:\n        \"\"\"\n        Crop atoms and cell to the given extents. For a non-orthogonal\n        cell, this must be specified in cell coordinates. This\n        function implicity `explode`s the cell as well.\n\n        To crop atoms only, use `crop_atoms` instead.\n        \"\"\"\n\n        cell = self.get_cell().crop(x_min, x_max, y_min, y_max, z_min, z_max, frame=frame)\n        atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n        return self.with_cell(cell).with_atoms(atoms)\n\n    def crop_atoms(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n                   y_min: float = -numpy.inf, y_max: float = numpy.inf,\n                   z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n                   frame: CoordinateFrame = 'local') -> Self:\n        atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n        return self.with_atoms(atoms)\n\n    def crop_to_box(self, eps: float = 1e-5) -> Self:\n        atoms = self._transform_atoms_in_frame('cell_box', lambda atoms: atoms.crop_atoms(*([-eps, 1-eps]*3)))\n        return self.with_atoms(atoms)\n\n    def wrap(self, eps: float = 1e-5) -> Self:\n        \"\"\"Wrap atoms around the cell boundaries.\"\"\"\n        return self.with_atoms(self._transform_atoms_in_frame('cell_box', lambda a: a._wrap(eps)))\n\n    def _repeat_to_contain(self, pts: numpy.ndarray, pad: int = 0, frame: CoordinateFrame = 'cell_frac') -> Self:\n        #print(f\"pts: {pts} in frame {frame}\")\n        pts = self.get_transform('cell_frac', frame) @ pts\n\n        bbox = BBox3D.unit() | BBox3D.from_pts(pts)\n        min_bounds = numpy.floor(bbox.min).astype(int) - pad\n        max_bounds = numpy.ceil(bbox.max).astype(int) + pad\n        #print(f\"tiling to {min_bounds}, {max_bounds}\")\n        repeat = max_bounds - min_bounds\n        cells = numpy.stack(numpy.meshgrid(*map(numpy.arange, repeat))).reshape(3, -1).T.astype(float)\n\n        atoms = self.get_atoms('cell_frac')\n        atoms = Atoms.concat([\n            atoms.transform(AffineTransform3D.translate(cell))\n            for cell in cells\n        ])\n        #print(f\"atoms:\\n{atoms}\")\n        cell = self.get_cell().repeat(repeat) \\\n            .transform_cell(AffineTransform3D.translate(min_bounds), 'cell_frac')\n        return self.with_cell(cell).with_atoms(atoms, 'cell_frac')\n\n    def repeat(self, n: t.Union[int, VecLike]) -> Self:\n        \"\"\"Tile the cell\"\"\"\n        ns = numpy.broadcast_to(n, 3)\n        if not numpy.issubdtype(ns.dtype, numpy.integer):\n            raise ValueError(\"repeat() argument must be an integer or integer array.\")\n\n        cells = numpy.stack(numpy.meshgrid(*map(numpy.arange, ns))) \\\n            .reshape(3, -1).T.astype(float)\n        cells = cells * self.box_size\n\n        atoms = self.get_atoms('cell')\n        atoms = Atoms.concat([\n            atoms.transform(AffineTransform3D.translate(cell))\n            for cell in cells\n        ]) #.transform(self.cell.get_transform('local', 'cell_frac'))\n        return self.with_atoms(atoms, 'cell').with_cell(self.get_cell().repeat(ns))\n\n    def repeat_to(self, size: VecLike, crop: t.Union[bool, t.Sequence[bool]] = False) -> Self:\n        \"\"\"\n        Repeat the cell so it is at least `size` along the crystal's axes.\n\n        If `crop`, then crop the cell to exactly `size`. This may break periodicity.\n        `crop` may be a vector, in which case you can specify cropping only along some axes.\n        \"\"\"\n        size = to_vec3(size)\n        cell_size = self.cell_size * self.n_cells\n        repeat = numpy.maximum(numpy.ceil(size / cell_size).astype(int), 1)\n        atom_cell = self.repeat(repeat)\n\n        crop_v = to_vec3(crop, dtype=numpy.bool_)\n        if numpy.any(crop_v):\n            crop_x, crop_y, crop_z = crop_v\n            return atom_cell.crop(\n                x_max = size[0] if crop_x else numpy.inf,\n                y_max = size[1] if crop_y else numpy.inf,\n                z_max = size[2] if crop_z else numpy.inf,\n                frame='cell'\n            )\n\n        return atom_cell\n\n    def repeat_x(self, n: int) -> Self:\n        \"\"\"Tile the cell in the x axis.\"\"\"\n        return self.repeat((n, 1, 1))\n\n    def repeat_y(self, n: int) -> Self:\n        \"\"\"Tile the cell in the y axis.\"\"\"\n        return self.repeat((1, n, 1))\n\n    def repeat_z(self, n: int) -> Self:\n        \"\"\"Tile the cell in the z axis.\"\"\"\n        return self.repeat((1, 1, n))\n\n    def repeat_to_x(self, size: float, crop: bool = False) -> Self:\n        \"\"\"Repeat the cell so it is at least size `size` along the x axis.\"\"\"\n        return self.repeat_to([size, 0., 0.], [crop, False, False])\n\n    def repeat_to_y(self, size: float, crop: bool = False) -> Self:\n        \"\"\"Repeat the cell so it is at least size `size` along the y axis.\"\"\"\n        return self.repeat_to([0., size, 0.], [False, crop, False])\n\n    def repeat_to_z(self, size: float, crop: bool = False) -> Self:\n        \"\"\"Repeat the cell so it is at least size `size` along the z axis.\"\"\"\n        return self.repeat_to([0., 0., size], [False, False, crop])\n\n    def repeat_to_aspect(self, plane: t.Literal['xy', 'xz', 'yz'] = 'xy', *,\n                         aspect: float = 1., min_size: t.Optional[VecLike] = None,\n                         max_size: t.Optional[VecLike] = None) -> Self:\n        \"\"\"\n        Repeat to optimize the aspect ratio in `plane`,\n        while staying above `min_size` and under `max_size`.\n        \"\"\"\n        if min_size is None:\n            min_n = numpy.array([1, 1, 1], numpy.int_)\n        else:\n            min_n = numpy.maximum(numpy.ceil(to_vec3(min_size) / self.box_size), 1).astype(numpy.int_)\n\n        if max_size is None:\n            max_n = 3 * min_n\n        else:\n            max_n = numpy.maximum(numpy.floor(to_vec3(max_size) / self.box_size), 1).astype(numpy.int_)\n\n        if plane == 'xy':\n            indices = [0, 1]\n        elif plane == 'xz':\n            indices = [0, 2]\n        elif plane == 'yz':\n            indices = [1, 2]\n        else:\n            raise ValueError(f\"Invalid plane '{plane}'. Exepcted 'xy', 'xz', 'or 'yz'.\")\n\n        na = numpy.arange(min_n[indices[0]], max_n[indices[0]])\n        nb = numpy.arange(min_n[indices[1]], max_n[indices[1]])\n        (na, nb) = numpy.meshgrid(na, nb)\n\n        aspects = na * self.box_size[indices[0]] / (nb * self.box_size[indices[1]])\n        # cost function: log(aspect)^2  (so cost(0.5) == cost(2))\n        min_i = numpy.argmin(numpy.log(aspects / aspect)**2)\n        repeat = numpy.array([1, 1, 1], numpy.int_)\n        repeat[indices] = na.flatten()[min_i], nb.flatten()[min_i]\n        return self.repeat(repeat)\n\n    def explode(self) -> Self:\n        \"\"\"Materialize repeated cells as one supercell.\"\"\"\n        frame = self.get_frame()\n\n        return self.with_atoms(self.get_atoms('local'), 'local') \\\n            .with_cell(self.get_cell().explode()) \\\n            .to_frame(frame)\n\n    def periodic_duplicate(self, eps: float = 1e-5) -> Self:\n        \"\"\"\n        Add duplicate copies of atoms near periodic boundaries.\n\n        For instance, an atom at a corner will be duplicated into 8 copies.\n        This is mostly only useful for visualization.\n        \"\"\"\n        frame_save = self.get_frame()\n        self = self.to_frame('cell_box').wrap(eps=eps)\n\n        for i in range(3):\n            self = self.concat((self,\n                self.filter(polars.col('coords').arr.get(i).abs() <= eps, frame='cell_box')\n                    .transform_atoms(AffineTransform3D.translate([1. if i == j else 0. for j in range(3)]), frame='cell_box')\n            ))\n\n        return self.to_frame(frame_save)\n\n    # add frame to some HasAtoms methods\n\n    @_fwd_atoms_get\n    def describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n                 interpolation: RollingInterpolationMethod = 'nearest',\n                 frame: t.Optional[CoordinateFrame] = None) -> polars.DataFrame:\n        \"\"\"\n        Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n        Args:\n          percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                       50% (median), and 75% (third quartile).\n\n        Returns:\n          A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_columns(self,\n                     *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                     frame: t.Optional[CoordinateFrame] = None,\n                     **named_exprs: IntoExpr) -> Self:\n        \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n        ...\n\n    with_column = with_columns\n\n    @_fwd_atoms_get\n    def get_column(self, name: str, *, frame: t.Optional[CoordinateFrame] = None) -> polars.Series:\n        \"\"\"\n        Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        ...\n\n    @_fwd_atoms_get\n    def get_columns(self, *, frame: t.Optional[CoordinateFrame] = None) -> t.List[polars.Series]:\n        \"\"\"\n        Return all columns from `self` as a list of [`Series`][polars.Series].\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        ...\n\n    @_fwd_atoms_get\n    def group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 maintain_order: bool = False, frame: t.Optional[CoordinateFrame] = None,\n                 **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n        \"\"\"\n        Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n        \"\"\"\n        ...\n\n    def pipe(self: HasAtomCellT, function: t.Callable[Concatenate[HasAtomCellT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n        \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n        return function(self, *args, **kwargs)\n\n    @_fwd_atoms_transform\n    def filter(\n        self,\n        *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n        frame: t.Optional[CoordinateFrame] = None,\n        **constraints: t.Any,\n    ) -> Self:\n        \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def sort(\n        self,\n        by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        *more_by: IntoExpr,\n        descending: t.Union[bool, t.Sequence[bool]] = False,\n        nulls_last: bool = False,\n    ) -> Self:\n        \"\"\"\n        Sort the atoms in `self` by the given columns/expressions.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def slice(self, offset: int, length: t.Optional[int] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"Return a slice of the rows in `self`.\"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def head(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"Return the first `n` rows of `self`.\"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def tail(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"Return the last `n` rows of `self`.\"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def fill_null(\n        self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n        limit: t.Optional[int] = None, matches_supertype: bool = True,\n    ) -> Self:\n        ...\n\n    @_fwd_atoms_transform\n    def fill_nan(self, value: t.Union[polars.Expr, int, float, None], *,\n                 frame: t.Optional[CoordinateFrame] = None) -> Self:\n        ...\n\n    # TODO: partition_by\n\n    @_fwd_atoms_get\n    def select(\n        self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        frame: t.Optional[CoordinateFrame] = None,\n        **named_exprs: IntoExpr\n    ) -> polars.DataFrame:\n        \"\"\"\n        Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n        Expressions may either be columns or expressions of columns.\n\n        [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def select_props(\n        self,\n        *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        frame: t.Optional[CoordinateFrame] = None,\n        **named_exprs: IntoExpr\n    ) -> Self:\n        \"\"\"\n        Select `exprs` from `self`, while keeping required columns.\n        Doesn't affect the cell.\n\n        Returns:\n          A [`HasAtomCell`][atomlib.atomcell.HasAtomCell] filtered to contain\n          the specified properties (as well as required columns).\n        \"\"\"\n        ...\n\n    @_fwd_atoms_get\n    def try_select(\n        self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        frame: t.Optional[CoordinateFrame] = None,\n        **named_exprs: IntoExpr\n    ) -> t.Optional[polars.DataFrame]:\n        \"\"\"\n        Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n        Expressions may either be columns or expressions of columns. Returns `None` if any\n        columns are missing.\n\n        [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def round_near_zero(self, tol: float = 1e-14, *,\n                        frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Round atom position values near zero to zero.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_get\n    def coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> NDArray[numpy.float64]:\n        \"\"\"\n        Return a `(N, 3)` ndarray of atom positions (dtype [`numpy.float64`][numpy.float64])\n        in the given coordinate frame.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_get\n    def velocities(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> t.Optional[NDArray[numpy.float64]]:\n        \"\"\"\n        Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64])\n        in the given coordinate frame.\n        \"\"\"\n        ...\n\n    @t.overload\n    def add_atom(self, elem: t.Union[int, str], x: ArrayLike, /, *,\n                 y: None = None, z: None = None, frame: t.Optional[CoordinateFrame] = None,\n                 **kwargs: t.Any) -> Self:\n        ...\n\n    @t.overload\n    def add_atom(self, elem: t.Union[int, str], /,\n                 x: float, y: float, z: float, *,\n                 frame: t.Optional[CoordinateFrame] = None,\n                 **kwargs: t.Any) -> Self:\n        ...\n\n    @_fwd_atoms_transform\n    def add_atom(self, elem: t.Union[int, str], /,  # type: ignore (spurious)\n                 x: t.Union[ArrayLike, float],\n                 y: t.Optional[float] = None,\n                 z: t.Optional[float] = None, *,\n                 frame: t.Optional[CoordinateFrame] = None,\n                 **kwargs: t.Any) -> Self:\n        \"\"\"\n        Return a copy of `self` with an extra atom.\n\n        By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n        Try to avoid calling this in a loop (Use [`concat`][atomlib.atomcell.HasAtomCell.concat] instead).\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_index(self, index: t.Optional[AtomValues] = None, *,\n                   frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n        If `index` is not specified, defaults to an existing index or a new index.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_wobble(self, wobble: t.Optional[AtomValues] = None, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n        If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None, *,\n                       frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n        If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None,\n                     frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Displace the atoms in `self` by the amount in the `wobble` column.\n        `wobble` is interpretated as a mean-squared displacement, which is distributed\n        equally over each axis.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_type(self, types: t.Optional[AtomValues] = None, *,\n                  frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atom types in column 'type'.\n        If `types` is not specified, use the already existing types or auto-assign them.\n\n        When auto-assigning, each symbol is given a unique value, case-sensitive.\n        Values are assigned from lowest atomic number to highest.\n        For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_mass(self, mass: t.Optional[ArrayLike] = None, *,\n                  frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atom masses in column `'mass'`.\n        If `mass` is not specified, use the already existing masses or auto-assign them.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atomic symbols.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` replaced with the given atomic positions.\n        \"\"\"\n        ...\n\n    @_fwd_atoms_transform\n    def with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                      selection: t.Optional[AtomSelection] = None, *,\n                      frame: t.Optional[CoordinateFrame] = None) -> Self:\n        \"\"\"\n        Return `self` replaced with the given atomic velocities.\n        If `pts` is not specified, use the already existing velocities or zero.\n        \"\"\"\n        ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.affine","title":"affine property","text":"
    affine: AffineTransform3D\n

    Affine transformation. Holds transformation from 'ortho' to 'local' coordinates, including rotation away from the standard crystal orientation.

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.ortho","title":"ortho property","text":"
    ortho: LinearTransform3D\n

    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.metric","title":"metric property","text":"
    metric: LinearTransform3D\n

    Cell metric tensor

    Returns the dot product between every combination of basis vectors. :math:\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.cell_size","title":"cell_size property","text":"
    cell_size: NDArray[float64]\n

    Unit cell size.

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.cell_angle","title":"cell_angle property","text":"
    cell_angle: NDArray[float64]\n

    Unit cell angles, in radians.

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.n_cells","title":"n_cells property","text":"
    n_cells: NDArray[int_]\n

    Number of unit cells.

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.pbc","title":"pbc property","text":"
    pbc: NDArray[bool_]\n

    Flags indicating the presence of periodic boundary conditions along each axis.

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.ortho_size","title":"ortho_size property","text":"
    ortho_size: NDArray[float64]\n

    Return size of orthogonal unit cell.

    Equivalent to the diagonal of the orthogonalization matrix.

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.box_size","title":"box_size property","text":"
    box_size: NDArray[float64]\n

    Return size of the cell box.

    Equivalent to self.n_cells * self.cell_size.

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.columns","title":"columns property","text":"
    columns: List[str]\n

    Return the column names in self.

    RETURNS DESCRIPTION List[str]

    A sequence of column names

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.dtypes","title":"dtypes property","text":"
    dtypes: List[DataType]\n

    Return the datatypes in self.

    RETURNS DESCRIPTION List[DataType]

    A sequence of column DataTypes

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.schema","title":"schema property","text":"
    schema: Schema\n

    Return the schema of self.

    RETURNS DESCRIPTION Schema

    A dictionary of column names and DataTypes

    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.unique","title":"unique class-attribute instance-attribute","text":"
    unique = deduplicate\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_column","title":"with_column class-attribute instance-attribute","text":"
    with_column = with_columns\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.get_cell","title":"get_cell abstractmethod","text":"
    get_cell() -> Cell\n

    Get the cell contained in self. This should be a low cost method.

    Source code in atomlib/cell.py
    @abc.abstractmethod\ndef get_cell(self) -> Cell:\n    \"\"\"Get the cell contained in ``self``. This should be a low cost method.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.get_transform","title":"get_transform","text":"
    get_transform(\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n

    In the two-argument form, get the transform to frame_to from frame_from. In the one-argument form, get the transform from local coordinates to 'frame'.

    Source code in atomlib/cell.py
    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n    \"\"\"\n    In the two-argument form, get the transform to `frame_to` from `frame_from`.\n    In the one-argument form, get the transform from local coordinates to 'frame'.\n    \"\"\"\n    transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n    transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n    if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n        return AffineTransform3D()\n    return transform_to.inverse() @ transform_from\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.corners","title":"corners","text":"
    corners(frame: CoordinateFrame = 'local') -> ndarray\n
    Source code in atomlib/cell.py
    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n    corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n    return self.get_transform(frame, 'cell_box') @ corners\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.bbox_cell","title":"bbox_cell","text":"
    bbox_cell(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the bounding box of the cell box in the given coordinate system.

    Source code in atomlib/cell.py
    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n    return BBox3D.from_pts(self.corners(frame))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.is_orthogonal","title":"is_orthogonal","text":"
    is_orthogonal(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal (axes are at right angles.)

    Source code in atomlib/cell.py
    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n    return self.ortho.is_diagonal(tol=tol)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.is_orthogonal_in_local","title":"is_orthogonal_in_local","text":"
    is_orthogonal_in_local(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal and aligned with the local coordinate system.

    Source code in atomlib/cell.py
    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n    transform = (self.affine @ self.ortho).to_linear()\n    if not transform.is_scaled_orthogonal(tol):\n        return False\n    normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n    # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n    return all(\n        any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n        for row in normed\n    )\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.to_ortho","title":"to_ortho","text":"
    to_ortho() -> AffineTransform3D\n
    Source code in atomlib/cell.py
    def to_ortho(self) -> AffineTransform3D:\n    return self.get_transform('local', 'cell_box')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.strain_orthogonal","title":"strain_orthogonal","text":"
    strain_orthogonal() -> HasCellT\n

    Orthogonalize using strain.

    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane. For small displacements, no hydrostatic strain is applied (volume is conserved).

    Source code in atomlib/cell.py
    def strain_orthogonal(self: HasCellT) -> HasCellT:\n    \"\"\"\n    Orthogonalize using strain.\n\n    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n    For small displacements, no hydrostatic strain is applied (volume is conserved).\n    \"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=LinearTransform3D(),\n        cell_size=self.cell_size,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.explode_z","title":"explode_z","text":"
    explode_z() -> HasCellT\n

    Materialize repeated cells as one supercell in z.

    Source code in atomlib/cell.py
    def explode_z(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n        n_cells=[*self.n_cells[:2], 1],\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.change_transform","title":"change_transform","text":"
    change_transform(\n    transform: AffineTransform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n

    Coordinate-change a transformation from frame_from into frame_to.

    Source code in atomlib/cell.py
    def change_transform(self, transform: Transform3D,\n                     frame_to: t.Optional[CoordinateFrame] = None,\n                     frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n    \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n    if frame_to == frame_from and frame_to is not None:\n        return transform\n    coord_change = self.get_transform(frame_to, frame_from)\n    return coord_change @ transform @ coord_change.inverse()\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n
    Source code in atomlib/atoms.py
    def assert_equal(self, other: t.Any):\n    assert isinstance(other, HasAtoms)\n    assert dict(self.schema) == dict(other.schema)\n    for col in self.schema.keys():\n        polars.testing.assert_series_equal(self[col], other[col], check_names=False, rtol=1e-3, atol=1e-8)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.insert_column","title":"insert_column","text":"
    insert_column(index: int, column: Series) -> DataFrame\n
    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n    return self._get_frame().insert_column(index, column)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.get_column_index","title":"get_column_index","text":"
    get_column_index(name: str) -> int\n

    Get the index of a column by name, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_column_index)\ndef get_column_index(self, name: str) -> int:\n    \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.clone","title":"clone","text":"
    clone() -> DataFrame\n

    Return a copy of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef clone(self) -> polars.DataFrame:\n    \"\"\"Return a copy of `self`.\"\"\"\n    return self._get_frame().clone()\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.drop","title":"drop","text":"
    drop(\n    *columns: Union[str, Iterable[str]], strict: bool = True\n) -> DataFrame\n

    Return self with the specified columns removed.

    Source code in atomlib/atoms.py
    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n    \"\"\"Return `self` with the specified columns removed.\"\"\"\n    return self._get_frame().drop(*columns, strict=strict)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.drop_nulls","title":"drop_nulls","text":"
    drop_nulls(\n    subset: Union[str, Collection[str], None] = None\n) -> DataFrame\n

    Drop rows that contain nulls in any of columns subset.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n    \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n    return self._get_frame().drop_nulls(subset)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.concat","title":"concat classmethod","text":"
    concat(\n    atoms: Union[\n        HasAtomsT,\n        IntoAtoms,\n        Iterable[Union[HasAtomsT, IntoAtoms]],\n    ],\n    *,\n    rechunk: bool = True,\n    how: ConcatMethod = \"vertical\"\n) -> HasAtomsT\n

    Concatenate multiple Atoms together, handling metadata appropriately.

    Source code in atomlib/atoms.py
    @classmethod\ndef concat(cls: t.Type[HasAtomsT],\n           atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n           rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n    \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n    # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n    # same type as ``cls``.\n    if _is_abstract(cls):\n        raise TypeError(\"concat() must be called on a concrete class.\")\n\n    if isinstance(atoms, HasAtoms):\n        atoms = (atoms,)\n    dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n    representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n    if len(dfs) == 0:\n        return representative.with_atoms(Atoms.empty(), 'local')\n\n    if how in ('vertical', 'vertical_relaxed'):\n        # get order from first member\n        cols = dfs[0].columns\n        dfs = [df.select(cols) for df in dfs]\n    elif how == 'inner':\n        cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n        schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n        if len(schema) == 0:\n            raise ValueError(\"Atoms have no columns in common\")\n\n        dfs = [_select_schema(df, schema) for df in dfs]\n        how = 'vertical'\n\n    return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.partition_by","title":"partition_by","text":"
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: Literal[False] = False\n) -> List[Self]\n
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: Literal[True] = ...\n) -> Dict[Any, Self]\n
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: bool = False\n) -> Union[List[Self], Dict[Any, Self]]\n

    Group by the given columns and partition into separate dataframes.

    Return the partitions as a dictionary by specifying as_dict=True.

    Source code in atomlib/atoms.py
    def partition_by(\n    self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n    maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n    \"\"\"\n    Group by the given columns and partition into separate dataframes.\n\n    Return the partitions as a dictionary by specifying `as_dict=True`.\n    \"\"\"\n    if as_dict:\n        d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n        return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n    return [\n        self.with_atoms(Atoms(df, _unchecked=True))\n        for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n    ]\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.select_schema","title":"select_schema","text":"
    select_schema(schema: SchemaDict) -> DataFrame\n

    Select columns from self and cast to the given schema. Raises TypeError if a column is not found or if it can't be cast.

    Source code in atomlib/atoms.py
    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n    \"\"\"\n    Select columns from `self` and cast to the given schema.\n    Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n    \"\"\"\n    return _select_schema(self, schema)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.try_get_column","title":"try_get_column","text":"
    try_get_column(name: str) -> Optional[Series]\n

    Try to get a column from self, returning None if it doesn't exist.

    Source code in atomlib/atoms.py
    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n    \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n    try:\n        return self.get_column(name)\n    except polars.exceptions.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.deduplicate","title":"deduplicate","text":"
    deduplicate(\n    tol: float = 0.001,\n    subset: Iterable[str] = (\"x\", \"y\", \"z\", \"symbol\"),\n    keep: UniqueKeepStrategy = \"first\",\n    maintain_order: bool = True,\n) -> Self\n

    De-duplicate atoms in self. Atoms of the same symbol that are closer than tolerance to each other (by Euclidian distance) will be removed, leaving only the atom specified by keep (defaults to the first atom).

    If subset is specified, only those columns will be included while assessing duplicates. Floating point columns other than 'x', 'y', and 'z' will not by toleranced.

    Source code in atomlib/atoms.py
    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n    \"\"\"\n    De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n    to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n    `keep` (defaults to the first atom).\n\n    If `subset` is specified, only those columns will be included while assessing duplicates.\n    Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n    \"\"\"\n    import scipy.spatial\n\n    cols = set((subset,) if isinstance(subset, str) else subset)\n\n    indices = numpy.arange(len(self))\n\n    spatial_cols = cols.intersection(('x', 'y', 'z'))\n    cols -= spatial_cols\n    if len(spatial_cols) > 0:\n        coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n        tree = scipy.spatial.KDTree(coords)\n\n        # TODO This is a bad algorithm\n        while True:\n            changed = False\n            for (i, j) in tree.query_pairs(tol, 2.):\n                # whenever we encounter a pair, ensure their index matches\n                i_i, i_j = indices[[i, j]]\n                if i_i != i_j:\n                    indices[i] = indices[j] = min(i_i, i_j)\n                    changed = True\n            if not changed:\n                break\n\n        self = self.with_column(polars.Series('_unique_pts', indices))\n        cols.add('_unique_pts')\n\n    frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n    if len(spatial_cols) > 0:\n        frame = frame.drop('_unique_pts')\n\n    return self.with_atoms(Atoms(frame, _unchecked=True))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_bounds","title":"with_bounds","text":"
    with_bounds(\n    cell_size: Optional[VecLike] = None,\n    cell_origin: Optional[VecLike] = None,\n) -> \"AtomCell\"\n

    Return a periodic cell with the given orthogonal cell dimensions.

    If cell_size is not specified, it will be assumed (and may be incorrect).

    Source code in atomlib/atoms.py
    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n    \"\"\"\n    Return a periodic cell with the given orthogonal cell dimensions.\n\n    If cell_size is not specified, it will be assumed (and may be incorrect).\n    \"\"\"\n    # TODO: test this\n    from .atomcell import AtomCell\n\n    if cell_size is None:\n        warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n        cell_size = self.bbox().size\n        cell_origin = self.bbox().min\n\n    # TODO test this origin code\n    cell = Cell.from_unit_cell(cell_size)\n    if cell_origin is not None:\n        cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n    return AtomCell(self.get_atoms(), cell, frame='local')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.x","title":"x","text":"
    x() -> Expr\n
    Source code in atomlib/atoms.py
    def x(self) -> polars.Expr:\n    return polars.col('coords').arr.get(0).alias('x')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.y","title":"y","text":"
    y() -> Expr\n
    Source code in atomlib/atoms.py
    def y(self) -> polars.Expr:\n    return polars.col('coords').arr.get(1).alias('y')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.z","title":"z","text":"
    z() -> Expr\n
    Source code in atomlib/atoms.py
    def z(self) -> polars.Expr:\n    return polars.col('coords').arr.get(2).alias('z')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.types","title":"types","text":"
    types() -> Optional[Series]\n

    Returns a Series of atom types (dtype polars.Int32).

    Source code in atomlib/atoms.py
    def types(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('type')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.masses","title":"masses","text":"
    masses() -> Optional[Series]\n

    Returns a Series of atom masses (dtype polars.Float32).

    Source code in atomlib/atoms.py
    def masses(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('mass')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.pos","title":"pos","text":"
    pos(\n    x: Sequence[Optional[float]],\n    /,\n    *,\n    y: None = None,\n    z: None = None,\n    tol: float = 1e-06,\n    **kwargs: Any,\n) -> Expr\n
    pos(\n    x: Optional[float] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n
    pos(\n    x: Union[Sequence[Optional[float]], float, None] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n

    Select all atoms at a given position.

    Formally, returns all atoms within a cube of radius tol centered at (x,y,z), exclusive of the cube's surface.

    Additional parameters given as kwargs will be checked as additional parameters (with strict equality).

    Source code in atomlib/atoms.py
    def pos(self,\n        x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n        y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n        tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n    \"\"\"\n    Select all atoms at a given position.\n\n    Formally, returns all atoms within a cube of radius ``tol``\n    centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n    Additional parameters given as ``kwargs`` will be checked\n    as additional parameters (with strict equality).\n    \"\"\"\n\n    if isinstance(x, t.Sequence):\n        (x, y, z) = x\n\n    tol = abs(float(tol))\n    selection = polars.lit(True)\n    if x is not None:\n        selection &= self.x().is_between(x - tol, x + tol, closed='none')\n    if y is not None:\n        selection &= self.y().is_between(y - tol, y + tol, closed='none')\n    if z is not None:\n        selection &= self.z().is_between(z - tol, z + tol, closed='none')\n    for (col, val) in kwargs.items():\n        selection &= (polars.col(col) == val)\n\n    return selection\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.apply_occupancy","title":"apply_occupancy","text":"
    apply_occupancy(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    For each atom in self, use its frac_occupancy to randomly decide whether to remove it.

    Source code in atomlib/atoms.py
    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n    \"\"\"\n    if 'frac_occupancy' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    frac = self.select('frac_occupancy').to_series().to_numpy()\n    choice = rng.binomial(1, frac).astype(numpy.bool_)\n    return self.filter(polars.lit(choice))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.get_frame","title":"get_frame abstractmethod","text":"
    get_frame() -> CoordinateFrame\n

    Get the coordinate frame atoms are stored in.

    Source code in atomlib/atomcell.py
    @abc.abstractmethod\ndef get_frame(self) -> CoordinateFrame:\n    \"\"\"Get the coordinate frame atoms are stored in.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_atoms","title":"with_atoms abstractmethod","text":"
    with_atoms(\n    atoms: HasAtoms, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Replace the atoms in self. If no coordinate frame is specified, keep the coordinate frame unchanged.

    Source code in atomlib/atomcell.py
    @abc.abstractmethod\ndef with_atoms(self, atoms: HasAtoms, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Replace the atoms in `self`. If no coordinate frame is specified, keep the coordinate frame unchanged.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_cell","title":"with_cell","text":"
    with_cell(cell: Cell) -> Self\n

    Replace the cell in self, without touching the atomic coordinates.

    Source code in atomlib/atomcell.py
    def with_cell(self, cell: Cell) -> Self:\n    \"\"\"\n    Replace the cell in `self`, without touching the atomic coordinates.\n    \"\"\"\n    return self.to_frame('local').with_cell(cell)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.get_atomcell","title":"get_atomcell","text":"
    get_atomcell() -> AtomCell\n
    Source code in atomlib/atomcell.py
    def get_atomcell(self) -> AtomCell:\n    frame = self.get_frame()\n    return AtomCell(self.get_atoms(frame), self.get_cell(), frame=frame, keep_frame=True)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.get_atoms","title":"get_atoms abstractmethod","text":"
    get_atoms(frame: Optional[CoordinateFrame] = None) -> Atoms\n

    Get atoms contained in self, in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @abc.abstractmethod\ndef get_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> Atoms:\n    \"\"\"Get atoms contained in `self`, in the given coordinate frame.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.bbox_atoms","title":"bbox_atoms","text":"
    bbox_atoms(\n    frame: Optional[CoordinateFrame] = None,\n) -> BBox3D\n

    Return the bounding box of all the atoms in self, in the given coordinate frame.

    Source code in atomlib/atomcell.py
    def bbox_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> BBox3D:\n    \"\"\"Return the bounding box of all the atoms in `self`, in the given coordinate frame.\"\"\"\n    return self.get_atoms(frame).bbox()\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.bbox","title":"bbox","text":"
    bbox(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the combined bounding box of the cell and atoms in the given coordinate system. To get the cell or atoms bounding box only, use bbox_cell or bbox_atoms.

    Source code in atomlib/atomcell.py
    def bbox(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"\n    Return the combined bounding box of the cell and atoms in the given coordinate system.\n    To get the cell or atoms bounding box only, use [`bbox_cell`][atomlib.atomcell.HasAtomCell.bbox_cell] or [`bbox_atoms`][atomlib.atomcell.HasAtomCell.bbox_atoms].\n    \"\"\"\n    return self.bbox_atoms(frame) | self.bbox_cell(frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.to_frame","title":"to_frame","text":"
    to_frame(frame: CoordinateFrame) -> Self\n

    Convert the stored Atoms to the given coordinate frame.

    Source code in atomlib/atomcell.py
    def to_frame(self, frame: CoordinateFrame) -> Self:\n    \"\"\"Convert the stored Atoms to the given coordinate frame.\"\"\"\n    return self.with_atoms(self.get_atoms(frame), frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.transform_atoms","title":"transform_atoms","text":"
    transform_atoms(\n    transform: IntoTransform3D,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: CoordinateFrame = \"local\",\n    transform_velocities: bool = False\n) -> Self\n

    Transform the atoms in self by transform. If selection is given, only transform the atoms in selection.

    Source code in atomlib/atomcell.py
    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                    frame: CoordinateFrame = 'local', transform_velocities: bool = False) -> Self:\n    \"\"\"\n    Transform the atoms in `self` by `transform`.\n    If `selection` is given, only transform the atoms in `selection`.\n    \"\"\"\n    transform = self.change_transform(Transform3D.make(transform), self.get_frame(), frame)\n    return self.with_atoms(self.get_atoms(self.get_frame()).transform(transform, selection, transform_velocities=transform_velocities))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.transform_cell","title":"transform_cell","text":"
    transform_cell(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> Self\n

    Apply the given transform to the unit cell, without changing atom positions. The transform is applied in coordinate frame 'frame'.

    Source code in atomlib/atomcell.py
    def transform_cell(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n    \"\"\"\n    Apply the given transform to the unit cell, without changing atom positions.\n    The transform is applied in coordinate frame 'frame'.\n    \"\"\"\n    return self.with_cell(self.get_cell().transform_cell(transform, frame=frame))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.transform","title":"transform","text":"
    transform(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> Self\n
    Source code in atomlib/atomcell.py
    def transform(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n    if isinstance(transform, Transform3D) and not isinstance(transform, AffineTransform3D):\n        raise ValueError(\"Non-affine transforms cannot change the box dimensions. Use 'transform_atoms' instead.\")\n    # TODO: cleanup once tests pass\n    # coordinate change the transform into atomic coordinates\n    new_cell = self.get_cell().transform_cell(transform, frame)\n    transform = self.get_cell().change_transform(transform, self.get_frame(), frame)\n    return self.with_atoms(self.get_atoms().transform(transform), self.get_frame()).with_cell(new_cell)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.crop","title":"crop","text":"
    crop(\n    x_min: float = -inf,\n    x_max: float = inf,\n    y_min: float = -inf,\n    y_max: float = inf,\n    z_min: float = -inf,\n    z_max: float = inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> Self\n

    Crop atoms and cell to the given extents. For a non-orthogonal cell, this must be specified in cell coordinates. This function implicity explodes the cell as well.

    To crop atoms only, use crop_atoms instead.

    Source code in atomlib/atomcell.py
    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n         frame: CoordinateFrame = 'local') -> Self:\n    \"\"\"\n    Crop atoms and cell to the given extents. For a non-orthogonal\n    cell, this must be specified in cell coordinates. This\n    function implicity `explode`s the cell as well.\n\n    To crop atoms only, use `crop_atoms` instead.\n    \"\"\"\n\n    cell = self.get_cell().crop(x_min, x_max, y_min, y_max, z_min, z_max, frame=frame)\n    atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n    return self.with_cell(cell).with_atoms(atoms)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.crop_atoms","title":"crop_atoms","text":"
    crop_atoms(\n    x_min: float = -inf,\n    x_max: float = inf,\n    y_min: float = -inf,\n    y_max: float = inf,\n    z_min: float = -inf,\n    z_max: float = inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> Self\n
    Source code in atomlib/atomcell.py
    def crop_atoms(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n               y_min: float = -numpy.inf, y_max: float = numpy.inf,\n               z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n               frame: CoordinateFrame = 'local') -> Self:\n    atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n    return self.with_atoms(atoms)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.crop_to_box","title":"crop_to_box","text":"
    crop_to_box(eps: float = 1e-05) -> Self\n
    Source code in atomlib/atomcell.py
    def crop_to_box(self, eps: float = 1e-5) -> Self:\n    atoms = self._transform_atoms_in_frame('cell_box', lambda atoms: atoms.crop_atoms(*([-eps, 1-eps]*3)))\n    return self.with_atoms(atoms)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.wrap","title":"wrap","text":"
    wrap(eps: float = 1e-05) -> Self\n

    Wrap atoms around the cell boundaries.

    Source code in atomlib/atomcell.py
    def wrap(self, eps: float = 1e-5) -> Self:\n    \"\"\"Wrap atoms around the cell boundaries.\"\"\"\n    return self.with_atoms(self._transform_atoms_in_frame('cell_box', lambda a: a._wrap(eps)))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.repeat","title":"repeat","text":"
    repeat(n: Union[int, VecLike]) -> Self\n

    Tile the cell

    Source code in atomlib/atomcell.py
    def repeat(self, n: t.Union[int, VecLike]) -> Self:\n    \"\"\"Tile the cell\"\"\"\n    ns = numpy.broadcast_to(n, 3)\n    if not numpy.issubdtype(ns.dtype, numpy.integer):\n        raise ValueError(\"repeat() argument must be an integer or integer array.\")\n\n    cells = numpy.stack(numpy.meshgrid(*map(numpy.arange, ns))) \\\n        .reshape(3, -1).T.astype(float)\n    cells = cells * self.box_size\n\n    atoms = self.get_atoms('cell')\n    atoms = Atoms.concat([\n        atoms.transform(AffineTransform3D.translate(cell))\n        for cell in cells\n    ]) #.transform(self.cell.get_transform('local', 'cell_frac'))\n    return self.with_atoms(atoms, 'cell').with_cell(self.get_cell().repeat(ns))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.repeat_to","title":"repeat_to","text":"
    repeat_to(\n    size: VecLike, crop: Union[bool, Sequence[bool]] = False\n) -> Self\n

    Repeat the cell so it is at least size along the crystal's axes.

    If crop, then crop the cell to exactly size. This may break periodicity. crop may be a vector, in which case you can specify cropping only along some axes.

    Source code in atomlib/atomcell.py
    def repeat_to(self, size: VecLike, crop: t.Union[bool, t.Sequence[bool]] = False) -> Self:\n    \"\"\"\n    Repeat the cell so it is at least `size` along the crystal's axes.\n\n    If `crop`, then crop the cell to exactly `size`. This may break periodicity.\n    `crop` may be a vector, in which case you can specify cropping only along some axes.\n    \"\"\"\n    size = to_vec3(size)\n    cell_size = self.cell_size * self.n_cells\n    repeat = numpy.maximum(numpy.ceil(size / cell_size).astype(int), 1)\n    atom_cell = self.repeat(repeat)\n\n    crop_v = to_vec3(crop, dtype=numpy.bool_)\n    if numpy.any(crop_v):\n        crop_x, crop_y, crop_z = crop_v\n        return atom_cell.crop(\n            x_max = size[0] if crop_x else numpy.inf,\n            y_max = size[1] if crop_y else numpy.inf,\n            z_max = size[2] if crop_z else numpy.inf,\n            frame='cell'\n        )\n\n    return atom_cell\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.repeat_x","title":"repeat_x","text":"
    repeat_x(n: int) -> Self\n

    Tile the cell in the x axis.

    Source code in atomlib/atomcell.py
    def repeat_x(self, n: int) -> Self:\n    \"\"\"Tile the cell in the x axis.\"\"\"\n    return self.repeat((n, 1, 1))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.repeat_y","title":"repeat_y","text":"
    repeat_y(n: int) -> Self\n

    Tile the cell in the y axis.

    Source code in atomlib/atomcell.py
    def repeat_y(self, n: int) -> Self:\n    \"\"\"Tile the cell in the y axis.\"\"\"\n    return self.repeat((1, n, 1))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.repeat_z","title":"repeat_z","text":"
    repeat_z(n: int) -> Self\n

    Tile the cell in the z axis.

    Source code in atomlib/atomcell.py
    def repeat_z(self, n: int) -> Self:\n    \"\"\"Tile the cell in the z axis.\"\"\"\n    return self.repeat((1, 1, n))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.repeat_to_x","title":"repeat_to_x","text":"
    repeat_to_x(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the x axis.

    Source code in atomlib/atomcell.py
    def repeat_to_x(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the x axis.\"\"\"\n    return self.repeat_to([size, 0., 0.], [crop, False, False])\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.repeat_to_y","title":"repeat_to_y","text":"
    repeat_to_y(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the y axis.

    Source code in atomlib/atomcell.py
    def repeat_to_y(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the y axis.\"\"\"\n    return self.repeat_to([0., size, 0.], [False, crop, False])\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.repeat_to_z","title":"repeat_to_z","text":"
    repeat_to_z(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the z axis.

    Source code in atomlib/atomcell.py
    def repeat_to_z(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the z axis.\"\"\"\n    return self.repeat_to([0., 0., size], [False, False, crop])\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.repeat_to_aspect","title":"repeat_to_aspect","text":"
    repeat_to_aspect(\n    plane: Literal[\"xy\", \"xz\", \"yz\"] = \"xy\",\n    *,\n    aspect: float = 1.0,\n    min_size: Optional[VecLike] = None,\n    max_size: Optional[VecLike] = None\n) -> Self\n

    Repeat to optimize the aspect ratio in plane, while staying above min_size and under max_size.

    Source code in atomlib/atomcell.py
    def repeat_to_aspect(self, plane: t.Literal['xy', 'xz', 'yz'] = 'xy', *,\n                     aspect: float = 1., min_size: t.Optional[VecLike] = None,\n                     max_size: t.Optional[VecLike] = None) -> Self:\n    \"\"\"\n    Repeat to optimize the aspect ratio in `plane`,\n    while staying above `min_size` and under `max_size`.\n    \"\"\"\n    if min_size is None:\n        min_n = numpy.array([1, 1, 1], numpy.int_)\n    else:\n        min_n = numpy.maximum(numpy.ceil(to_vec3(min_size) / self.box_size), 1).astype(numpy.int_)\n\n    if max_size is None:\n        max_n = 3 * min_n\n    else:\n        max_n = numpy.maximum(numpy.floor(to_vec3(max_size) / self.box_size), 1).astype(numpy.int_)\n\n    if plane == 'xy':\n        indices = [0, 1]\n    elif plane == 'xz':\n        indices = [0, 2]\n    elif plane == 'yz':\n        indices = [1, 2]\n    else:\n        raise ValueError(f\"Invalid plane '{plane}'. Exepcted 'xy', 'xz', 'or 'yz'.\")\n\n    na = numpy.arange(min_n[indices[0]], max_n[indices[0]])\n    nb = numpy.arange(min_n[indices[1]], max_n[indices[1]])\n    (na, nb) = numpy.meshgrid(na, nb)\n\n    aspects = na * self.box_size[indices[0]] / (nb * self.box_size[indices[1]])\n    # cost function: log(aspect)^2  (so cost(0.5) == cost(2))\n    min_i = numpy.argmin(numpy.log(aspects / aspect)**2)\n    repeat = numpy.array([1, 1, 1], numpy.int_)\n    repeat[indices] = na.flatten()[min_i], nb.flatten()[min_i]\n    return self.repeat(repeat)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.explode","title":"explode","text":"
    explode() -> Self\n

    Materialize repeated cells as one supercell.

    Source code in atomlib/atomcell.py
    def explode(self) -> Self:\n    \"\"\"Materialize repeated cells as one supercell.\"\"\"\n    frame = self.get_frame()\n\n    return self.with_atoms(self.get_atoms('local'), 'local') \\\n        .with_cell(self.get_cell().explode()) \\\n        .to_frame(frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.periodic_duplicate","title":"periodic_duplicate","text":"
    periodic_duplicate(eps: float = 1e-05) -> Self\n

    Add duplicate copies of atoms near periodic boundaries.

    For instance, an atom at a corner will be duplicated into 8 copies. This is mostly only useful for visualization.

    Source code in atomlib/atomcell.py
    def periodic_duplicate(self, eps: float = 1e-5) -> Self:\n    \"\"\"\n    Add duplicate copies of atoms near periodic boundaries.\n\n    For instance, an atom at a corner will be duplicated into 8 copies.\n    This is mostly only useful for visualization.\n    \"\"\"\n    frame_save = self.get_frame()\n    self = self.to_frame('cell_box').wrap(eps=eps)\n\n    for i in range(3):\n        self = self.concat((self,\n            self.filter(polars.col('coords').arr.get(i).abs() <= eps, frame='cell_box')\n                .transform_atoms(AffineTransform3D.translate([1. if i == j else 0. for j in range(3)]), frame='cell_box')\n        ))\n\n    return self.to_frame(frame_save)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.describe","title":"describe","text":"
    describe(\n    percentiles: Union[Sequence[float], float, None] = (\n        0.25,\n        0.5,\n        0.75,\n    ),\n    *,\n    interpolation: RollingInterpolationMethod = \"nearest\",\n    frame: Optional[CoordinateFrame] = None\n) -> DataFrame\n

    Return summary statistics for self. See DataFrame.describe for more information.

    PARAMETER DESCRIPTION percentiles

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), 50% (median), and 75% (third quartile).

    TYPE: Union[Sequence[float], float, None] DEFAULT: (0.25, 0.5, 0.75)

    RETURNS DESCRIPTION DataFrame

    A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n             interpolation: RollingInterpolationMethod = 'nearest',\n             frame: t.Optional[CoordinateFrame] = None) -> polars.DataFrame:\n    \"\"\"\n    Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n    Args:\n      percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                   50% (median), and 75% (third quartile).\n\n    Returns:\n      A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_columns","title":"with_columns","text":"
    with_columns(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self\n

    Return a copy of self with the given columns added.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_columns(self,\n                 *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 frame: t.Optional[CoordinateFrame] = None,\n                 **named_exprs: IntoExpr) -> Self:\n    \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.get_column","title":"get_column","text":"
    get_column(\n    name: str, *, frame: Optional[CoordinateFrame] = None\n) -> Series\n

    Get the specified column from self, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef get_column(self, name: str, *, frame: t.Optional[CoordinateFrame] = None) -> polars.Series:\n    \"\"\"\n    Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.get_columns","title":"get_columns","text":"
    get_columns(\n    *, frame: Optional[CoordinateFrame] = None\n) -> List[Series]\n

    Return all columns from self as a list of Series.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef get_columns(self, *, frame: t.Optional[CoordinateFrame] = None) -> t.List[polars.Series]:\n    \"\"\"\n    Return all columns from `self` as a list of [`Series`][polars.Series].\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.group_by","title":"group_by","text":"
    group_by(\n    *by: Union[IntoExpr, Iterable[IntoExpr]],\n    maintain_order: bool = False,\n    frame: Optional[CoordinateFrame] = None,\n    **named_by: IntoExpr\n) -> GroupBy\n

    Start a group by operation. See DataFrame.group_by for more information.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n             maintain_order: bool = False, frame: t.Optional[CoordinateFrame] = None,\n             **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n    \"\"\"\n    Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.pipe","title":"pipe","text":"
    pipe(\n    function: Callable[Concatenate[HasAtomCellT, P], T],\n    *args: args,\n    **kwargs: kwargs\n) -> T\n

    Apply function to self (in method-call syntax).

    Source code in atomlib/atomcell.py
    def pipe(self: HasAtomCellT, function: t.Callable[Concatenate[HasAtomCellT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n    \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n    return function(self, *args, **kwargs)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.filter","title":"filter","text":"
    filter(\n    *predicates: Union[\n        None,\n        IntoExprColumn,\n        Iterable[IntoExprColumn],\n        bool,\n        List[bool],\n        ndarray,\n    ],\n    frame: Optional[CoordinateFrame] = None,\n    **constraints: Any\n) -> Self\n

    Filter self, removing rows which evaluate to False.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef filter(\n    self,\n    *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n    frame: t.Optional[CoordinateFrame] = None,\n    **constraints: t.Any,\n) -> Self:\n    \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.sort","title":"sort","text":"
    sort(\n    by: Union[IntoExpr, Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: Union[bool, Sequence[bool]] = False,\n    nulls_last: bool = False\n) -> Self\n

    Sort the atoms in self by the given columns/expressions.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef sort(\n    self,\n    by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: t.Union[bool, t.Sequence[bool]] = False,\n    nulls_last: bool = False,\n) -> Self:\n    \"\"\"\n    Sort the atoms in `self` by the given columns/expressions.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.slice","title":"slice","text":"
    slice(\n    offset: int,\n    length: Optional[int] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return a slice of the rows in self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef slice(self, offset: int, length: t.Optional[int] = None, *,\n          frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return a slice of the rows in `self`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.head","title":"head","text":"
    head(\n    n: int = 5, *, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return the first n rows of self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef head(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return the first `n` rows of `self`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.tail","title":"tail","text":"
    tail(\n    n: int = 5, *, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return the last n rows of self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef tail(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return the last `n` rows of `self`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.fill_null","title":"fill_null","text":"
    fill_null(\n    value: Any = None,\n    strategy: Optional[FillNullStrategy] = None,\n    limit: Optional[int] = None,\n    matches_supertype: bool = True,\n) -> Self\n
    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef fill_null(\n    self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n    limit: t.Optional[int] = None, matches_supertype: bool = True,\n) -> Self:\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.fill_nan","title":"fill_nan","text":"
    fill_nan(\n    value: Union[Expr, int, float, None],\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n
    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef fill_nan(self, value: t.Union[polars.Expr, int, float, None], *,\n             frame: t.Optional[CoordinateFrame] = None) -> Self:\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.select","title":"select","text":"
    select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef select(\n    self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> polars.DataFrame:\n    \"\"\"\n    Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.select_props","title":"select_props","text":"
    select_props(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self\n

    Select exprs from self, while keeping required columns. Doesn't affect the cell.

    RETURNS DESCRIPTION Self

    A HasAtomCell filtered to contain

    Self

    the specified properties (as well as required columns).

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef select_props(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self:\n    \"\"\"\n    Select `exprs` from `self`, while keeping required columns.\n    Doesn't affect the cell.\n\n    Returns:\n      A [`HasAtomCell`][atomlib.atomcell.HasAtomCell] filtered to contain\n      the specified properties (as well as required columns).\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.try_select","title":"try_select","text":"
    try_select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Optional[DataFrame]\n

    Try to select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns. Returns None if any columns are missing.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef try_select(\n    self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> t.Optional[polars.DataFrame]:\n    \"\"\"\n    Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns. Returns `None` if any\n    columns are missing.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.round_near_zero","title":"round_near_zero","text":"
    round_near_zero(\n    tol: float = 1e-14,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Round atom position values near zero to zero.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef round_near_zero(self, tol: float = 1e-14, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Round atom position values near zero to zero.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.coords","title":"coords","text":"
    coords(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> NDArray[float64]\n

    Return a (N, 3) ndarray of atom positions (dtype numpy.float64) in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> NDArray[numpy.float64]:\n    \"\"\"\n    Return a `(N, 3)` ndarray of atom positions (dtype [`numpy.float64`][numpy.float64])\n    in the given coordinate frame.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.velocities","title":"velocities","text":"
    velocities(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Optional[NDArray[float64]]\n

    Return a (N, 3) ndarray of atom velocities (dtype numpy.float64) in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef velocities(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> t.Optional[NDArray[numpy.float64]]:\n    \"\"\"\n    Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64])\n    in the given coordinate frame.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.add_atom","title":"add_atom","text":"
    add_atom(\n    elem: Union[int, str],\n    x: ArrayLike,\n    /,\n    *,\n    y: None = None,\n    z: None = None,\n    frame: Optional[CoordinateFrame] = None,\n    **kwargs: Any,\n) -> Self\n
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: float,\n    y: float,\n    z: float,\n    *,\n    frame: Optional[CoordinateFrame] = None,\n    **kwargs: Any,\n) -> Self\n
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: Union[ArrayLike, float],\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None,\n    **kwargs: Any,\n) -> Self\n

    Return a copy of self with an extra atom.

    By default, all extra columns present in self must be specified as **kwargs.

    Try to avoid calling this in a loop (Use concat instead).

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef add_atom(self, elem: t.Union[int, str], /,  # type: ignore (spurious)\n             x: t.Union[ArrayLike, float],\n             y: t.Optional[float] = None,\n             z: t.Optional[float] = None, *,\n             frame: t.Optional[CoordinateFrame] = None,\n             **kwargs: t.Any) -> Self:\n    \"\"\"\n    Return a copy of `self` with an extra atom.\n\n    By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n    Try to avoid calling this in a loop (Use [`concat`][atomlib.atomcell.HasAtomCell.concat] instead).\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_index","title":"with_index","text":"
    with_index(\n    index: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Returns self with a row index added in column 'i' (dtype polars.Int64). If index is not specified, defaults to an existing index or a new index.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_index(self, index: t.Optional[AtomValues] = None, *,\n               frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n    If `index` is not specified, defaults to an existing index or a new index.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_wobble","title":"with_wobble","text":"
    with_wobble(\n    wobble: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given displacements in column 'wobble' (dtype polars.Float64). If wobble is not specified, defaults to the already-existing wobbles or 0.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_wobble(self, wobble: t.Optional[AtomValues] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_occupancy","title":"with_occupancy","text":"
    with_occupancy(\n    frac_occupancy: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given fractional occupancies (dtype polars.Float64). If frac_occupancy is not specified, defaults to the already-existing occupancies or 1.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None, *,\n                   frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.apply_wobble","title":"apply_wobble","text":"
    apply_wobble(\n    rng: Union[Generator, int, None] = None,\n    frame: Optional[CoordinateFrame] = None,\n) -> Self\n

    Displace the atoms in self by the amount in the wobble column. wobble is interpretated as a mean-squared displacement, which is distributed equally over each axis.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None,\n                 frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Displace the atoms in `self` by the amount in the `wobble` column.\n    `wobble` is interpretated as a mean-squared displacement, which is distributed\n    equally over each axis.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_type","title":"with_type","text":"
    with_type(\n    types: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atom types in column 'type'. If types is not specified, use the already existing types or auto-assign them.

    When auto-assigning, each symbol is given a unique value, case-sensitive. Values are assigned from lowest atomic number to highest. For instance: [\"Ag+\", \"Na\", \"H\", \"Ag\"] => [3, 11, 1, 2]

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_type(self, types: t.Optional[AtomValues] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom types in column 'type'.\n    If `types` is not specified, use the already existing types or auto-assign them.\n\n    When auto-assigning, each symbol is given a unique value, case-sensitive.\n    Values are assigned from lowest atomic number to highest.\n    For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_mass","title":"with_mass","text":"
    with_mass(\n    mass: Optional[ArrayLike] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atom masses in column 'mass'. If mass is not specified, use the already existing masses or auto-assign them.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_mass(self, mass: t.Optional[ArrayLike] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom masses in column `'mass'`.\n    If `mass` is not specified, use the already existing masses or auto-assign them.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_symbol","title":"with_symbol","text":"
    with_symbol(\n    symbols: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atomic symbols.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atomic symbols.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_coords","title":"with_coords","text":"
    with_coords(\n    pts: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self replaced with the given atomic positions.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic positions.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.HasAtomCell.with_velocity","title":"with_velocity","text":"
    with_velocity(\n    pts: Optional[ArrayLike] = None,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self replaced with the given atomic velocities. If pts is not specified, use the already existing velocities or zero.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                  selection: t.Optional[AtomSelection] = None, *,\n                  frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic velocities.\n    If `pts` is not specified, use the already existing velocities or zero.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell","title":"AtomCell dataclass","text":"

    Bases: AtomCellIOMixin, HasAtomCell

    Cell of atoms with known size and periodic boundary conditions.

    Source code in atomlib/atomcell.py
    @dataclass(init=False, repr=False, frozen=True)\nclass AtomCell(AtomCellIOMixin, HasAtomCell):\n    \"\"\"\n    Cell of atoms with known size and periodic boundary conditions.\n    \"\"\"\n\n    atoms: Atoms\n    \"\"\"Atoms in the cell. Stored in 'local' coordinates (i.e. relative to the enclosing group but not relative to box dimensions).\"\"\"\n\n    cell: Cell\n    \"\"\"Cell coordinate system.\"\"\"\n\n    frame: CoordinateFrame = 'local'\n    \"\"\"Coordinate frame 'atoms' are stored in.\"\"\"\n\n    def get_cell(self) -> Cell:\n        return self.cell\n\n    def with_cell(self, cell: Cell) -> Self:\n        return self.__class__(self.atoms, cell, frame=self.frame, keep_frame=True)\n\n    def get_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> Atoms:\n        \"\"\"Get atoms contained in ``self``, in the given coordinate frame.\"\"\"\n\n        if frame is None or frame == self.get_frame():\n            return self.atoms\n        return self.atoms.transform(self.get_transform(frame, self.get_frame()))\n\n    def with_atoms(self, atoms: HasAtoms, frame: t.Optional[CoordinateFrame] = None) -> Self:\n        frame = frame if frame is not None else self.frame\n        return self.__class__(atoms.get_atoms(), cell=self.cell, frame=frame, keep_frame=True)\n        #return replace(self, atoms=atoms, frame = frame if frame is not None else self.frame, keep_frame=True)\n\n    def get_frame(self) -> CoordinateFrame:\n        \"\"\"Get the coordinate frame atoms are stored in.\"\"\"\n        return self.frame\n\n    @classmethod\n    def _combine_metadata(cls: t.Type[AtomCellT], *atoms: HasAtoms, n: t.Optional[int] = None) -> AtomCellT:\n        \"\"\"\n        When combining multiple [`HasAtoms`][atomlib.atoms.HasAtoms], check that they are compatible with each other,\n        and return a 'representative' which best represents the combined metadata.\n        Implementors should treat [`Atoms`][atomlib.atoms.Atoms] as acceptable, but having no metadata.\n        \"\"\"\n        if n is not None:\n            rep = atoms[n]\n            if not isinstance(rep, AtomCell):\n                raise ValueError(f\"Atoms #{n} has no cell\")\n        else:\n            atom_cells = [a for a in atoms if isinstance(a, AtomCell)]\n            if len(atom_cells) == 0:\n                raise TypeError(\"No AtomCells to combine\")\n            rep = atom_cells[0]\n            if not all(a.cell == rep.cell for a in atom_cells[1:]):\n                raise TypeError(\"Can't combine AtomCells with different cells\")\n\n        return cls(Atoms.empty(), frame=rep.frame, cell=rep.cell)\n\n    @classmethod\n    def from_ortho(cls, atoms: IntoAtoms, ortho: LinearTransform3D, *,\n                   n_cells: t.Optional[VecLike] = None,\n                   frame: CoordinateFrame = 'local',\n                   keep_frame: bool = False):\n        \"\"\"\n        Make an atom cell given a list of atoms and an orthogonalization matrix.\n        Atoms are assumed to be in the coordinate system `frame`.\n        \"\"\"\n        cell = Cell.from_ortho(ortho, n_cells)\n        return cls(atoms, cell, frame=frame, keep_frame=keep_frame)\n\n    @classmethod\n    def from_unit_cell(cls, atoms: IntoAtoms, cell_size: VecLike,\n                       cell_angle: t.Optional[VecLike] = None, *,\n                       n_cells: t.Optional[VecLike] = None,\n                       frame: CoordinateFrame = 'local',\n                       keep_frame: bool = False):\n        \"\"\"\n        Make a cell given a list of atoms and unit cell parameters.\n        Atoms are assumed to be in the coordinate system `frame`.\n        \"\"\"\n        cell = Cell.from_unit_cell(cell_size, cell_angle, n_cells=n_cells)\n        return cls(atoms, cell, frame=frame, keep_frame=keep_frame)\n\n    def __init__(self, atoms: IntoAtoms, cell: Cell, *,\n                 frame: CoordinateFrame = 'local',\n                 keep_frame: bool = False):\n        atoms = Atoms(atoms)\n        # by default, store in local coordinates\n        if not keep_frame and frame != 'local':\n            atoms = atoms.transform(cell.get_transform('local', frame))\n            frame = 'local'\n\n        object.__setattr__(self, 'atoms', atoms)\n        object.__setattr__(self, 'cell', cell)\n        object.__setattr__(self, 'frame', frame)\n\n        self.__post_init__()\n\n    def __post_init__(self):\n        pass\n\n    def orthogonalize(self) -> OrthoCell:\n        if self.is_orthogonal():\n            return OrthoCell(self.atoms, self.cell, frame=self.frame)\n        raise NotImplementedError()\n\n    def clone(self: AtomCellT) -> AtomCellT:\n        \"\"\"Make a deep copy of `self`.\"\"\"\n        return self.__class__(**{field.name: copy.deepcopy(getattr(self, field.name)) for field in fields(self)})\n\n    def assert_equal(self, other: t.Any):\n        \"\"\"Assert this structure is equal to `other`\"\"\"\n        assert isinstance(other, AtomCell)\n        self.cell.assert_equal(other.cell)\n        self.get_atoms('local').assert_equal(other.get_atoms('local'))\n\n    def _str_parts(self) -> t.Iterable[t.Any]:\n        return (\n            f\"Cell size:  {self.cell.cell_size!s}\",\n            f\"Cell angle: {self.cell.cell_angle!s}\",\n            f\"# Cells: {self.cell.n_cells!s}\",\n            f\"Frame: {self.frame}\",\n            self.atoms,\n        )\n\n    def __str__(self) -> str:\n        return \"\\n\".join(map(str, self._str_parts()))\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}({self.atoms!r}, cell={self.cell!r}, frame={self.frame})\"\n\n    def _repr_pretty_(self, p: t.Any, cycle: bool) -> None:\n        p.text(f'{self.__class__.__name__}(...)') if cycle else p.text(str(self))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.affine","title":"affine property","text":"
    affine: AffineTransform3D\n

    Affine transformation. Holds transformation from 'ortho' to 'local' coordinates, including rotation away from the standard crystal orientation.

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.ortho","title":"ortho property","text":"
    ortho: LinearTransform3D\n

    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.metric","title":"metric property","text":"
    metric: LinearTransform3D\n

    Cell metric tensor

    Returns the dot product between every combination of basis vectors. :math:\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.cell_size","title":"cell_size property","text":"
    cell_size: NDArray[float64]\n

    Unit cell size.

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.cell_angle","title":"cell_angle property","text":"
    cell_angle: NDArray[float64]\n

    Unit cell angles, in radians.

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.n_cells","title":"n_cells property","text":"
    n_cells: NDArray[int_]\n

    Number of unit cells.

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.pbc","title":"pbc property","text":"
    pbc: NDArray[bool_]\n

    Flags indicating the presence of periodic boundary conditions along each axis.

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.ortho_size","title":"ortho_size property","text":"
    ortho_size: NDArray[float64]\n

    Return size of orthogonal unit cell.

    Equivalent to the diagonal of the orthogonalization matrix.

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.box_size","title":"box_size property","text":"
    box_size: NDArray[float64]\n

    Return size of the cell box.

    Equivalent to self.n_cells * self.cell_size.

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.columns","title":"columns property","text":"
    columns: List[str]\n

    Return the column names in self.

    RETURNS DESCRIPTION List[str]

    A sequence of column names

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.dtypes","title":"dtypes property","text":"
    dtypes: List[DataType]\n

    Return the datatypes in self.

    RETURNS DESCRIPTION List[DataType]

    A sequence of column DataTypes

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.schema","title":"schema property","text":"
    schema: Schema\n

    Return the schema of self.

    RETURNS DESCRIPTION Schema

    A dictionary of column names and DataTypes

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_column","title":"with_column class-attribute instance-attribute","text":"
    with_column = with_columns\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.unique","title":"unique class-attribute instance-attribute","text":"
    unique = deduplicate\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.atoms","title":"atoms instance-attribute","text":"
    atoms: Atoms\n

    Atoms in the cell. Stored in 'local' coordinates (i.e. relative to the enclosing group but not relative to box dimensions).

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.cell","title":"cell instance-attribute","text":"
    cell: Cell\n

    Cell coordinate system.

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.frame","title":"frame class-attribute instance-attribute","text":"
    frame: CoordinateFrame = 'local'\n

    Coordinate frame 'atoms' are stored in.

    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.get_transform","title":"get_transform","text":"
    get_transform(\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n

    In the two-argument form, get the transform to frame_to from frame_from. In the one-argument form, get the transform from local coordinates to 'frame'.

    Source code in atomlib/cell.py
    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n    \"\"\"\n    In the two-argument form, get the transform to `frame_to` from `frame_from`.\n    In the one-argument form, get the transform from local coordinates to 'frame'.\n    \"\"\"\n    transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n    transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n    if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n        return AffineTransform3D()\n    return transform_to.inverse() @ transform_from\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.corners","title":"corners","text":"
    corners(frame: CoordinateFrame = 'local') -> ndarray\n
    Source code in atomlib/cell.py
    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n    corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n    return self.get_transform(frame, 'cell_box') @ corners\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.bbox_cell","title":"bbox_cell","text":"
    bbox_cell(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the bounding box of the cell box in the given coordinate system.

    Source code in atomlib/cell.py
    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n    return BBox3D.from_pts(self.corners(frame))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.bbox","title":"bbox","text":"
    bbox(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the combined bounding box of the cell and atoms in the given coordinate system. To get the cell or atoms bounding box only, use bbox_cell or bbox_atoms.

    Source code in atomlib/atomcell.py
    def bbox(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"\n    Return the combined bounding box of the cell and atoms in the given coordinate system.\n    To get the cell or atoms bounding box only, use [`bbox_cell`][atomlib.atomcell.HasAtomCell.bbox_cell] or [`bbox_atoms`][atomlib.atomcell.HasAtomCell.bbox_atoms].\n    \"\"\"\n    return self.bbox_atoms(frame) | self.bbox_cell(frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.is_orthogonal","title":"is_orthogonal","text":"
    is_orthogonal(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal (axes are at right angles.)

    Source code in atomlib/cell.py
    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n    return self.ortho.is_diagonal(tol=tol)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.is_orthogonal_in_local","title":"is_orthogonal_in_local","text":"
    is_orthogonal_in_local(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal and aligned with the local coordinate system.

    Source code in atomlib/cell.py
    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n    transform = (self.affine @ self.ortho).to_linear()\n    if not transform.is_scaled_orthogonal(tol):\n        return False\n    normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n    # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n    return all(\n        any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n        for row in normed\n    )\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.to_ortho","title":"to_ortho","text":"
    to_ortho() -> AffineTransform3D\n
    Source code in atomlib/cell.py
    def to_ortho(self) -> AffineTransform3D:\n    return self.get_transform('local', 'cell_box')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.transform_cell","title":"transform_cell","text":"
    transform_cell(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> Self\n

    Apply the given transform to the unit cell, without changing atom positions. The transform is applied in coordinate frame 'frame'.

    Source code in atomlib/atomcell.py
    def transform_cell(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n    \"\"\"\n    Apply the given transform to the unit cell, without changing atom positions.\n    The transform is applied in coordinate frame 'frame'.\n    \"\"\"\n    return self.with_cell(self.get_cell().transform_cell(transform, frame=frame))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.strain_orthogonal","title":"strain_orthogonal","text":"
    strain_orthogonal() -> HasCellT\n

    Orthogonalize using strain.

    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane. For small displacements, no hydrostatic strain is applied (volume is conserved).

    Source code in atomlib/cell.py
    def strain_orthogonal(self: HasCellT) -> HasCellT:\n    \"\"\"\n    Orthogonalize using strain.\n\n    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n    For small displacements, no hydrostatic strain is applied (volume is conserved).\n    \"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=LinearTransform3D(),\n        cell_size=self.cell_size,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.repeat","title":"repeat","text":"
    repeat(n: Union[int, VecLike]) -> Self\n

    Tile the cell

    Source code in atomlib/atomcell.py
    def repeat(self, n: t.Union[int, VecLike]) -> Self:\n    \"\"\"Tile the cell\"\"\"\n    ns = numpy.broadcast_to(n, 3)\n    if not numpy.issubdtype(ns.dtype, numpy.integer):\n        raise ValueError(\"repeat() argument must be an integer or integer array.\")\n\n    cells = numpy.stack(numpy.meshgrid(*map(numpy.arange, ns))) \\\n        .reshape(3, -1).T.astype(float)\n    cells = cells * self.box_size\n\n    atoms = self.get_atoms('cell')\n    atoms = Atoms.concat([\n        atoms.transform(AffineTransform3D.translate(cell))\n        for cell in cells\n    ]) #.transform(self.cell.get_transform('local', 'cell_frac'))\n    return self.with_atoms(atoms, 'cell').with_cell(self.get_cell().repeat(ns))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.explode","title":"explode","text":"
    explode() -> Self\n

    Materialize repeated cells as one supercell.

    Source code in atomlib/atomcell.py
    def explode(self) -> Self:\n    \"\"\"Materialize repeated cells as one supercell.\"\"\"\n    frame = self.get_frame()\n\n    return self.with_atoms(self.get_atoms('local'), 'local') \\\n        .with_cell(self.get_cell().explode()) \\\n        .to_frame(frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.explode_z","title":"explode_z","text":"
    explode_z() -> HasCellT\n

    Materialize repeated cells as one supercell in z.

    Source code in atomlib/cell.py
    def explode_z(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n        n_cells=[*self.n_cells[:2], 1],\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.crop","title":"crop","text":"
    crop(\n    x_min: float = -inf,\n    x_max: float = inf,\n    y_min: float = -inf,\n    y_max: float = inf,\n    z_min: float = -inf,\n    z_max: float = inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> Self\n

    Crop atoms and cell to the given extents. For a non-orthogonal cell, this must be specified in cell coordinates. This function implicity explodes the cell as well.

    To crop atoms only, use crop_atoms instead.

    Source code in atomlib/atomcell.py
    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n         frame: CoordinateFrame = 'local') -> Self:\n    \"\"\"\n    Crop atoms and cell to the given extents. For a non-orthogonal\n    cell, this must be specified in cell coordinates. This\n    function implicity `explode`s the cell as well.\n\n    To crop atoms only, use `crop_atoms` instead.\n    \"\"\"\n\n    cell = self.get_cell().crop(x_min, x_max, y_min, y_max, z_min, z_max, frame=frame)\n    atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n    return self.with_cell(cell).with_atoms(atoms)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.change_transform","title":"change_transform","text":"
    change_transform(\n    transform: AffineTransform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n

    Coordinate-change a transformation from frame_from into frame_to.

    Source code in atomlib/cell.py
    def change_transform(self, transform: Transform3D,\n                     frame_to: t.Optional[CoordinateFrame] = None,\n                     frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n    \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n    if frame_to == frame_from and frame_to is not None:\n        return transform\n    coord_change = self.get_transform(frame_to, frame_from)\n    return coord_change @ transform @ coord_change.inverse()\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.describe","title":"describe","text":"
    describe(\n    percentiles: Union[Sequence[float], float, None] = (\n        0.25,\n        0.5,\n        0.75,\n    ),\n    *,\n    interpolation: RollingInterpolationMethod = \"nearest\",\n    frame: Optional[CoordinateFrame] = None\n) -> DataFrame\n

    Return summary statistics for self. See DataFrame.describe for more information.

    PARAMETER DESCRIPTION percentiles

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), 50% (median), and 75% (third quartile).

    TYPE: Union[Sequence[float], float, None] DEFAULT: (0.25, 0.5, 0.75)

    RETURNS DESCRIPTION DataFrame

    A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n             interpolation: RollingInterpolationMethod = 'nearest',\n             frame: t.Optional[CoordinateFrame] = None) -> polars.DataFrame:\n    \"\"\"\n    Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n    Args:\n      percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                   50% (median), and 75% (third quartile).\n\n    Returns:\n      A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_columns","title":"with_columns","text":"
    with_columns(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self\n

    Return a copy of self with the given columns added.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_columns(self,\n                 *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 frame: t.Optional[CoordinateFrame] = None,\n                 **named_exprs: IntoExpr) -> Self:\n    \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.insert_column","title":"insert_column","text":"
    insert_column(index: int, column: Series) -> DataFrame\n
    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n    return self._get_frame().insert_column(index, column)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.get_column","title":"get_column","text":"
    get_column(\n    name: str, *, frame: Optional[CoordinateFrame] = None\n) -> Series\n

    Get the specified column from self, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef get_column(self, name: str, *, frame: t.Optional[CoordinateFrame] = None) -> polars.Series:\n    \"\"\"\n    Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.get_columns","title":"get_columns","text":"
    get_columns(\n    *, frame: Optional[CoordinateFrame] = None\n) -> List[Series]\n

    Return all columns from self as a list of Series.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef get_columns(self, *, frame: t.Optional[CoordinateFrame] = None) -> t.List[polars.Series]:\n    \"\"\"\n    Return all columns from `self` as a list of [`Series`][polars.Series].\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.get_column_index","title":"get_column_index","text":"
    get_column_index(name: str) -> int\n

    Get the index of a column by name, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_column_index)\ndef get_column_index(self, name: str) -> int:\n    \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.group_by","title":"group_by","text":"
    group_by(\n    *by: Union[IntoExpr, Iterable[IntoExpr]],\n    maintain_order: bool = False,\n    frame: Optional[CoordinateFrame] = None,\n    **named_by: IntoExpr\n) -> GroupBy\n

    Start a group by operation. See DataFrame.group_by for more information.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n             maintain_order: bool = False, frame: t.Optional[CoordinateFrame] = None,\n             **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n    \"\"\"\n    Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.pipe","title":"pipe","text":"
    pipe(\n    function: Callable[Concatenate[HasAtomCellT, P], T],\n    *args: args,\n    **kwargs: kwargs\n) -> T\n

    Apply function to self (in method-call syntax).

    Source code in atomlib/atomcell.py
    def pipe(self: HasAtomCellT, function: t.Callable[Concatenate[HasAtomCellT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n    \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n    return function(self, *args, **kwargs)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.drop","title":"drop","text":"
    drop(\n    *columns: Union[str, Iterable[str]], strict: bool = True\n) -> DataFrame\n

    Return self with the specified columns removed.

    Source code in atomlib/atoms.py
    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n    \"\"\"Return `self` with the specified columns removed.\"\"\"\n    return self._get_frame().drop(*columns, strict=strict)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.filter","title":"filter","text":"
    filter(\n    *predicates: Union[\n        None,\n        IntoExprColumn,\n        Iterable[IntoExprColumn],\n        bool,\n        List[bool],\n        ndarray,\n    ],\n    frame: Optional[CoordinateFrame] = None,\n    **constraints: Any\n) -> Self\n

    Filter self, removing rows which evaluate to False.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef filter(\n    self,\n    *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n    frame: t.Optional[CoordinateFrame] = None,\n    **constraints: t.Any,\n) -> Self:\n    \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.sort","title":"sort","text":"
    sort(\n    by: Union[IntoExpr, Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: Union[bool, Sequence[bool]] = False,\n    nulls_last: bool = False\n) -> Self\n

    Sort the atoms in self by the given columns/expressions.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef sort(\n    self,\n    by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: t.Union[bool, t.Sequence[bool]] = False,\n    nulls_last: bool = False,\n) -> Self:\n    \"\"\"\n    Sort the atoms in `self` by the given columns/expressions.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.slice","title":"slice","text":"
    slice(\n    offset: int,\n    length: Optional[int] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return a slice of the rows in self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef slice(self, offset: int, length: t.Optional[int] = None, *,\n          frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return a slice of the rows in `self`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.head","title":"head","text":"
    head(\n    n: int = 5, *, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return the first n rows of self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef head(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return the first `n` rows of `self`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.tail","title":"tail","text":"
    tail(\n    n: int = 5, *, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return the last n rows of self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef tail(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return the last `n` rows of `self`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.drop_nulls","title":"drop_nulls","text":"
    drop_nulls(\n    subset: Union[str, Collection[str], None] = None\n) -> DataFrame\n

    Drop rows that contain nulls in any of columns subset.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n    \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n    return self._get_frame().drop_nulls(subset)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.fill_null","title":"fill_null","text":"
    fill_null(\n    value: Any = None,\n    strategy: Optional[FillNullStrategy] = None,\n    limit: Optional[int] = None,\n    matches_supertype: bool = True,\n) -> Self\n
    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef fill_null(\n    self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n    limit: t.Optional[int] = None, matches_supertype: bool = True,\n) -> Self:\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.fill_nan","title":"fill_nan","text":"
    fill_nan(\n    value: Union[Expr, int, float, None],\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n
    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef fill_nan(self, value: t.Union[polars.Expr, int, float, None], *,\n             frame: t.Optional[CoordinateFrame] = None) -> Self:\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.concat","title":"concat classmethod","text":"
    concat(\n    atoms: Union[\n        HasAtomsT,\n        IntoAtoms,\n        Iterable[Union[HasAtomsT, IntoAtoms]],\n    ],\n    *,\n    rechunk: bool = True,\n    how: ConcatMethod = \"vertical\"\n) -> HasAtomsT\n

    Concatenate multiple Atoms together, handling metadata appropriately.

    Source code in atomlib/atoms.py
    @classmethod\ndef concat(cls: t.Type[HasAtomsT],\n           atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n           rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n    \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n    # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n    # same type as ``cls``.\n    if _is_abstract(cls):\n        raise TypeError(\"concat() must be called on a concrete class.\")\n\n    if isinstance(atoms, HasAtoms):\n        atoms = (atoms,)\n    dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n    representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n    if len(dfs) == 0:\n        return representative.with_atoms(Atoms.empty(), 'local')\n\n    if how in ('vertical', 'vertical_relaxed'):\n        # get order from first member\n        cols = dfs[0].columns\n        dfs = [df.select(cols) for df in dfs]\n    elif how == 'inner':\n        cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n        schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n        if len(schema) == 0:\n            raise ValueError(\"Atoms have no columns in common\")\n\n        dfs = [_select_schema(df, schema) for df in dfs]\n        how = 'vertical'\n\n    return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.partition_by","title":"partition_by","text":"
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: Literal[False] = False\n) -> List[Self]\n
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: Literal[True] = ...\n) -> Dict[Any, Self]\n
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: bool = False\n) -> Union[List[Self], Dict[Any, Self]]\n

    Group by the given columns and partition into separate dataframes.

    Return the partitions as a dictionary by specifying as_dict=True.

    Source code in atomlib/atoms.py
    def partition_by(\n    self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n    maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n    \"\"\"\n    Group by the given columns and partition into separate dataframes.\n\n    Return the partitions as a dictionary by specifying `as_dict=True`.\n    \"\"\"\n    if as_dict:\n        d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n        return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n    return [\n        self.with_atoms(Atoms(df, _unchecked=True))\n        for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n    ]\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.select","title":"select","text":"
    select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef select(\n    self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> polars.DataFrame:\n    \"\"\"\n    Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.select_schema","title":"select_schema","text":"
    select_schema(schema: SchemaDict) -> DataFrame\n

    Select columns from self and cast to the given schema. Raises TypeError if a column is not found or if it can't be cast.

    Source code in atomlib/atoms.py
    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n    \"\"\"\n    Select columns from `self` and cast to the given schema.\n    Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n    \"\"\"\n    return _select_schema(self, schema)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.select_props","title":"select_props","text":"
    select_props(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self\n

    Select exprs from self, while keeping required columns. Doesn't affect the cell.

    RETURNS DESCRIPTION Self

    A HasAtomCell filtered to contain

    Self

    the specified properties (as well as required columns).

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef select_props(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self:\n    \"\"\"\n    Select `exprs` from `self`, while keeping required columns.\n    Doesn't affect the cell.\n\n    Returns:\n      A [`HasAtomCell`][atomlib.atomcell.HasAtomCell] filtered to contain\n      the specified properties (as well as required columns).\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.try_select","title":"try_select","text":"
    try_select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Optional[DataFrame]\n

    Try to select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns. Returns None if any columns are missing.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef try_select(\n    self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> t.Optional[polars.DataFrame]:\n    \"\"\"\n    Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns. Returns `None` if any\n    columns are missing.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.try_get_column","title":"try_get_column","text":"
    try_get_column(name: str) -> Optional[Series]\n

    Try to get a column from self, returning None if it doesn't exist.

    Source code in atomlib/atoms.py
    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n    \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n    try:\n        return self.get_column(name)\n    except polars.exceptions.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.bbox_atoms","title":"bbox_atoms","text":"
    bbox_atoms(\n    frame: Optional[CoordinateFrame] = None,\n) -> BBox3D\n

    Return the bounding box of all the atoms in self, in the given coordinate frame.

    Source code in atomlib/atomcell.py
    def bbox_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> BBox3D:\n    \"\"\"Return the bounding box of all the atoms in `self`, in the given coordinate frame.\"\"\"\n    return self.get_atoms(frame).bbox()\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.transform_atoms","title":"transform_atoms","text":"
    transform_atoms(\n    transform: IntoTransform3D,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: CoordinateFrame = \"local\",\n    transform_velocities: bool = False\n) -> Self\n

    Transform the atoms in self by transform. If selection is given, only transform the atoms in selection.

    Source code in atomlib/atomcell.py
    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                    frame: CoordinateFrame = 'local', transform_velocities: bool = False) -> Self:\n    \"\"\"\n    Transform the atoms in `self` by `transform`.\n    If `selection` is given, only transform the atoms in `selection`.\n    \"\"\"\n    transform = self.change_transform(Transform3D.make(transform), self.get_frame(), frame)\n    return self.with_atoms(self.get_atoms(self.get_frame()).transform(transform, selection, transform_velocities=transform_velocities))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.transform","title":"transform","text":"
    transform(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> Self\n
    Source code in atomlib/atomcell.py
    def transform(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n    if isinstance(transform, Transform3D) and not isinstance(transform, AffineTransform3D):\n        raise ValueError(\"Non-affine transforms cannot change the box dimensions. Use 'transform_atoms' instead.\")\n    # TODO: cleanup once tests pass\n    # coordinate change the transform into atomic coordinates\n    new_cell = self.get_cell().transform_cell(transform, frame)\n    transform = self.get_cell().change_transform(transform, self.get_frame(), frame)\n    return self.with_atoms(self.get_atoms().transform(transform), self.get_frame()).with_cell(new_cell)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.round_near_zero","title":"round_near_zero","text":"
    round_near_zero(\n    tol: float = 1e-14,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Round atom position values near zero to zero.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef round_near_zero(self, tol: float = 1e-14, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Round atom position values near zero to zero.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.crop_atoms","title":"crop_atoms","text":"
    crop_atoms(\n    x_min: float = -inf,\n    x_max: float = inf,\n    y_min: float = -inf,\n    y_max: float = inf,\n    z_min: float = -inf,\n    z_max: float = inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> Self\n
    Source code in atomlib/atomcell.py
    def crop_atoms(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n               y_min: float = -numpy.inf, y_max: float = numpy.inf,\n               z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n               frame: CoordinateFrame = 'local') -> Self:\n    atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n    return self.with_atoms(atoms)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.deduplicate","title":"deduplicate","text":"
    deduplicate(\n    tol: float = 0.001,\n    subset: Iterable[str] = (\"x\", \"y\", \"z\", \"symbol\"),\n    keep: UniqueKeepStrategy = \"first\",\n    maintain_order: bool = True,\n) -> Self\n

    De-duplicate atoms in self. Atoms of the same symbol that are closer than tolerance to each other (by Euclidian distance) will be removed, leaving only the atom specified by keep (defaults to the first atom).

    If subset is specified, only those columns will be included while assessing duplicates. Floating point columns other than 'x', 'y', and 'z' will not by toleranced.

    Source code in atomlib/atoms.py
    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n    \"\"\"\n    De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n    to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n    `keep` (defaults to the first atom).\n\n    If `subset` is specified, only those columns will be included while assessing duplicates.\n    Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n    \"\"\"\n    import scipy.spatial\n\n    cols = set((subset,) if isinstance(subset, str) else subset)\n\n    indices = numpy.arange(len(self))\n\n    spatial_cols = cols.intersection(('x', 'y', 'z'))\n    cols -= spatial_cols\n    if len(spatial_cols) > 0:\n        coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n        tree = scipy.spatial.KDTree(coords)\n\n        # TODO This is a bad algorithm\n        while True:\n            changed = False\n            for (i, j) in tree.query_pairs(tol, 2.):\n                # whenever we encounter a pair, ensure their index matches\n                i_i, i_j = indices[[i, j]]\n                if i_i != i_j:\n                    indices[i] = indices[j] = min(i_i, i_j)\n                    changed = True\n            if not changed:\n                break\n\n        self = self.with_column(polars.Series('_unique_pts', indices))\n        cols.add('_unique_pts')\n\n    frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n    if len(spatial_cols) > 0:\n        frame = frame.drop('_unique_pts')\n\n    return self.with_atoms(Atoms(frame, _unchecked=True))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_bounds","title":"with_bounds","text":"
    with_bounds(\n    cell_size: Optional[VecLike] = None,\n    cell_origin: Optional[VecLike] = None,\n) -> \"AtomCell\"\n

    Return a periodic cell with the given orthogonal cell dimensions.

    If cell_size is not specified, it will be assumed (and may be incorrect).

    Source code in atomlib/atoms.py
    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n    \"\"\"\n    Return a periodic cell with the given orthogonal cell dimensions.\n\n    If cell_size is not specified, it will be assumed (and may be incorrect).\n    \"\"\"\n    # TODO: test this\n    from .atomcell import AtomCell\n\n    if cell_size is None:\n        warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n        cell_size = self.bbox().size\n        cell_origin = self.bbox().min\n\n    # TODO test this origin code\n    cell = Cell.from_unit_cell(cell_size)\n    if cell_origin is not None:\n        cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n    return AtomCell(self.get_atoms(), cell, frame='local')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.coords","title":"coords","text":"
    coords(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> NDArray[float64]\n

    Return a (N, 3) ndarray of atom positions (dtype numpy.float64) in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> NDArray[numpy.float64]:\n    \"\"\"\n    Return a `(N, 3)` ndarray of atom positions (dtype [`numpy.float64`][numpy.float64])\n    in the given coordinate frame.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.x","title":"x","text":"
    x() -> Expr\n
    Source code in atomlib/atoms.py
    def x(self) -> polars.Expr:\n    return polars.col('coords').arr.get(0).alias('x')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.y","title":"y","text":"
    y() -> Expr\n
    Source code in atomlib/atoms.py
    def y(self) -> polars.Expr:\n    return polars.col('coords').arr.get(1).alias('y')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.z","title":"z","text":"
    z() -> Expr\n
    Source code in atomlib/atoms.py
    def z(self) -> polars.Expr:\n    return polars.col('coords').arr.get(2).alias('z')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.velocities","title":"velocities","text":"
    velocities(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Optional[NDArray[float64]]\n

    Return a (N, 3) ndarray of atom velocities (dtype numpy.float64) in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef velocities(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> t.Optional[NDArray[numpy.float64]]:\n    \"\"\"\n    Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64])\n    in the given coordinate frame.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.types","title":"types","text":"
    types() -> Optional[Series]\n

    Returns a Series of atom types (dtype polars.Int32).

    Source code in atomlib/atoms.py
    def types(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('type')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.masses","title":"masses","text":"
    masses() -> Optional[Series]\n

    Returns a Series of atom masses (dtype polars.Float32).

    Source code in atomlib/atoms.py
    def masses(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('mass')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.add_atom","title":"add_atom","text":"
    add_atom(\n    elem: Union[int, str],\n    x: ArrayLike,\n    /,\n    *,\n    y: None = None,\n    z: None = None,\n    frame: Optional[CoordinateFrame] = None,\n    **kwargs: Any,\n) -> Self\n
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: float,\n    y: float,\n    z: float,\n    *,\n    frame: Optional[CoordinateFrame] = None,\n    **kwargs: Any,\n) -> Self\n
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: Union[ArrayLike, float],\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None,\n    **kwargs: Any,\n) -> Self\n

    Return a copy of self with an extra atom.

    By default, all extra columns present in self must be specified as **kwargs.

    Try to avoid calling this in a loop (Use concat instead).

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef add_atom(self, elem: t.Union[int, str], /,  # type: ignore (spurious)\n             x: t.Union[ArrayLike, float],\n             y: t.Optional[float] = None,\n             z: t.Optional[float] = None, *,\n             frame: t.Optional[CoordinateFrame] = None,\n             **kwargs: t.Any) -> Self:\n    \"\"\"\n    Return a copy of `self` with an extra atom.\n\n    By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n    Try to avoid calling this in a loop (Use [`concat`][atomlib.atomcell.HasAtomCell.concat] instead).\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.pos","title":"pos","text":"
    pos(\n    x: Sequence[Optional[float]],\n    /,\n    *,\n    y: None = None,\n    z: None = None,\n    tol: float = 1e-06,\n    **kwargs: Any,\n) -> Expr\n
    pos(\n    x: Optional[float] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n
    pos(\n    x: Union[Sequence[Optional[float]], float, None] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n

    Select all atoms at a given position.

    Formally, returns all atoms within a cube of radius tol centered at (x,y,z), exclusive of the cube's surface.

    Additional parameters given as kwargs will be checked as additional parameters (with strict equality).

    Source code in atomlib/atoms.py
    def pos(self,\n        x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n        y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n        tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n    \"\"\"\n    Select all atoms at a given position.\n\n    Formally, returns all atoms within a cube of radius ``tol``\n    centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n    Additional parameters given as ``kwargs`` will be checked\n    as additional parameters (with strict equality).\n    \"\"\"\n\n    if isinstance(x, t.Sequence):\n        (x, y, z) = x\n\n    tol = abs(float(tol))\n    selection = polars.lit(True)\n    if x is not None:\n        selection &= self.x().is_between(x - tol, x + tol, closed='none')\n    if y is not None:\n        selection &= self.y().is_between(y - tol, y + tol, closed='none')\n    if z is not None:\n        selection &= self.z().is_between(z - tol, z + tol, closed='none')\n    for (col, val) in kwargs.items():\n        selection &= (polars.col(col) == val)\n\n    return selection\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_index","title":"with_index","text":"
    with_index(\n    index: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Returns self with a row index added in column 'i' (dtype polars.Int64). If index is not specified, defaults to an existing index or a new index.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_index(self, index: t.Optional[AtomValues] = None, *,\n               frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n    If `index` is not specified, defaults to an existing index or a new index.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_wobble","title":"with_wobble","text":"
    with_wobble(\n    wobble: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given displacements in column 'wobble' (dtype polars.Float64). If wobble is not specified, defaults to the already-existing wobbles or 0.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_wobble(self, wobble: t.Optional[AtomValues] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_occupancy","title":"with_occupancy","text":"
    with_occupancy(\n    frac_occupancy: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given fractional occupancies (dtype polars.Float64). If frac_occupancy is not specified, defaults to the already-existing occupancies or 1.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None, *,\n                   frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.apply_wobble","title":"apply_wobble","text":"
    apply_wobble(\n    rng: Union[Generator, int, None] = None,\n    frame: Optional[CoordinateFrame] = None,\n) -> Self\n

    Displace the atoms in self by the amount in the wobble column. wobble is interpretated as a mean-squared displacement, which is distributed equally over each axis.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None,\n                 frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Displace the atoms in `self` by the amount in the `wobble` column.\n    `wobble` is interpretated as a mean-squared displacement, which is distributed\n    equally over each axis.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.apply_occupancy","title":"apply_occupancy","text":"
    apply_occupancy(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    For each atom in self, use its frac_occupancy to randomly decide whether to remove it.

    Source code in atomlib/atoms.py
    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n    \"\"\"\n    if 'frac_occupancy' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    frac = self.select('frac_occupancy').to_series().to_numpy()\n    choice = rng.binomial(1, frac).astype(numpy.bool_)\n    return self.filter(polars.lit(choice))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_type","title":"with_type","text":"
    with_type(\n    types: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atom types in column 'type'. If types is not specified, use the already existing types or auto-assign them.

    When auto-assigning, each symbol is given a unique value, case-sensitive. Values are assigned from lowest atomic number to highest. For instance: [\"Ag+\", \"Na\", \"H\", \"Ag\"] => [3, 11, 1, 2]

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_type(self, types: t.Optional[AtomValues] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom types in column 'type'.\n    If `types` is not specified, use the already existing types or auto-assign them.\n\n    When auto-assigning, each symbol is given a unique value, case-sensitive.\n    Values are assigned from lowest atomic number to highest.\n    For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_mass","title":"with_mass","text":"
    with_mass(\n    mass: Optional[ArrayLike] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atom masses in column 'mass'. If mass is not specified, use the already existing masses or auto-assign them.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_mass(self, mass: t.Optional[ArrayLike] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom masses in column `'mass'`.\n    If `mass` is not specified, use the already existing masses or auto-assign them.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_symbol","title":"with_symbol","text":"
    with_symbol(\n    symbols: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atomic symbols.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atomic symbols.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_coords","title":"with_coords","text":"
    with_coords(\n    pts: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self replaced with the given atomic positions.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic positions.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_velocity","title":"with_velocity","text":"
    with_velocity(\n    pts: Optional[ArrayLike] = None,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self replaced with the given atomic velocities. If pts is not specified, use the already existing velocities or zero.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                  selection: t.Optional[AtomSelection] = None, *,\n                  frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic velocities.\n    If `pts` is not specified, use the already existing velocities or zero.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.get_atomcell","title":"get_atomcell","text":"
    get_atomcell() -> AtomCell\n
    Source code in atomlib/atomcell.py
    def get_atomcell(self) -> AtomCell:\n    frame = self.get_frame()\n    return AtomCell(self.get_atoms(frame), self.get_cell(), frame=frame, keep_frame=True)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.to_frame","title":"to_frame","text":"
    to_frame(frame: CoordinateFrame) -> Self\n

    Convert the stored Atoms to the given coordinate frame.

    Source code in atomlib/atomcell.py
    def to_frame(self, frame: CoordinateFrame) -> Self:\n    \"\"\"Convert the stored Atoms to the given coordinate frame.\"\"\"\n    return self.with_atoms(self.get_atoms(frame), frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.crop_to_box","title":"crop_to_box","text":"
    crop_to_box(eps: float = 1e-05) -> Self\n
    Source code in atomlib/atomcell.py
    def crop_to_box(self, eps: float = 1e-5) -> Self:\n    atoms = self._transform_atoms_in_frame('cell_box', lambda atoms: atoms.crop_atoms(*([-eps, 1-eps]*3)))\n    return self.with_atoms(atoms)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.wrap","title":"wrap","text":"
    wrap(eps: float = 1e-05) -> Self\n

    Wrap atoms around the cell boundaries.

    Source code in atomlib/atomcell.py
    def wrap(self, eps: float = 1e-5) -> Self:\n    \"\"\"Wrap atoms around the cell boundaries.\"\"\"\n    return self.with_atoms(self._transform_atoms_in_frame('cell_box', lambda a: a._wrap(eps)))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.repeat_to","title":"repeat_to","text":"
    repeat_to(\n    size: VecLike, crop: Union[bool, Sequence[bool]] = False\n) -> Self\n

    Repeat the cell so it is at least size along the crystal's axes.

    If crop, then crop the cell to exactly size. This may break periodicity. crop may be a vector, in which case you can specify cropping only along some axes.

    Source code in atomlib/atomcell.py
    def repeat_to(self, size: VecLike, crop: t.Union[bool, t.Sequence[bool]] = False) -> Self:\n    \"\"\"\n    Repeat the cell so it is at least `size` along the crystal's axes.\n\n    If `crop`, then crop the cell to exactly `size`. This may break periodicity.\n    `crop` may be a vector, in which case you can specify cropping only along some axes.\n    \"\"\"\n    size = to_vec3(size)\n    cell_size = self.cell_size * self.n_cells\n    repeat = numpy.maximum(numpy.ceil(size / cell_size).astype(int), 1)\n    atom_cell = self.repeat(repeat)\n\n    crop_v = to_vec3(crop, dtype=numpy.bool_)\n    if numpy.any(crop_v):\n        crop_x, crop_y, crop_z = crop_v\n        return atom_cell.crop(\n            x_max = size[0] if crop_x else numpy.inf,\n            y_max = size[1] if crop_y else numpy.inf,\n            z_max = size[2] if crop_z else numpy.inf,\n            frame='cell'\n        )\n\n    return atom_cell\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.repeat_x","title":"repeat_x","text":"
    repeat_x(n: int) -> Self\n

    Tile the cell in the x axis.

    Source code in atomlib/atomcell.py
    def repeat_x(self, n: int) -> Self:\n    \"\"\"Tile the cell in the x axis.\"\"\"\n    return self.repeat((n, 1, 1))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.repeat_y","title":"repeat_y","text":"
    repeat_y(n: int) -> Self\n

    Tile the cell in the y axis.

    Source code in atomlib/atomcell.py
    def repeat_y(self, n: int) -> Self:\n    \"\"\"Tile the cell in the y axis.\"\"\"\n    return self.repeat((1, n, 1))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.repeat_z","title":"repeat_z","text":"
    repeat_z(n: int) -> Self\n

    Tile the cell in the z axis.

    Source code in atomlib/atomcell.py
    def repeat_z(self, n: int) -> Self:\n    \"\"\"Tile the cell in the z axis.\"\"\"\n    return self.repeat((1, 1, n))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.repeat_to_x","title":"repeat_to_x","text":"
    repeat_to_x(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the x axis.

    Source code in atomlib/atomcell.py
    def repeat_to_x(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the x axis.\"\"\"\n    return self.repeat_to([size, 0., 0.], [crop, False, False])\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.repeat_to_y","title":"repeat_to_y","text":"
    repeat_to_y(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the y axis.

    Source code in atomlib/atomcell.py
    def repeat_to_y(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the y axis.\"\"\"\n    return self.repeat_to([0., size, 0.], [False, crop, False])\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.repeat_to_z","title":"repeat_to_z","text":"
    repeat_to_z(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the z axis.

    Source code in atomlib/atomcell.py
    def repeat_to_z(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the z axis.\"\"\"\n    return self.repeat_to([0., 0., size], [False, False, crop])\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.repeat_to_aspect","title":"repeat_to_aspect","text":"
    repeat_to_aspect(\n    plane: Literal[\"xy\", \"xz\", \"yz\"] = \"xy\",\n    *,\n    aspect: float = 1.0,\n    min_size: Optional[VecLike] = None,\n    max_size: Optional[VecLike] = None\n) -> Self\n

    Repeat to optimize the aspect ratio in plane, while staying above min_size and under max_size.

    Source code in atomlib/atomcell.py
    def repeat_to_aspect(self, plane: t.Literal['xy', 'xz', 'yz'] = 'xy', *,\n                     aspect: float = 1., min_size: t.Optional[VecLike] = None,\n                     max_size: t.Optional[VecLike] = None) -> Self:\n    \"\"\"\n    Repeat to optimize the aspect ratio in `plane`,\n    while staying above `min_size` and under `max_size`.\n    \"\"\"\n    if min_size is None:\n        min_n = numpy.array([1, 1, 1], numpy.int_)\n    else:\n        min_n = numpy.maximum(numpy.ceil(to_vec3(min_size) / self.box_size), 1).astype(numpy.int_)\n\n    if max_size is None:\n        max_n = 3 * min_n\n    else:\n        max_n = numpy.maximum(numpy.floor(to_vec3(max_size) / self.box_size), 1).astype(numpy.int_)\n\n    if plane == 'xy':\n        indices = [0, 1]\n    elif plane == 'xz':\n        indices = [0, 2]\n    elif plane == 'yz':\n        indices = [1, 2]\n    else:\n        raise ValueError(f\"Invalid plane '{plane}'. Exepcted 'xy', 'xz', 'or 'yz'.\")\n\n    na = numpy.arange(min_n[indices[0]], max_n[indices[0]])\n    nb = numpy.arange(min_n[indices[1]], max_n[indices[1]])\n    (na, nb) = numpy.meshgrid(na, nb)\n\n    aspects = na * self.box_size[indices[0]] / (nb * self.box_size[indices[1]])\n    # cost function: log(aspect)^2  (so cost(0.5) == cost(2))\n    min_i = numpy.argmin(numpy.log(aspects / aspect)**2)\n    repeat = numpy.array([1, 1, 1], numpy.int_)\n    repeat[indices] = na.flatten()[min_i], nb.flatten()[min_i]\n    return self.repeat(repeat)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.periodic_duplicate","title":"periodic_duplicate","text":"
    periodic_duplicate(eps: float = 1e-05) -> Self\n

    Add duplicate copies of atoms near periodic boundaries.

    For instance, an atom at a corner will be duplicated into 8 copies. This is mostly only useful for visualization.

    Source code in atomlib/atomcell.py
    def periodic_duplicate(self, eps: float = 1e-5) -> Self:\n    \"\"\"\n    Add duplicate copies of atoms near periodic boundaries.\n\n    For instance, an atom at a corner will be duplicated into 8 copies.\n    This is mostly only useful for visualization.\n    \"\"\"\n    frame_save = self.get_frame()\n    self = self.to_frame('cell_box').wrap(eps=eps)\n\n    for i in range(3):\n        self = self.concat((self,\n            self.filter(polars.col('coords').arr.get(i).abs() <= eps, frame='cell_box')\n                .transform_atoms(AffineTransform3D.translate([1. if i == j else 0. for j in range(3)]), frame='cell_box')\n        ))\n\n    return self.to_frame(frame_save)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.read","title":"read classmethod","text":"
    read(path: FileOrPath, ty: FileType) -> HasAtomsT\n
    read(\n    path: Union[str, Path, TextIO], ty: Literal[None] = None\n) -> HasAtomsT\n
    read(\n    path: FileOrPath, ty: Optional[FileType] = None\n) -> HasAtomsT\n

    Read a structure from a file.

    Supported types can be found in the io module. If no ty is specified, it is inferred from the file's extension.

    Source code in atomlib/mixins.py
    @classmethod\ndef read(cls: t.Type[HasAtomsT], path: FileOrPath, ty: t.Optional[FileType] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a file.\n\n    Supported types can be found in the [io][atomlib.io] module.\n    If no `ty` is specified, it is inferred from the file's extension.\n    \"\"\"\n    from .io import read\n    return _cast_atoms(read(path, ty), cls)  # type: ignore\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.read_cif","title":"read_cif classmethod","text":"
    read_cif(\n    f: Union[FileOrPath, CIF, CIFDataBlock],\n    block: Union[int, str, None] = None,\n) -> HasAtomsT\n

    Read a structure from a CIF file.

    If block is specified, read data from the given block of the CIF file (index or name).

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cif(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CIF, CIFDataBlock], block: t.Union[int, str, None] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a CIF file.\n\n    If `block` is specified, read data from the given block of the CIF file (index or name).\n    \"\"\"\n    from .io import read_cif\n    return _cast_atoms(read_cif(f, block), cls)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.read_xyz","title":"read_xyz classmethod","text":"
    read_xyz(f: Union[FileOrPath, XYZ]) -> HasAtomsT\n

    Read a structure from an XYZ file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xyz(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XYZ]) -> HasAtomsT:\n    \"\"\"Read a structure from an XYZ file.\"\"\"\n    from .io import read_xyz\n    return _cast_atoms(read_xyz(f), cls)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.read_xsf","title":"read_xsf classmethod","text":"
    read_xsf(f: Union[FileOrPath, XSF]) -> HasAtomsT\n

    Read a structure from an XSF file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xsf(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XSF]) -> HasAtomsT:\n    \"\"\"Read a structure from an XSF file.\"\"\"\n    from .io import read_xsf\n    return _cast_atoms(read_xsf(f), cls)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.read_cfg","title":"read_cfg classmethod","text":"
    read_cfg(f: Union[FileOrPath, CFG]) -> HasAtomsT\n

    Read a structure from a CFG file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cfg(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CFG]) -> HasAtomsT:\n    \"\"\"Read a structure from a CFG file.\"\"\"\n    from .io import read_cfg\n    return _cast_atoms(read_cfg(f), cls)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.read_lmp","title":"read_lmp classmethod","text":"
    read_lmp(\n    f: Union[FileOrPath, LMP],\n    type_map: Optional[Dict[int, Union[str, int]]] = None,\n) -> HasAtomsT\n

    Read a structure from a LAAMPS data file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_lmp(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, LMP], type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> HasAtomsT:\n    \"\"\"Read a structure from a LAAMPS data file.\"\"\"\n    from .io import read_lmp\n    return _cast_atoms(read_lmp(f, type_map=type_map), cls)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.write_cif","title":"write_cif","text":"
    write_cif(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cif(self, f: FileOrPath):\n    from .io import write_cif\n    write_cif(self, f)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.write_xyz","title":"write_xyz","text":"
    write_xyz(f: FileOrPath, fmt: XYZFormat = 'exyz')\n
    Source code in atomlib/mixins.py
    def write_xyz(self, f: FileOrPath, fmt: XYZFormat = 'exyz'):\n    from .io import write_xyz\n    write_xyz(self, f, fmt)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.write_xsf","title":"write_xsf","text":"
    write_xsf(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_xsf(self, f: FileOrPath):\n    from .io import write_xsf\n    write_xsf(self, f)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.write_cfg","title":"write_cfg","text":"
    write_cfg(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cfg(self, f: FileOrPath):\n    from .io import write_cfg\n    write_cfg(self, f)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.write_lmp","title":"write_lmp","text":"
    write_lmp(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_lmp(self, f: FileOrPath):\n    from .io import write_lmp\n    write_lmp(self, f)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.write","title":"write","text":"
    write(path: FileOrPath, ty: FileType)\n
    write(\n    path: Union[str, Path, TextIO], ty: Literal[None] = None\n)\n
    write(path: FileOrPath, ty: Optional[FileType] = None)\n

    Write this structure to a file.

    A file type may be specified using ty. If no ty is specified, it is inferred from the path's extension.

    Source code in atomlib/mixins.py
    def write(self, path: FileOrPath, ty: t.Optional[FileType] = None):\n    \"\"\"\n    Write this structure to a file.\n\n    A file type may be specified using `ty`.\n    If no `ty` is specified, it is inferred from the path's extension.\n    \"\"\"\n    from .io import write\n    write(self, path, ty)  # type: ignore\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.write_mslice","title":"write_mslice","text":"
    write_mslice(\n    f: BinaryFileOrPath,\n    template: Optional[MSliceFile] = None,\n    *,\n    slice_thickness: Optional[float] = None,\n    scan_points: Optional[ArrayLike] = None,\n    scan_extent: Optional[ArrayLike] = None,\n    noise_sigma: Optional[float] = None,\n    conv_angle: Optional[float] = None,\n    energy: Optional[float] = None,\n    defocus: Optional[float] = None,\n    tilt: Optional[Tuple[float, float]] = None,\n    tds: Optional[bool] = None,\n    n_cells: Optional[ArrayLike] = None\n)\n

    Write a structure to an mslice file.

    template may be a file, path, or ElementTree containing an existing mslice file. Its structure will be modified to make the final output. If not specified, a default template will be used.

    Additional options modify simulation properties. If an option is not specified, the template's properties are used.

    Source code in atomlib/mixins.py
    def write_mslice(self, f: BinaryFileOrPath, template: t.Optional[MSliceFile] = None, *,\n             slice_thickness: t.Optional[float] = None,  # angstrom\n             scan_points: t.Optional[ArrayLike] = None,\n             scan_extent: t.Optional[ArrayLike] = None,\n             noise_sigma: t.Optional[float] = None,  # angstrom\n             conv_angle: t.Optional[float] = None,  # mrad\n             energy: t.Optional[float] = None,  # keV\n             defocus: t.Optional[float] = None,  # angstrom\n             tilt: t.Optional[t.Tuple[float, float]] = None,  # (mrad, mrad)\n             tds: t.Optional[bool] = None,\n             n_cells: t.Optional[ArrayLike] = None):\n    \"\"\"\n    Write a structure to an mslice file.\n\n    `template` may be a file, path, or `ElementTree` containing an existing mslice file.\n    Its structure will be modified to make the final output. If not specified, a default\n    template will be used.\n\n    Additional options modify simulation properties. If an option is not specified, the\n    template's properties are used.\n    \"\"\"\n    from .io import write_mslice\n    return write_mslice(self, f, template, slice_thickness=slice_thickness,\n                        scan_points=scan_points, scan_extent=scan_extent,\n                        conv_angle=conv_angle, energy=energy, defocus=defocus,\n                        noise_sigma=noise_sigma, tilt=tilt, tds=tds, n_cells=n_cells)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.write_qe","title":"write_qe","text":"
    write_qe(\n    f: FileOrPath,\n    pseudo: Optional[Mapping[str, str]] = None,\n)\n

    Write a structure to a Quantum Espresso pw.x file.

    PARAMETER DESCRIPTION f

    File or path to write to

    TYPE: FileOrPath

    pseudo

    Mapping from atom symbol

    TYPE: Optional[Mapping[str, str]] DEFAULT: None

    Source code in atomlib/mixins.py
    def write_qe(self, f: FileOrPath, pseudo: t.Optional[t.Mapping[str, str]] = None):\n    \"\"\"\n    Write a structure to a Quantum Espresso pw.x file.\n\n    Args:\n      f: File or path to write to\n      pseudo: Mapping from atom symbol\n    \"\"\"\n    from .io import write_qe\n    write_qe(self, f, pseudo)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.get_cell","title":"get_cell","text":"
    get_cell() -> Cell\n
    Source code in atomlib/atomcell.py
    def get_cell(self) -> Cell:\n    return self.cell\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_cell","title":"with_cell","text":"
    with_cell(cell: Cell) -> Self\n
    Source code in atomlib/atomcell.py
    def with_cell(self, cell: Cell) -> Self:\n    return self.__class__(self.atoms, cell, frame=self.frame, keep_frame=True)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.get_atoms","title":"get_atoms","text":"
    get_atoms(frame: Optional[CoordinateFrame] = None) -> Atoms\n

    Get atoms contained in self, in the given coordinate frame.

    Source code in atomlib/atomcell.py
    def get_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> Atoms:\n    \"\"\"Get atoms contained in ``self``, in the given coordinate frame.\"\"\"\n\n    if frame is None or frame == self.get_frame():\n        return self.atoms\n    return self.atoms.transform(self.get_transform(frame, self.get_frame()))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.with_atoms","title":"with_atoms","text":"
    with_atoms(\n    atoms: HasAtoms, frame: Optional[CoordinateFrame] = None\n) -> Self\n
    Source code in atomlib/atomcell.py
    def with_atoms(self, atoms: HasAtoms, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    frame = frame if frame is not None else self.frame\n    return self.__class__(atoms.get_atoms(), cell=self.cell, frame=frame, keep_frame=True)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.get_frame","title":"get_frame","text":"
    get_frame() -> CoordinateFrame\n

    Get the coordinate frame atoms are stored in.

    Source code in atomlib/atomcell.py
    def get_frame(self) -> CoordinateFrame:\n    \"\"\"Get the coordinate frame atoms are stored in.\"\"\"\n    return self.frame\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.from_ortho","title":"from_ortho classmethod","text":"
    from_ortho(\n    atoms: IntoAtoms,\n    ortho: LinearTransform3D,\n    *,\n    n_cells: Optional[VecLike] = None,\n    frame: CoordinateFrame = \"local\",\n    keep_frame: bool = False\n)\n

    Make an atom cell given a list of atoms and an orthogonalization matrix. Atoms are assumed to be in the coordinate system frame.

    Source code in atomlib/atomcell.py
    @classmethod\ndef from_ortho(cls, atoms: IntoAtoms, ortho: LinearTransform3D, *,\n               n_cells: t.Optional[VecLike] = None,\n               frame: CoordinateFrame = 'local',\n               keep_frame: bool = False):\n    \"\"\"\n    Make an atom cell given a list of atoms and an orthogonalization matrix.\n    Atoms are assumed to be in the coordinate system `frame`.\n    \"\"\"\n    cell = Cell.from_ortho(ortho, n_cells)\n    return cls(atoms, cell, frame=frame, keep_frame=keep_frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.from_unit_cell","title":"from_unit_cell classmethod","text":"
    from_unit_cell(\n    atoms: IntoAtoms,\n    cell_size: VecLike,\n    cell_angle: Optional[VecLike] = None,\n    *,\n    n_cells: Optional[VecLike] = None,\n    frame: CoordinateFrame = \"local\",\n    keep_frame: bool = False\n)\n

    Make a cell given a list of atoms and unit cell parameters. Atoms are assumed to be in the coordinate system frame.

    Source code in atomlib/atomcell.py
    @classmethod\ndef from_unit_cell(cls, atoms: IntoAtoms, cell_size: VecLike,\n                   cell_angle: t.Optional[VecLike] = None, *,\n                   n_cells: t.Optional[VecLike] = None,\n                   frame: CoordinateFrame = 'local',\n                   keep_frame: bool = False):\n    \"\"\"\n    Make a cell given a list of atoms and unit cell parameters.\n    Atoms are assumed to be in the coordinate system `frame`.\n    \"\"\"\n    cell = Cell.from_unit_cell(cell_size, cell_angle, n_cells=n_cells)\n    return cls(atoms, cell, frame=frame, keep_frame=keep_frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.orthogonalize","title":"orthogonalize","text":"
    orthogonalize() -> OrthoCell\n
    Source code in atomlib/atomcell.py
    def orthogonalize(self) -> OrthoCell:\n    if self.is_orthogonal():\n        return OrthoCell(self.atoms, self.cell, frame=self.frame)\n    raise NotImplementedError()\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.clone","title":"clone","text":"
    clone() -> AtomCellT\n

    Make a deep copy of self.

    Source code in atomlib/atomcell.py
    def clone(self: AtomCellT) -> AtomCellT:\n    \"\"\"Make a deep copy of `self`.\"\"\"\n    return self.__class__(**{field.name: copy.deepcopy(getattr(self, field.name)) for field in fields(self)})\n
    "},{"location":"api/atomcell/#atomlib.atomcell.AtomCell.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n

    Assert this structure is equal to other

    Source code in atomlib/atomcell.py
    def assert_equal(self, other: t.Any):\n    \"\"\"Assert this structure is equal to `other`\"\"\"\n    assert isinstance(other, AtomCell)\n    self.cell.assert_equal(other.cell)\n    self.get_atoms('local').assert_equal(other.get_atoms('local'))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell","title":"OrthoCell dataclass","text":"

    Bases: AtomCell

    Source code in atomlib/atomcell.py
    class OrthoCell(AtomCell):\n    def __post_init__(self):\n        if not numpy.allclose(self.cell.cell_angle, numpy.pi/2.):\n            raise ValueError(f\"OrthoCell constructed with non-orthogonal angles: {self.cell.cell_angle}\")\n\n    def is_orthogonal(self, tol: float = 1e-8) -> t.Literal[True]:\n        \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n        return True\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.affine","title":"affine property","text":"
    affine: AffineTransform3D\n

    Affine transformation. Holds transformation from 'ortho' to 'local' coordinates, including rotation away from the standard crystal orientation.

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.ortho","title":"ortho property","text":"
    ortho: LinearTransform3D\n

    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.metric","title":"metric property","text":"
    metric: LinearTransform3D\n

    Cell metric tensor

    Returns the dot product between every combination of basis vectors. :math:\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.cell_size","title":"cell_size property","text":"
    cell_size: NDArray[float64]\n

    Unit cell size.

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.cell_angle","title":"cell_angle property","text":"
    cell_angle: NDArray[float64]\n

    Unit cell angles, in radians.

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.n_cells","title":"n_cells property","text":"
    n_cells: NDArray[int_]\n

    Number of unit cells.

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.pbc","title":"pbc property","text":"
    pbc: NDArray[bool_]\n

    Flags indicating the presence of periodic boundary conditions along each axis.

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.ortho_size","title":"ortho_size property","text":"
    ortho_size: NDArray[float64]\n

    Return size of orthogonal unit cell.

    Equivalent to the diagonal of the orthogonalization matrix.

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.box_size","title":"box_size property","text":"
    box_size: NDArray[float64]\n

    Return size of the cell box.

    Equivalent to self.n_cells * self.cell_size.

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.columns","title":"columns property","text":"
    columns: List[str]\n

    Return the column names in self.

    RETURNS DESCRIPTION List[str]

    A sequence of column names

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.dtypes","title":"dtypes property","text":"
    dtypes: List[DataType]\n

    Return the datatypes in self.

    RETURNS DESCRIPTION List[DataType]

    A sequence of column DataTypes

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.schema","title":"schema property","text":"
    schema: Schema\n

    Return the schema of self.

    RETURNS DESCRIPTION Schema

    A dictionary of column names and DataTypes

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_column","title":"with_column class-attribute instance-attribute","text":"
    with_column = with_columns\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.unique","title":"unique class-attribute instance-attribute","text":"
    unique = deduplicate\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.atoms","title":"atoms instance-attribute","text":"
    atoms: Atoms\n

    Atoms in the cell. Stored in 'local' coordinates (i.e. relative to the enclosing group but not relative to box dimensions).

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.cell","title":"cell instance-attribute","text":"
    cell: Cell\n

    Cell coordinate system.

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.frame","title":"frame class-attribute instance-attribute","text":"
    frame: CoordinateFrame = 'local'\n

    Coordinate frame 'atoms' are stored in.

    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.get_cell","title":"get_cell","text":"
    get_cell() -> Cell\n
    Source code in atomlib/atomcell.py
    def get_cell(self) -> Cell:\n    return self.cell\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_cell","title":"with_cell","text":"
    with_cell(cell: Cell) -> Self\n
    Source code in atomlib/atomcell.py
    def with_cell(self, cell: Cell) -> Self:\n    return self.__class__(self.atoms, cell, frame=self.frame, keep_frame=True)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.get_transform","title":"get_transform","text":"
    get_transform(\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n

    In the two-argument form, get the transform to frame_to from frame_from. In the one-argument form, get the transform from local coordinates to 'frame'.

    Source code in atomlib/cell.py
    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n    \"\"\"\n    In the two-argument form, get the transform to `frame_to` from `frame_from`.\n    In the one-argument form, get the transform from local coordinates to 'frame'.\n    \"\"\"\n    transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n    transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n    if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n        return AffineTransform3D()\n    return transform_to.inverse() @ transform_from\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.corners","title":"corners","text":"
    corners(frame: CoordinateFrame = 'local') -> ndarray\n
    Source code in atomlib/cell.py
    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n    corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n    return self.get_transform(frame, 'cell_box') @ corners\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.bbox_cell","title":"bbox_cell","text":"
    bbox_cell(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the bounding box of the cell box in the given coordinate system.

    Source code in atomlib/cell.py
    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n    return BBox3D.from_pts(self.corners(frame))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.bbox","title":"bbox","text":"
    bbox(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the combined bounding box of the cell and atoms in the given coordinate system. To get the cell or atoms bounding box only, use bbox_cell or bbox_atoms.

    Source code in atomlib/atomcell.py
    def bbox(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"\n    Return the combined bounding box of the cell and atoms in the given coordinate system.\n    To get the cell or atoms bounding box only, use [`bbox_cell`][atomlib.atomcell.HasAtomCell.bbox_cell] or [`bbox_atoms`][atomlib.atomcell.HasAtomCell.bbox_atoms].\n    \"\"\"\n    return self.bbox_atoms(frame) | self.bbox_cell(frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.is_orthogonal_in_local","title":"is_orthogonal_in_local","text":"
    is_orthogonal_in_local(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal and aligned with the local coordinate system.

    Source code in atomlib/cell.py
    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n    transform = (self.affine @ self.ortho).to_linear()\n    if not transform.is_scaled_orthogonal(tol):\n        return False\n    normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n    # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n    return all(\n        any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n        for row in normed\n    )\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.to_ortho","title":"to_ortho","text":"
    to_ortho() -> AffineTransform3D\n
    Source code in atomlib/cell.py
    def to_ortho(self) -> AffineTransform3D:\n    return self.get_transform('local', 'cell_box')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.transform_cell","title":"transform_cell","text":"
    transform_cell(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> Self\n

    Apply the given transform to the unit cell, without changing atom positions. The transform is applied in coordinate frame 'frame'.

    Source code in atomlib/atomcell.py
    def transform_cell(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n    \"\"\"\n    Apply the given transform to the unit cell, without changing atom positions.\n    The transform is applied in coordinate frame 'frame'.\n    \"\"\"\n    return self.with_cell(self.get_cell().transform_cell(transform, frame=frame))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.strain_orthogonal","title":"strain_orthogonal","text":"
    strain_orthogonal() -> HasCellT\n

    Orthogonalize using strain.

    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane. For small displacements, no hydrostatic strain is applied (volume is conserved).

    Source code in atomlib/cell.py
    def strain_orthogonal(self: HasCellT) -> HasCellT:\n    \"\"\"\n    Orthogonalize using strain.\n\n    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n    For small displacements, no hydrostatic strain is applied (volume is conserved).\n    \"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=LinearTransform3D(),\n        cell_size=self.cell_size,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.repeat","title":"repeat","text":"
    repeat(n: Union[int, VecLike]) -> Self\n

    Tile the cell

    Source code in atomlib/atomcell.py
    def repeat(self, n: t.Union[int, VecLike]) -> Self:\n    \"\"\"Tile the cell\"\"\"\n    ns = numpy.broadcast_to(n, 3)\n    if not numpy.issubdtype(ns.dtype, numpy.integer):\n        raise ValueError(\"repeat() argument must be an integer or integer array.\")\n\n    cells = numpy.stack(numpy.meshgrid(*map(numpy.arange, ns))) \\\n        .reshape(3, -1).T.astype(float)\n    cells = cells * self.box_size\n\n    atoms = self.get_atoms('cell')\n    atoms = Atoms.concat([\n        atoms.transform(AffineTransform3D.translate(cell))\n        for cell in cells\n    ]) #.transform(self.cell.get_transform('local', 'cell_frac'))\n    return self.with_atoms(atoms, 'cell').with_cell(self.get_cell().repeat(ns))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.explode","title":"explode","text":"
    explode() -> Self\n

    Materialize repeated cells as one supercell.

    Source code in atomlib/atomcell.py
    def explode(self) -> Self:\n    \"\"\"Materialize repeated cells as one supercell.\"\"\"\n    frame = self.get_frame()\n\n    return self.with_atoms(self.get_atoms('local'), 'local') \\\n        .with_cell(self.get_cell().explode()) \\\n        .to_frame(frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.explode_z","title":"explode_z","text":"
    explode_z() -> HasCellT\n

    Materialize repeated cells as one supercell in z.

    Source code in atomlib/cell.py
    def explode_z(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n        n_cells=[*self.n_cells[:2], 1],\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.crop","title":"crop","text":"
    crop(\n    x_min: float = -inf,\n    x_max: float = inf,\n    y_min: float = -inf,\n    y_max: float = inf,\n    z_min: float = -inf,\n    z_max: float = inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> Self\n

    Crop atoms and cell to the given extents. For a non-orthogonal cell, this must be specified in cell coordinates. This function implicity explodes the cell as well.

    To crop atoms only, use crop_atoms instead.

    Source code in atomlib/atomcell.py
    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n         frame: CoordinateFrame = 'local') -> Self:\n    \"\"\"\n    Crop atoms and cell to the given extents. For a non-orthogonal\n    cell, this must be specified in cell coordinates. This\n    function implicity `explode`s the cell as well.\n\n    To crop atoms only, use `crop_atoms` instead.\n    \"\"\"\n\n    cell = self.get_cell().crop(x_min, x_max, y_min, y_max, z_min, z_max, frame=frame)\n    atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n    return self.with_cell(cell).with_atoms(atoms)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.change_transform","title":"change_transform","text":"
    change_transform(\n    transform: AffineTransform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n

    Coordinate-change a transformation from frame_from into frame_to.

    Source code in atomlib/cell.py
    def change_transform(self, transform: Transform3D,\n                     frame_to: t.Optional[CoordinateFrame] = None,\n                     frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n    \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n    if frame_to == frame_from and frame_to is not None:\n        return transform\n    coord_change = self.get_transform(frame_to, frame_from)\n    return coord_change @ transform @ coord_change.inverse()\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n

    Assert this structure is equal to other

    Source code in atomlib/atomcell.py
    def assert_equal(self, other: t.Any):\n    \"\"\"Assert this structure is equal to `other`\"\"\"\n    assert isinstance(other, AtomCell)\n    self.cell.assert_equal(other.cell)\n    self.get_atoms('local').assert_equal(other.get_atoms('local'))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.get_atoms","title":"get_atoms","text":"
    get_atoms(frame: Optional[CoordinateFrame] = None) -> Atoms\n

    Get atoms contained in self, in the given coordinate frame.

    Source code in atomlib/atomcell.py
    def get_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> Atoms:\n    \"\"\"Get atoms contained in ``self``, in the given coordinate frame.\"\"\"\n\n    if frame is None or frame == self.get_frame():\n        return self.atoms\n    return self.atoms.transform(self.get_transform(frame, self.get_frame()))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_atoms","title":"with_atoms","text":"
    with_atoms(\n    atoms: HasAtoms, frame: Optional[CoordinateFrame] = None\n) -> Self\n
    Source code in atomlib/atomcell.py
    def with_atoms(self, atoms: HasAtoms, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    frame = frame if frame is not None else self.frame\n    return self.__class__(atoms.get_atoms(), cell=self.cell, frame=frame, keep_frame=True)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.describe","title":"describe","text":"
    describe(\n    percentiles: Union[Sequence[float], float, None] = (\n        0.25,\n        0.5,\n        0.75,\n    ),\n    *,\n    interpolation: RollingInterpolationMethod = \"nearest\",\n    frame: Optional[CoordinateFrame] = None\n) -> DataFrame\n

    Return summary statistics for self. See DataFrame.describe for more information.

    PARAMETER DESCRIPTION percentiles

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), 50% (median), and 75% (third quartile).

    TYPE: Union[Sequence[float], float, None] DEFAULT: (0.25, 0.5, 0.75)

    RETURNS DESCRIPTION DataFrame

    A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n             interpolation: RollingInterpolationMethod = 'nearest',\n             frame: t.Optional[CoordinateFrame] = None) -> polars.DataFrame:\n    \"\"\"\n    Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n    Args:\n      percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                   50% (median), and 75% (third quartile).\n\n    Returns:\n      A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_columns","title":"with_columns","text":"
    with_columns(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self\n

    Return a copy of self with the given columns added.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_columns(self,\n                 *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 frame: t.Optional[CoordinateFrame] = None,\n                 **named_exprs: IntoExpr) -> Self:\n    \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.insert_column","title":"insert_column","text":"
    insert_column(index: int, column: Series) -> DataFrame\n
    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n    return self._get_frame().insert_column(index, column)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.get_column","title":"get_column","text":"
    get_column(\n    name: str, *, frame: Optional[CoordinateFrame] = None\n) -> Series\n

    Get the specified column from self, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef get_column(self, name: str, *, frame: t.Optional[CoordinateFrame] = None) -> polars.Series:\n    \"\"\"\n    Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.get_columns","title":"get_columns","text":"
    get_columns(\n    *, frame: Optional[CoordinateFrame] = None\n) -> List[Series]\n

    Return all columns from self as a list of Series.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef get_columns(self, *, frame: t.Optional[CoordinateFrame] = None) -> t.List[polars.Series]:\n    \"\"\"\n    Return all columns from `self` as a list of [`Series`][polars.Series].\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.get_column_index","title":"get_column_index","text":"
    get_column_index(name: str) -> int\n

    Get the index of a column by name, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_column_index)\ndef get_column_index(self, name: str) -> int:\n    \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.group_by","title":"group_by","text":"
    group_by(\n    *by: Union[IntoExpr, Iterable[IntoExpr]],\n    maintain_order: bool = False,\n    frame: Optional[CoordinateFrame] = None,\n    **named_by: IntoExpr\n) -> GroupBy\n

    Start a group by operation. See DataFrame.group_by for more information.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n             maintain_order: bool = False, frame: t.Optional[CoordinateFrame] = None,\n             **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n    \"\"\"\n    Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.pipe","title":"pipe","text":"
    pipe(\n    function: Callable[Concatenate[HasAtomCellT, P], T],\n    *args: args,\n    **kwargs: kwargs\n) -> T\n

    Apply function to self (in method-call syntax).

    Source code in atomlib/atomcell.py
    def pipe(self: HasAtomCellT, function: t.Callable[Concatenate[HasAtomCellT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n    \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n    return function(self, *args, **kwargs)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.clone","title":"clone","text":"
    clone() -> AtomCellT\n

    Make a deep copy of self.

    Source code in atomlib/atomcell.py
    def clone(self: AtomCellT) -> AtomCellT:\n    \"\"\"Make a deep copy of `self`.\"\"\"\n    return self.__class__(**{field.name: copy.deepcopy(getattr(self, field.name)) for field in fields(self)})\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.drop","title":"drop","text":"
    drop(\n    *columns: Union[str, Iterable[str]], strict: bool = True\n) -> DataFrame\n

    Return self with the specified columns removed.

    Source code in atomlib/atoms.py
    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n    \"\"\"Return `self` with the specified columns removed.\"\"\"\n    return self._get_frame().drop(*columns, strict=strict)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.filter","title":"filter","text":"
    filter(\n    *predicates: Union[\n        None,\n        IntoExprColumn,\n        Iterable[IntoExprColumn],\n        bool,\n        List[bool],\n        ndarray,\n    ],\n    frame: Optional[CoordinateFrame] = None,\n    **constraints: Any\n) -> Self\n

    Filter self, removing rows which evaluate to False.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef filter(\n    self,\n    *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n    frame: t.Optional[CoordinateFrame] = None,\n    **constraints: t.Any,\n) -> Self:\n    \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.sort","title":"sort","text":"
    sort(\n    by: Union[IntoExpr, Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: Union[bool, Sequence[bool]] = False,\n    nulls_last: bool = False\n) -> Self\n

    Sort the atoms in self by the given columns/expressions.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef sort(\n    self,\n    by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: t.Union[bool, t.Sequence[bool]] = False,\n    nulls_last: bool = False,\n) -> Self:\n    \"\"\"\n    Sort the atoms in `self` by the given columns/expressions.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.slice","title":"slice","text":"
    slice(\n    offset: int,\n    length: Optional[int] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return a slice of the rows in self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef slice(self, offset: int, length: t.Optional[int] = None, *,\n          frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return a slice of the rows in `self`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.head","title":"head","text":"
    head(\n    n: int = 5, *, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return the first n rows of self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef head(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return the first `n` rows of `self`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.tail","title":"tail","text":"
    tail(\n    n: int = 5, *, frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return the last n rows of self.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef tail(self, n: int = 5, *, frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"Return the last `n` rows of `self`.\"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.drop_nulls","title":"drop_nulls","text":"
    drop_nulls(\n    subset: Union[str, Collection[str], None] = None\n) -> DataFrame\n

    Drop rows that contain nulls in any of columns subset.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n    \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n    return self._get_frame().drop_nulls(subset)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.fill_null","title":"fill_null","text":"
    fill_null(\n    value: Any = None,\n    strategy: Optional[FillNullStrategy] = None,\n    limit: Optional[int] = None,\n    matches_supertype: bool = True,\n) -> Self\n
    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef fill_null(\n    self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n    limit: t.Optional[int] = None, matches_supertype: bool = True,\n) -> Self:\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.fill_nan","title":"fill_nan","text":"
    fill_nan(\n    value: Union[Expr, int, float, None],\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n
    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef fill_nan(self, value: t.Union[polars.Expr, int, float, None], *,\n             frame: t.Optional[CoordinateFrame] = None) -> Self:\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.concat","title":"concat classmethod","text":"
    concat(\n    atoms: Union[\n        HasAtomsT,\n        IntoAtoms,\n        Iterable[Union[HasAtomsT, IntoAtoms]],\n    ],\n    *,\n    rechunk: bool = True,\n    how: ConcatMethod = \"vertical\"\n) -> HasAtomsT\n

    Concatenate multiple Atoms together, handling metadata appropriately.

    Source code in atomlib/atoms.py
    @classmethod\ndef concat(cls: t.Type[HasAtomsT],\n           atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n           rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n    \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n    # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n    # same type as ``cls``.\n    if _is_abstract(cls):\n        raise TypeError(\"concat() must be called on a concrete class.\")\n\n    if isinstance(atoms, HasAtoms):\n        atoms = (atoms,)\n    dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n    representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n    if len(dfs) == 0:\n        return representative.with_atoms(Atoms.empty(), 'local')\n\n    if how in ('vertical', 'vertical_relaxed'):\n        # get order from first member\n        cols = dfs[0].columns\n        dfs = [df.select(cols) for df in dfs]\n    elif how == 'inner':\n        cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n        schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n        if len(schema) == 0:\n            raise ValueError(\"Atoms have no columns in common\")\n\n        dfs = [_select_schema(df, schema) for df in dfs]\n        how = 'vertical'\n\n    return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.partition_by","title":"partition_by","text":"
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: Literal[False] = False\n) -> List[Self]\n
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: Literal[True] = ...\n) -> Dict[Any, Self]\n
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: bool = False\n) -> Union[List[Self], Dict[Any, Self]]\n

    Group by the given columns and partition into separate dataframes.

    Return the partitions as a dictionary by specifying as_dict=True.

    Source code in atomlib/atoms.py
    def partition_by(\n    self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n    maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n    \"\"\"\n    Group by the given columns and partition into separate dataframes.\n\n    Return the partitions as a dictionary by specifying `as_dict=True`.\n    \"\"\"\n    if as_dict:\n        d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n        return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n    return [\n        self.with_atoms(Atoms(df, _unchecked=True))\n        for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n    ]\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.select","title":"select","text":"
    select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef select(\n    self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> polars.DataFrame:\n    \"\"\"\n    Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.select_schema","title":"select_schema","text":"
    select_schema(schema: SchemaDict) -> DataFrame\n

    Select columns from self and cast to the given schema. Raises TypeError if a column is not found or if it can't be cast.

    Source code in atomlib/atoms.py
    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n    \"\"\"\n    Select columns from `self` and cast to the given schema.\n    Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n    \"\"\"\n    return _select_schema(self, schema)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.select_props","title":"select_props","text":"
    select_props(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self\n

    Select exprs from self, while keeping required columns. Doesn't affect the cell.

    RETURNS DESCRIPTION Self

    A HasAtomCell filtered to contain

    Self

    the specified properties (as well as required columns).

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef select_props(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Self:\n    \"\"\"\n    Select `exprs` from `self`, while keeping required columns.\n    Doesn't affect the cell.\n\n    Returns:\n      A [`HasAtomCell`][atomlib.atomcell.HasAtomCell] filtered to contain\n      the specified properties (as well as required columns).\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.try_select","title":"try_select","text":"
    try_select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    frame: Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> Optional[DataFrame]\n

    Try to select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns. Returns None if any columns are missing.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef try_select(\n    self, *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    frame: t.Optional[CoordinateFrame] = None,\n    **named_exprs: IntoExpr\n) -> t.Optional[polars.DataFrame]:\n    \"\"\"\n    Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns. Returns `None` if any\n    columns are missing.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.try_get_column","title":"try_get_column","text":"
    try_get_column(name: str) -> Optional[Series]\n

    Try to get a column from self, returning None if it doesn't exist.

    Source code in atomlib/atoms.py
    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n    \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n    try:\n        return self.get_column(name)\n    except polars.exceptions.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.bbox_atoms","title":"bbox_atoms","text":"
    bbox_atoms(\n    frame: Optional[CoordinateFrame] = None,\n) -> BBox3D\n

    Return the bounding box of all the atoms in self, in the given coordinate frame.

    Source code in atomlib/atomcell.py
    def bbox_atoms(self, frame: t.Optional[CoordinateFrame] = None) -> BBox3D:\n    \"\"\"Return the bounding box of all the atoms in `self`, in the given coordinate frame.\"\"\"\n    return self.get_atoms(frame).bbox()\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.transform_atoms","title":"transform_atoms","text":"
    transform_atoms(\n    transform: IntoTransform3D,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: CoordinateFrame = \"local\",\n    transform_velocities: bool = False\n) -> Self\n

    Transform the atoms in self by transform. If selection is given, only transform the atoms in selection.

    Source code in atomlib/atomcell.py
    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                    frame: CoordinateFrame = 'local', transform_velocities: bool = False) -> Self:\n    \"\"\"\n    Transform the atoms in `self` by `transform`.\n    If `selection` is given, only transform the atoms in `selection`.\n    \"\"\"\n    transform = self.change_transform(Transform3D.make(transform), self.get_frame(), frame)\n    return self.with_atoms(self.get_atoms(self.get_frame()).transform(transform, selection, transform_velocities=transform_velocities))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.transform","title":"transform","text":"
    transform(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> Self\n
    Source code in atomlib/atomcell.py
    def transform(self, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> Self:\n    if isinstance(transform, Transform3D) and not isinstance(transform, AffineTransform3D):\n        raise ValueError(\"Non-affine transforms cannot change the box dimensions. Use 'transform_atoms' instead.\")\n    # TODO: cleanup once tests pass\n    # coordinate change the transform into atomic coordinates\n    new_cell = self.get_cell().transform_cell(transform, frame)\n    transform = self.get_cell().change_transform(transform, self.get_frame(), frame)\n    return self.with_atoms(self.get_atoms().transform(transform), self.get_frame()).with_cell(new_cell)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.round_near_zero","title":"round_near_zero","text":"
    round_near_zero(\n    tol: float = 1e-14,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Round atom position values near zero to zero.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef round_near_zero(self, tol: float = 1e-14, *,\n                    frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Round atom position values near zero to zero.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.crop_atoms","title":"crop_atoms","text":"
    crop_atoms(\n    x_min: float = -inf,\n    x_max: float = inf,\n    y_min: float = -inf,\n    y_max: float = inf,\n    z_min: float = -inf,\n    z_max: float = inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> Self\n
    Source code in atomlib/atomcell.py
    def crop_atoms(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n               y_min: float = -numpy.inf, y_max: float = numpy.inf,\n               z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n               frame: CoordinateFrame = 'local') -> Self:\n    atoms = self._transform_atoms_in_frame(frame, lambda atoms: atoms.crop_atoms(x_min, x_max, y_min, y_max, z_min, z_max))\n    return self.with_atoms(atoms)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.deduplicate","title":"deduplicate","text":"
    deduplicate(\n    tol: float = 0.001,\n    subset: Iterable[str] = (\"x\", \"y\", \"z\", \"symbol\"),\n    keep: UniqueKeepStrategy = \"first\",\n    maintain_order: bool = True,\n) -> Self\n

    De-duplicate atoms in self. Atoms of the same symbol that are closer than tolerance to each other (by Euclidian distance) will be removed, leaving only the atom specified by keep (defaults to the first atom).

    If subset is specified, only those columns will be included while assessing duplicates. Floating point columns other than 'x', 'y', and 'z' will not by toleranced.

    Source code in atomlib/atoms.py
    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n    \"\"\"\n    De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n    to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n    `keep` (defaults to the first atom).\n\n    If `subset` is specified, only those columns will be included while assessing duplicates.\n    Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n    \"\"\"\n    import scipy.spatial\n\n    cols = set((subset,) if isinstance(subset, str) else subset)\n\n    indices = numpy.arange(len(self))\n\n    spatial_cols = cols.intersection(('x', 'y', 'z'))\n    cols -= spatial_cols\n    if len(spatial_cols) > 0:\n        coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n        tree = scipy.spatial.KDTree(coords)\n\n        # TODO This is a bad algorithm\n        while True:\n            changed = False\n            for (i, j) in tree.query_pairs(tol, 2.):\n                # whenever we encounter a pair, ensure their index matches\n                i_i, i_j = indices[[i, j]]\n                if i_i != i_j:\n                    indices[i] = indices[j] = min(i_i, i_j)\n                    changed = True\n            if not changed:\n                break\n\n        self = self.with_column(polars.Series('_unique_pts', indices))\n        cols.add('_unique_pts')\n\n    frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n    if len(spatial_cols) > 0:\n        frame = frame.drop('_unique_pts')\n\n    return self.with_atoms(Atoms(frame, _unchecked=True))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_bounds","title":"with_bounds","text":"
    with_bounds(\n    cell_size: Optional[VecLike] = None,\n    cell_origin: Optional[VecLike] = None,\n) -> \"AtomCell\"\n

    Return a periodic cell with the given orthogonal cell dimensions.

    If cell_size is not specified, it will be assumed (and may be incorrect).

    Source code in atomlib/atoms.py
    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n    \"\"\"\n    Return a periodic cell with the given orthogonal cell dimensions.\n\n    If cell_size is not specified, it will be assumed (and may be incorrect).\n    \"\"\"\n    # TODO: test this\n    from .atomcell import AtomCell\n\n    if cell_size is None:\n        warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n        cell_size = self.bbox().size\n        cell_origin = self.bbox().min\n\n    # TODO test this origin code\n    cell = Cell.from_unit_cell(cell_size)\n    if cell_origin is not None:\n        cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n    return AtomCell(self.get_atoms(), cell, frame='local')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.coords","title":"coords","text":"
    coords(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> NDArray[float64]\n

    Return a (N, 3) ndarray of atom positions (dtype numpy.float64) in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> NDArray[numpy.float64]:\n    \"\"\"\n    Return a `(N, 3)` ndarray of atom positions (dtype [`numpy.float64`][numpy.float64])\n    in the given coordinate frame.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.x","title":"x","text":"
    x() -> Expr\n
    Source code in atomlib/atoms.py
    def x(self) -> polars.Expr:\n    return polars.col('coords').arr.get(0).alias('x')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.y","title":"y","text":"
    y() -> Expr\n
    Source code in atomlib/atoms.py
    def y(self) -> polars.Expr:\n    return polars.col('coords').arr.get(1).alias('y')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.z","title":"z","text":"
    z() -> Expr\n
    Source code in atomlib/atoms.py
    def z(self) -> polars.Expr:\n    return polars.col('coords').arr.get(2).alias('z')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.velocities","title":"velocities","text":"
    velocities(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Optional[NDArray[float64]]\n

    Return a (N, 3) ndarray of atom velocities (dtype numpy.float64) in the given coordinate frame.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_get\ndef velocities(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Optional[CoordinateFrame] = None) -> t.Optional[NDArray[numpy.float64]]:\n    \"\"\"\n    Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64])\n    in the given coordinate frame.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.types","title":"types","text":"
    types() -> Optional[Series]\n

    Returns a Series of atom types (dtype polars.Int32).

    Source code in atomlib/atoms.py
    def types(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('type')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.masses","title":"masses","text":"
    masses() -> Optional[Series]\n

    Returns a Series of atom masses (dtype polars.Float32).

    Source code in atomlib/atoms.py
    def masses(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('mass')\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.add_atom","title":"add_atom","text":"
    add_atom(\n    elem: Union[int, str],\n    x: ArrayLike,\n    /,\n    *,\n    y: None = None,\n    z: None = None,\n    frame: Optional[CoordinateFrame] = None,\n    **kwargs: Any,\n) -> Self\n
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: float,\n    y: float,\n    z: float,\n    *,\n    frame: Optional[CoordinateFrame] = None,\n    **kwargs: Any,\n) -> Self\n
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: Union[ArrayLike, float],\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None,\n    **kwargs: Any,\n) -> Self\n

    Return a copy of self with an extra atom.

    By default, all extra columns present in self must be specified as **kwargs.

    Try to avoid calling this in a loop (Use concat instead).

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef add_atom(self, elem: t.Union[int, str], /,  # type: ignore (spurious)\n             x: t.Union[ArrayLike, float],\n             y: t.Optional[float] = None,\n             z: t.Optional[float] = None, *,\n             frame: t.Optional[CoordinateFrame] = None,\n             **kwargs: t.Any) -> Self:\n    \"\"\"\n    Return a copy of `self` with an extra atom.\n\n    By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n    Try to avoid calling this in a loop (Use [`concat`][atomlib.atomcell.HasAtomCell.concat] instead).\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.pos","title":"pos","text":"
    pos(\n    x: Sequence[Optional[float]],\n    /,\n    *,\n    y: None = None,\n    z: None = None,\n    tol: float = 1e-06,\n    **kwargs: Any,\n) -> Expr\n
    pos(\n    x: Optional[float] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n
    pos(\n    x: Union[Sequence[Optional[float]], float, None] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n

    Select all atoms at a given position.

    Formally, returns all atoms within a cube of radius tol centered at (x,y,z), exclusive of the cube's surface.

    Additional parameters given as kwargs will be checked as additional parameters (with strict equality).

    Source code in atomlib/atoms.py
    def pos(self,\n        x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n        y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n        tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n    \"\"\"\n    Select all atoms at a given position.\n\n    Formally, returns all atoms within a cube of radius ``tol``\n    centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n    Additional parameters given as ``kwargs`` will be checked\n    as additional parameters (with strict equality).\n    \"\"\"\n\n    if isinstance(x, t.Sequence):\n        (x, y, z) = x\n\n    tol = abs(float(tol))\n    selection = polars.lit(True)\n    if x is not None:\n        selection &= self.x().is_between(x - tol, x + tol, closed='none')\n    if y is not None:\n        selection &= self.y().is_between(y - tol, y + tol, closed='none')\n    if z is not None:\n        selection &= self.z().is_between(z - tol, z + tol, closed='none')\n    for (col, val) in kwargs.items():\n        selection &= (polars.col(col) == val)\n\n    return selection\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_index","title":"with_index","text":"
    with_index(\n    index: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Returns self with a row index added in column 'i' (dtype polars.Int64). If index is not specified, defaults to an existing index or a new index.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_index(self, index: t.Optional[AtomValues] = None, *,\n               frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n    If `index` is not specified, defaults to an existing index or a new index.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_wobble","title":"with_wobble","text":"
    with_wobble(\n    wobble: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given displacements in column 'wobble' (dtype polars.Float64). If wobble is not specified, defaults to the already-existing wobbles or 0.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_wobble(self, wobble: t.Optional[AtomValues] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_occupancy","title":"with_occupancy","text":"
    with_occupancy(\n    frac_occupancy: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given fractional occupancies (dtype polars.Float64). If frac_occupancy is not specified, defaults to the already-existing occupancies or 1.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None, *,\n                   frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.apply_wobble","title":"apply_wobble","text":"
    apply_wobble(\n    rng: Union[Generator, int, None] = None,\n    frame: Optional[CoordinateFrame] = None,\n) -> Self\n

    Displace the atoms in self by the amount in the wobble column. wobble is interpretated as a mean-squared displacement, which is distributed equally over each axis.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None,\n                 frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Displace the atoms in `self` by the amount in the `wobble` column.\n    `wobble` is interpretated as a mean-squared displacement, which is distributed\n    equally over each axis.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.apply_occupancy","title":"apply_occupancy","text":"
    apply_occupancy(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    For each atom in self, use its frac_occupancy to randomly decide whether to remove it.

    Source code in atomlib/atoms.py
    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n    \"\"\"\n    if 'frac_occupancy' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    frac = self.select('frac_occupancy').to_series().to_numpy()\n    choice = rng.binomial(1, frac).astype(numpy.bool_)\n    return self.filter(polars.lit(choice))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_type","title":"with_type","text":"
    with_type(\n    types: Optional[AtomValues] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atom types in column 'type'. If types is not specified, use the already existing types or auto-assign them.

    When auto-assigning, each symbol is given a unique value, case-sensitive. Values are assigned from lowest atomic number to highest. For instance: [\"Ag+\", \"Na\", \"H\", \"Ag\"] => [3, 11, 1, 2]

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_type(self, types: t.Optional[AtomValues] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom types in column 'type'.\n    If `types` is not specified, use the already existing types or auto-assign them.\n\n    When auto-assigning, each symbol is given a unique value, case-sensitive.\n    Values are assigned from lowest atomic number to highest.\n    For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_mass","title":"with_mass","text":"
    with_mass(\n    mass: Optional[ArrayLike] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atom masses in column 'mass'. If mass is not specified, use the already existing masses or auto-assign them.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_mass(self, mass: t.Optional[ArrayLike] = None, *,\n              frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom masses in column `'mass'`.\n    If `mass` is not specified, use the already existing masses or auto-assign them.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_symbol","title":"with_symbol","text":"
    with_symbol(\n    symbols: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self with the given atomic symbols.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atomic symbols.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_coords","title":"with_coords","text":"
    with_coords(\n    pts: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self replaced with the given atomic positions.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *,\n                frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic positions.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.with_velocity","title":"with_velocity","text":"
    with_velocity(\n    pts: Optional[ArrayLike] = None,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Optional[CoordinateFrame] = None\n) -> Self\n

    Return self replaced with the given atomic velocities. If pts is not specified, use the already existing velocities or zero.

    Source code in atomlib/atomcell.py
    @_fwd_atoms_transform\ndef with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                  selection: t.Optional[AtomSelection] = None, *,\n                  frame: t.Optional[CoordinateFrame] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic velocities.\n    If `pts` is not specified, use the already existing velocities or zero.\n    \"\"\"\n    ...\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.get_frame","title":"get_frame","text":"
    get_frame() -> CoordinateFrame\n

    Get the coordinate frame atoms are stored in.

    Source code in atomlib/atomcell.py
    def get_frame(self) -> CoordinateFrame:\n    \"\"\"Get the coordinate frame atoms are stored in.\"\"\"\n    return self.frame\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.get_atomcell","title":"get_atomcell","text":"
    get_atomcell() -> AtomCell\n
    Source code in atomlib/atomcell.py
    def get_atomcell(self) -> AtomCell:\n    frame = self.get_frame()\n    return AtomCell(self.get_atoms(frame), self.get_cell(), frame=frame, keep_frame=True)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.to_frame","title":"to_frame","text":"
    to_frame(frame: CoordinateFrame) -> Self\n

    Convert the stored Atoms to the given coordinate frame.

    Source code in atomlib/atomcell.py
    def to_frame(self, frame: CoordinateFrame) -> Self:\n    \"\"\"Convert the stored Atoms to the given coordinate frame.\"\"\"\n    return self.with_atoms(self.get_atoms(frame), frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.crop_to_box","title":"crop_to_box","text":"
    crop_to_box(eps: float = 1e-05) -> Self\n
    Source code in atomlib/atomcell.py
    def crop_to_box(self, eps: float = 1e-5) -> Self:\n    atoms = self._transform_atoms_in_frame('cell_box', lambda atoms: atoms.crop_atoms(*([-eps, 1-eps]*3)))\n    return self.with_atoms(atoms)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.wrap","title":"wrap","text":"
    wrap(eps: float = 1e-05) -> Self\n

    Wrap atoms around the cell boundaries.

    Source code in atomlib/atomcell.py
    def wrap(self, eps: float = 1e-5) -> Self:\n    \"\"\"Wrap atoms around the cell boundaries.\"\"\"\n    return self.with_atoms(self._transform_atoms_in_frame('cell_box', lambda a: a._wrap(eps)))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.repeat_to","title":"repeat_to","text":"
    repeat_to(\n    size: VecLike, crop: Union[bool, Sequence[bool]] = False\n) -> Self\n

    Repeat the cell so it is at least size along the crystal's axes.

    If crop, then crop the cell to exactly size. This may break periodicity. crop may be a vector, in which case you can specify cropping only along some axes.

    Source code in atomlib/atomcell.py
    def repeat_to(self, size: VecLike, crop: t.Union[bool, t.Sequence[bool]] = False) -> Self:\n    \"\"\"\n    Repeat the cell so it is at least `size` along the crystal's axes.\n\n    If `crop`, then crop the cell to exactly `size`. This may break periodicity.\n    `crop` may be a vector, in which case you can specify cropping only along some axes.\n    \"\"\"\n    size = to_vec3(size)\n    cell_size = self.cell_size * self.n_cells\n    repeat = numpy.maximum(numpy.ceil(size / cell_size).astype(int), 1)\n    atom_cell = self.repeat(repeat)\n\n    crop_v = to_vec3(crop, dtype=numpy.bool_)\n    if numpy.any(crop_v):\n        crop_x, crop_y, crop_z = crop_v\n        return atom_cell.crop(\n            x_max = size[0] if crop_x else numpy.inf,\n            y_max = size[1] if crop_y else numpy.inf,\n            z_max = size[2] if crop_z else numpy.inf,\n            frame='cell'\n        )\n\n    return atom_cell\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.repeat_x","title":"repeat_x","text":"
    repeat_x(n: int) -> Self\n

    Tile the cell in the x axis.

    Source code in atomlib/atomcell.py
    def repeat_x(self, n: int) -> Self:\n    \"\"\"Tile the cell in the x axis.\"\"\"\n    return self.repeat((n, 1, 1))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.repeat_y","title":"repeat_y","text":"
    repeat_y(n: int) -> Self\n

    Tile the cell in the y axis.

    Source code in atomlib/atomcell.py
    def repeat_y(self, n: int) -> Self:\n    \"\"\"Tile the cell in the y axis.\"\"\"\n    return self.repeat((1, n, 1))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.repeat_z","title":"repeat_z","text":"
    repeat_z(n: int) -> Self\n

    Tile the cell in the z axis.

    Source code in atomlib/atomcell.py
    def repeat_z(self, n: int) -> Self:\n    \"\"\"Tile the cell in the z axis.\"\"\"\n    return self.repeat((1, 1, n))\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.repeat_to_x","title":"repeat_to_x","text":"
    repeat_to_x(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the x axis.

    Source code in atomlib/atomcell.py
    def repeat_to_x(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the x axis.\"\"\"\n    return self.repeat_to([size, 0., 0.], [crop, False, False])\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.repeat_to_y","title":"repeat_to_y","text":"
    repeat_to_y(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the y axis.

    Source code in atomlib/atomcell.py
    def repeat_to_y(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the y axis.\"\"\"\n    return self.repeat_to([0., size, 0.], [False, crop, False])\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.repeat_to_z","title":"repeat_to_z","text":"
    repeat_to_z(size: float, crop: bool = False) -> Self\n

    Repeat the cell so it is at least size size along the z axis.

    Source code in atomlib/atomcell.py
    def repeat_to_z(self, size: float, crop: bool = False) -> Self:\n    \"\"\"Repeat the cell so it is at least size `size` along the z axis.\"\"\"\n    return self.repeat_to([0., 0., size], [False, False, crop])\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.repeat_to_aspect","title":"repeat_to_aspect","text":"
    repeat_to_aspect(\n    plane: Literal[\"xy\", \"xz\", \"yz\"] = \"xy\",\n    *,\n    aspect: float = 1.0,\n    min_size: Optional[VecLike] = None,\n    max_size: Optional[VecLike] = None\n) -> Self\n

    Repeat to optimize the aspect ratio in plane, while staying above min_size and under max_size.

    Source code in atomlib/atomcell.py
    def repeat_to_aspect(self, plane: t.Literal['xy', 'xz', 'yz'] = 'xy', *,\n                     aspect: float = 1., min_size: t.Optional[VecLike] = None,\n                     max_size: t.Optional[VecLike] = None) -> Self:\n    \"\"\"\n    Repeat to optimize the aspect ratio in `plane`,\n    while staying above `min_size` and under `max_size`.\n    \"\"\"\n    if min_size is None:\n        min_n = numpy.array([1, 1, 1], numpy.int_)\n    else:\n        min_n = numpy.maximum(numpy.ceil(to_vec3(min_size) / self.box_size), 1).astype(numpy.int_)\n\n    if max_size is None:\n        max_n = 3 * min_n\n    else:\n        max_n = numpy.maximum(numpy.floor(to_vec3(max_size) / self.box_size), 1).astype(numpy.int_)\n\n    if plane == 'xy':\n        indices = [0, 1]\n    elif plane == 'xz':\n        indices = [0, 2]\n    elif plane == 'yz':\n        indices = [1, 2]\n    else:\n        raise ValueError(f\"Invalid plane '{plane}'. Exepcted 'xy', 'xz', 'or 'yz'.\")\n\n    na = numpy.arange(min_n[indices[0]], max_n[indices[0]])\n    nb = numpy.arange(min_n[indices[1]], max_n[indices[1]])\n    (na, nb) = numpy.meshgrid(na, nb)\n\n    aspects = na * self.box_size[indices[0]] / (nb * self.box_size[indices[1]])\n    # cost function: log(aspect)^2  (so cost(0.5) == cost(2))\n    min_i = numpy.argmin(numpy.log(aspects / aspect)**2)\n    repeat = numpy.array([1, 1, 1], numpy.int_)\n    repeat[indices] = na.flatten()[min_i], nb.flatten()[min_i]\n    return self.repeat(repeat)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.periodic_duplicate","title":"periodic_duplicate","text":"
    periodic_duplicate(eps: float = 1e-05) -> Self\n

    Add duplicate copies of atoms near periodic boundaries.

    For instance, an atom at a corner will be duplicated into 8 copies. This is mostly only useful for visualization.

    Source code in atomlib/atomcell.py
    def periodic_duplicate(self, eps: float = 1e-5) -> Self:\n    \"\"\"\n    Add duplicate copies of atoms near periodic boundaries.\n\n    For instance, an atom at a corner will be duplicated into 8 copies.\n    This is mostly only useful for visualization.\n    \"\"\"\n    frame_save = self.get_frame()\n    self = self.to_frame('cell_box').wrap(eps=eps)\n\n    for i in range(3):\n        self = self.concat((self,\n            self.filter(polars.col('coords').arr.get(i).abs() <= eps, frame='cell_box')\n                .transform_atoms(AffineTransform3D.translate([1. if i == j else 0. for j in range(3)]), frame='cell_box')\n        ))\n\n    return self.to_frame(frame_save)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.read","title":"read classmethod","text":"
    read(path: FileOrPath, ty: FileType) -> HasAtomsT\n
    read(\n    path: Union[str, Path, TextIO], ty: Literal[None] = None\n) -> HasAtomsT\n
    read(\n    path: FileOrPath, ty: Optional[FileType] = None\n) -> HasAtomsT\n

    Read a structure from a file.

    Supported types can be found in the io module. If no ty is specified, it is inferred from the file's extension.

    Source code in atomlib/mixins.py
    @classmethod\ndef read(cls: t.Type[HasAtomsT], path: FileOrPath, ty: t.Optional[FileType] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a file.\n\n    Supported types can be found in the [io][atomlib.io] module.\n    If no `ty` is specified, it is inferred from the file's extension.\n    \"\"\"\n    from .io import read\n    return _cast_atoms(read(path, ty), cls)  # type: ignore\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.read_cif","title":"read_cif classmethod","text":"
    read_cif(\n    f: Union[FileOrPath, CIF, CIFDataBlock],\n    block: Union[int, str, None] = None,\n) -> HasAtomsT\n

    Read a structure from a CIF file.

    If block is specified, read data from the given block of the CIF file (index or name).

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cif(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CIF, CIFDataBlock], block: t.Union[int, str, None] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a CIF file.\n\n    If `block` is specified, read data from the given block of the CIF file (index or name).\n    \"\"\"\n    from .io import read_cif\n    return _cast_atoms(read_cif(f, block), cls)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.read_xyz","title":"read_xyz classmethod","text":"
    read_xyz(f: Union[FileOrPath, XYZ]) -> HasAtomsT\n

    Read a structure from an XYZ file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xyz(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XYZ]) -> HasAtomsT:\n    \"\"\"Read a structure from an XYZ file.\"\"\"\n    from .io import read_xyz\n    return _cast_atoms(read_xyz(f), cls)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.read_xsf","title":"read_xsf classmethod","text":"
    read_xsf(f: Union[FileOrPath, XSF]) -> HasAtomsT\n

    Read a structure from an XSF file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xsf(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XSF]) -> HasAtomsT:\n    \"\"\"Read a structure from an XSF file.\"\"\"\n    from .io import read_xsf\n    return _cast_atoms(read_xsf(f), cls)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.read_cfg","title":"read_cfg classmethod","text":"
    read_cfg(f: Union[FileOrPath, CFG]) -> HasAtomsT\n

    Read a structure from a CFG file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cfg(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CFG]) -> HasAtomsT:\n    \"\"\"Read a structure from a CFG file.\"\"\"\n    from .io import read_cfg\n    return _cast_atoms(read_cfg(f), cls)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.read_lmp","title":"read_lmp classmethod","text":"
    read_lmp(\n    f: Union[FileOrPath, LMP],\n    type_map: Optional[Dict[int, Union[str, int]]] = None,\n) -> HasAtomsT\n

    Read a structure from a LAAMPS data file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_lmp(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, LMP], type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> HasAtomsT:\n    \"\"\"Read a structure from a LAAMPS data file.\"\"\"\n    from .io import read_lmp\n    return _cast_atoms(read_lmp(f, type_map=type_map), cls)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.write_cif","title":"write_cif","text":"
    write_cif(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cif(self, f: FileOrPath):\n    from .io import write_cif\n    write_cif(self, f)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.write_xyz","title":"write_xyz","text":"
    write_xyz(f: FileOrPath, fmt: XYZFormat = 'exyz')\n
    Source code in atomlib/mixins.py
    def write_xyz(self, f: FileOrPath, fmt: XYZFormat = 'exyz'):\n    from .io import write_xyz\n    write_xyz(self, f, fmt)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.write_xsf","title":"write_xsf","text":"
    write_xsf(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_xsf(self, f: FileOrPath):\n    from .io import write_xsf\n    write_xsf(self, f)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.write_cfg","title":"write_cfg","text":"
    write_cfg(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cfg(self, f: FileOrPath):\n    from .io import write_cfg\n    write_cfg(self, f)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.write_lmp","title":"write_lmp","text":"
    write_lmp(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_lmp(self, f: FileOrPath):\n    from .io import write_lmp\n    write_lmp(self, f)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.write","title":"write","text":"
    write(path: FileOrPath, ty: FileType)\n
    write(\n    path: Union[str, Path, TextIO], ty: Literal[None] = None\n)\n
    write(path: FileOrPath, ty: Optional[FileType] = None)\n

    Write this structure to a file.

    A file type may be specified using ty. If no ty is specified, it is inferred from the path's extension.

    Source code in atomlib/mixins.py
    def write(self, path: FileOrPath, ty: t.Optional[FileType] = None):\n    \"\"\"\n    Write this structure to a file.\n\n    A file type may be specified using `ty`.\n    If no `ty` is specified, it is inferred from the path's extension.\n    \"\"\"\n    from .io import write\n    write(self, path, ty)  # type: ignore\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.write_mslice","title":"write_mslice","text":"
    write_mslice(\n    f: BinaryFileOrPath,\n    template: Optional[MSliceFile] = None,\n    *,\n    slice_thickness: Optional[float] = None,\n    scan_points: Optional[ArrayLike] = None,\n    scan_extent: Optional[ArrayLike] = None,\n    noise_sigma: Optional[float] = None,\n    conv_angle: Optional[float] = None,\n    energy: Optional[float] = None,\n    defocus: Optional[float] = None,\n    tilt: Optional[Tuple[float, float]] = None,\n    tds: Optional[bool] = None,\n    n_cells: Optional[ArrayLike] = None\n)\n

    Write a structure to an mslice file.

    template may be a file, path, or ElementTree containing an existing mslice file. Its structure will be modified to make the final output. If not specified, a default template will be used.

    Additional options modify simulation properties. If an option is not specified, the template's properties are used.

    Source code in atomlib/mixins.py
    def write_mslice(self, f: BinaryFileOrPath, template: t.Optional[MSliceFile] = None, *,\n             slice_thickness: t.Optional[float] = None,  # angstrom\n             scan_points: t.Optional[ArrayLike] = None,\n             scan_extent: t.Optional[ArrayLike] = None,\n             noise_sigma: t.Optional[float] = None,  # angstrom\n             conv_angle: t.Optional[float] = None,  # mrad\n             energy: t.Optional[float] = None,  # keV\n             defocus: t.Optional[float] = None,  # angstrom\n             tilt: t.Optional[t.Tuple[float, float]] = None,  # (mrad, mrad)\n             tds: t.Optional[bool] = None,\n             n_cells: t.Optional[ArrayLike] = None):\n    \"\"\"\n    Write a structure to an mslice file.\n\n    `template` may be a file, path, or `ElementTree` containing an existing mslice file.\n    Its structure will be modified to make the final output. If not specified, a default\n    template will be used.\n\n    Additional options modify simulation properties. If an option is not specified, the\n    template's properties are used.\n    \"\"\"\n    from .io import write_mslice\n    return write_mslice(self, f, template, slice_thickness=slice_thickness,\n                        scan_points=scan_points, scan_extent=scan_extent,\n                        conv_angle=conv_angle, energy=energy, defocus=defocus,\n                        noise_sigma=noise_sigma, tilt=tilt, tds=tds, n_cells=n_cells)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.write_qe","title":"write_qe","text":"
    write_qe(\n    f: FileOrPath,\n    pseudo: Optional[Mapping[str, str]] = None,\n)\n

    Write a structure to a Quantum Espresso pw.x file.

    PARAMETER DESCRIPTION f

    File or path to write to

    TYPE: FileOrPath

    pseudo

    Mapping from atom symbol

    TYPE: Optional[Mapping[str, str]] DEFAULT: None

    Source code in atomlib/mixins.py
    def write_qe(self, f: FileOrPath, pseudo: t.Optional[t.Mapping[str, str]] = None):\n    \"\"\"\n    Write a structure to a Quantum Espresso pw.x file.\n\n    Args:\n      f: File or path to write to\n      pseudo: Mapping from atom symbol\n    \"\"\"\n    from .io import write_qe\n    write_qe(self, f, pseudo)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.from_ortho","title":"from_ortho classmethod","text":"
    from_ortho(\n    atoms: IntoAtoms,\n    ortho: LinearTransform3D,\n    *,\n    n_cells: Optional[VecLike] = None,\n    frame: CoordinateFrame = \"local\",\n    keep_frame: bool = False\n)\n

    Make an atom cell given a list of atoms and an orthogonalization matrix. Atoms are assumed to be in the coordinate system frame.

    Source code in atomlib/atomcell.py
    @classmethod\ndef from_ortho(cls, atoms: IntoAtoms, ortho: LinearTransform3D, *,\n               n_cells: t.Optional[VecLike] = None,\n               frame: CoordinateFrame = 'local',\n               keep_frame: bool = False):\n    \"\"\"\n    Make an atom cell given a list of atoms and an orthogonalization matrix.\n    Atoms are assumed to be in the coordinate system `frame`.\n    \"\"\"\n    cell = Cell.from_ortho(ortho, n_cells)\n    return cls(atoms, cell, frame=frame, keep_frame=keep_frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.from_unit_cell","title":"from_unit_cell classmethod","text":"
    from_unit_cell(\n    atoms: IntoAtoms,\n    cell_size: VecLike,\n    cell_angle: Optional[VecLike] = None,\n    *,\n    n_cells: Optional[VecLike] = None,\n    frame: CoordinateFrame = \"local\",\n    keep_frame: bool = False\n)\n

    Make a cell given a list of atoms and unit cell parameters. Atoms are assumed to be in the coordinate system frame.

    Source code in atomlib/atomcell.py
    @classmethod\ndef from_unit_cell(cls, atoms: IntoAtoms, cell_size: VecLike,\n                   cell_angle: t.Optional[VecLike] = None, *,\n                   n_cells: t.Optional[VecLike] = None,\n                   frame: CoordinateFrame = 'local',\n                   keep_frame: bool = False):\n    \"\"\"\n    Make a cell given a list of atoms and unit cell parameters.\n    Atoms are assumed to be in the coordinate system `frame`.\n    \"\"\"\n    cell = Cell.from_unit_cell(cell_size, cell_angle, n_cells=n_cells)\n    return cls(atoms, cell, frame=frame, keep_frame=keep_frame)\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.orthogonalize","title":"orthogonalize","text":"
    orthogonalize() -> OrthoCell\n
    Source code in atomlib/atomcell.py
    def orthogonalize(self) -> OrthoCell:\n    if self.is_orthogonal():\n        return OrthoCell(self.atoms, self.cell, frame=self.frame)\n    raise NotImplementedError()\n
    "},{"location":"api/atomcell/#atomlib.atomcell.OrthoCell.is_orthogonal","title":"is_orthogonal","text":"
    is_orthogonal(tol: float = 1e-08) -> Literal[True]\n

    Returns whether this cell is orthogonal (axes are at right angles.)

    Source code in atomlib/atomcell.py
    def is_orthogonal(self, tol: float = 1e-8) -> t.Literal[True]:\n    \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n    return True\n
    "},{"location":"api/atoms/","title":"atomlib.atoms","text":"

    Raw atoms collection

    This module defines HasAtoms and the concrete Atoms, which holds a collection of atoms with no cell or periodicity. Atoms is essentially a wrapper around a polars.DataFrame.

    "},{"location":"api/atoms/#atomlib.atoms.SchemaDict","title":"SchemaDict module-attribute","text":"
    SchemaDict: TypeAlias = OrderedDict[str, DataType]\n
    "},{"location":"api/atoms/#atomlib.atoms.IntoExprColumn","title":"IntoExprColumn module-attribute","text":"
    IntoExprColumn: TypeAlias = IntoExprColumn\n
    "},{"location":"api/atoms/#atomlib.atoms.IntoExpr","title":"IntoExpr module-attribute","text":"
    IntoExpr: TypeAlias = IntoExpr\n
    "},{"location":"api/atoms/#atomlib.atoms.UniqueKeepStrategy","title":"UniqueKeepStrategy module-attribute","text":"
    UniqueKeepStrategy: TypeAlias = UniqueKeepStrategy\n
    "},{"location":"api/atoms/#atomlib.atoms.FillNullStrategy","title":"FillNullStrategy module-attribute","text":"
    FillNullStrategy: TypeAlias = FillNullStrategy\n
    "},{"location":"api/atoms/#atomlib.atoms.RollingInterpolationMethod","title":"RollingInterpolationMethod module-attribute","text":"
    RollingInterpolationMethod: TypeAlias = (\n    RollingInterpolationMethod\n)\n
    "},{"location":"api/atoms/#atomlib.atoms.ConcatMethod","title":"ConcatMethod module-attribute","text":"
    ConcatMethod: TypeAlias = Literal[\n    \"horizontal\", \"vertical\", \"diagonal\", \"inner\", \"align\"\n]\n
    "},{"location":"api/atoms/#atomlib.atoms.IntoAtoms","title":"IntoAtoms module-attribute","text":"
    IntoAtoms: TypeAlias = Union[\n    Dict[str, Sequence[Any]],\n    Sequence[Any],\n    ndarray,\n    DataFrame,\n    \"Atoms\",\n]\n

    A type convertible into an Atoms.

    "},{"location":"api/atoms/#atomlib.atoms.AtomSelection","title":"AtomSelection module-attribute","text":"
    AtomSelection: TypeAlias = Union[\n    IntoExprColumn,\n    NDArray[bool_],\n    ArrayLike,\n    Mapping[str, Any],\n]\n

    Polars expression selecting a subset of atoms. Can be used with many Atoms methods.

    "},{"location":"api/atoms/#atomlib.atoms.AtomValues","title":"AtomValues module-attribute","text":"
    AtomValues: TypeAlias = Union[\n    IntoExprColumn,\n    NDArray[generic],\n    ArrayLike,\n    Mapping[str, Any],\n]\n

    Array, value, or polars expression mapping atom symbols to values. Can be used with with_* methods on Atoms

    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms","title":"HasAtoms","text":"

    Bases: ABC

    Abstract class representing any (possibly compound) collection of atoms.

    Source code in atomlib/atoms.py
    class HasAtoms(abc.ABC):\n    \"\"\"Abstract class representing any (possibly compound) collection of atoms.\"\"\"\n\n    # abstract methods\n\n    @abc.abstractmethod\n    def get_atoms(self, frame: t.Literal['local'] = 'local') -> Atoms:\n        \"\"\"\n        Get atoms contained in `self`. This should be a low cost method.\n\n        Args:\n          frame: Coordinate frame to return atoms in. For a plain [`HasAtoms`][atomlib.atoms.HasAtoms],\n                 only `'local'` is supported.\n\n        Return:\n          The contained atoms\n        \"\"\"\n        ...\n\n    @abc.abstractmethod\n    def with_atoms(self, atoms: HasAtoms, frame: t.Literal['local'] = 'local') -> Self:\n        \"\"\"\n        Return a copy of self with the inner [`Atoms`][atomlib.atoms.Atoms] replaced.\n\n        Args:\n          atoms: [`HasAtoms`][atomlib.atoms.HasAtoms] to replace these with.\n          frame: Coordinate frame inside atoms are in. For a plain [`HasAtoms`][atomlib.atoms.HasAtoms],\n                 only `'local'` is supported.\n\n        Return:\n          A copy of `self` updated with the given atoms\n        \"\"\"\n        ...\n\n    @classmethod\n    @abc.abstractmethod\n    def _combine_metadata(cls: t.Type[HasAtomsT], *atoms: HasAtoms) -> HasAtomsT:\n        \"\"\"\n        When combining multiple `HasAtoms`, check that they are compatible with each other,\n        and return a 'representative' which best represents the combined metadata.\n        Implementors should treat `Atoms` as acceptable, but having no metadata.\n        \"\"\"\n        ...\n\n    def _get_frame(self) -> polars.DataFrame:\n        return self.get_atoms().inner\n\n    # dataframe methods\n\n    @property\n    @_fwd_frame(lambda df: df.columns)\n    def columns(self) -> t.List[str]:\n        \"\"\"\n        Return the column names in `self`.\n\n        Returns:\n          A sequence of column names\n        \"\"\"\n        ...\n\n    @property\n    @_fwd_frame(lambda df: df.dtypes)\n    def dtypes(self) -> t.List[polars.DataType]:\n        \"\"\"\n        Return the datatypes in `self`.\n\n        Returns:\n          A sequence of column [`DataType`][polars.datatypes.DataType]s\n        \"\"\"\n        ...\n\n    @property\n    @_fwd_frame(lambda df: df.schema)  # type: ignore\n    def schema(self) -> Schema:\n        \"\"\"\n        Return the schema of `self`.\n\n        Returns:\n          A dictionary of column names and [`DataType`][polars.datatypes.DataType]s\n        \"\"\"\n        ...\n\n    @_fwd_frame(polars.DataFrame.describe)\n    def describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n                 interpolation: RollingInterpolationMethod = 'nearest') -> polars.DataFrame:\n        \"\"\"\n        Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n        Args:\n          percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                       50% (median), and 75% (third quartile).\n\n        Returns:\n          A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n        \"\"\"\n        ...\n\n    @_fwd_frame_map\n    def with_columns(self,\n                     *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                     **named_exprs: IntoExpr) -> polars.DataFrame:\n        \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n        return self._get_frame().with_columns(*exprs, **named_exprs)\n\n    with_column = with_columns\n\n    @_fwd_frame_map\n    def insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n        return self._get_frame().insert_column(index, column)\n\n    @_fwd_frame(lambda df, name: df.get_column(name))\n    def get_column(self, name: str) -> polars.Series:\n        \"\"\"\n        Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        ...\n\n    @_fwd_frame(polars.DataFrame.get_columns)\n    def get_columns(self) -> t.List[polars.Series]:\n        \"\"\"\n        Return all columns from `self` as a list of [`Series`][polars.Series].\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        ...\n\n    @_fwd_frame(polars.DataFrame.get_column_index)\n    def get_column_index(self, name: str) -> int:\n        \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n        ...\n\n    @_fwd_frame(polars.DataFrame.group_by)\n    def group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]], maintain_order: bool = False,\n                 **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n        \"\"\"\n        Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n        \"\"\"\n        ...\n\n    def pipe(self: HasAtomsT, function: t.Callable[Concatenate[HasAtomsT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n        \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n        return function(self, *args, **kwargs)\n\n    @_fwd_frame_map\n    def clone(self) -> polars.DataFrame:\n        \"\"\"Return a copy of `self`.\"\"\"\n        return self._get_frame().clone()\n\n    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n        \"\"\"Return `self` with the specified columns removed.\"\"\"\n        return self._get_frame().drop(*columns, strict=strict)\n\n    # row-wise operations\n\n    def filter(\n        self,\n        *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n        **constraints: t.Any,\n    ) -> Self:\n        \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n        # TODO clean up\n        preds_not_none = tuple(filter(lambda p: p is not None, predicates))\n        if not len(preds_not_none) and not len(constraints):\n            return self\n        return self.with_atoms(Atoms(self._get_frame().filter(*preds_not_none, **constraints), _unchecked=True))  # type: ignore\n\n    @_fwd_frame_map\n    def sort(\n        self,\n        by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        *more_by: IntoExpr,\n        descending: t.Union[bool, t.Sequence[bool]] = False,\n        nulls_last: bool = False,\n    ) -> polars.DataFrame:\n        \"\"\"\n        Sort the atoms in `self` by the given columns/expressions.\n        \"\"\"\n        return self._get_frame().sort(\n            by, *more_by, descending=descending, nulls_last=nulls_last\n        )\n\n    @_fwd_frame_map\n    def slice(self, offset: int, length: t.Optional[int] = None) -> polars.DataFrame:\n        \"\"\"Return a slice of the rows in `self`.\"\"\"\n        return self._get_frame().slice(offset, length)\n\n    @_fwd_frame_map\n    def head(self, n: int = 5) -> polars.DataFrame:\n        \"\"\"Return the first `n` rows of `self`.\"\"\"\n        return self._get_frame().head(n)\n\n    @_fwd_frame_map\n    def tail(self, n: int = 5) -> polars.DataFrame:\n        \"\"\"Return the last `n` rows of `self`.\"\"\"\n        return self._get_frame().tail(n)\n\n    @_fwd_frame_map\n    def drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n        \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n        return self._get_frame().drop_nulls(subset)\n\n    @_fwd_frame_map\n    def fill_null(\n        self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n        limit: t.Optional[int] = None, matches_supertype: bool = True,\n    ) -> polars.DataFrame:\n        \"\"\"Fill null values in `self`, using the specified value or strategy.\"\"\"\n        return self._get_frame().fill_null(value, strategy, limit, matches_supertype=matches_supertype)\n\n    @_fwd_frame_map\n    def fill_nan(self, value: t.Union[polars.Expr, int, float, None]) -> polars.DataFrame:\n        \"\"\"Fill floating-point NaN values in `self`.\"\"\"\n        return self._get_frame().fill_nan(value)\n\n    @classmethod\n    def concat(cls: t.Type[HasAtomsT],\n               atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n               rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n        \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n        # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n        # same type as ``cls``.\n        if _is_abstract(cls):\n            raise TypeError(\"concat() must be called on a concrete class.\")\n\n        if isinstance(atoms, HasAtoms):\n            atoms = (atoms,)\n        dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n        representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n        if len(dfs) == 0:\n            return representative.with_atoms(Atoms.empty(), 'local')\n\n        if how in ('vertical', 'vertical_relaxed'):\n            # get order from first member\n            cols = dfs[0].columns\n            dfs = [df.select(cols) for df in dfs]\n        elif how == 'inner':\n            cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n            schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n            if len(schema) == 0:\n                raise ValueError(\"Atoms have no columns in common\")\n\n            dfs = [_select_schema(df, schema) for df in dfs]\n            how = 'vertical'\n\n        return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n\n    @t.overload\n    def partition_by(\n        self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n        maintain_order: bool = True, include_key: bool = True, as_dict: t.Literal[False] = False\n    ) -> t.List[Self]:\n        ...\n\n    @t.overload\n    def partition_by(\n        self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n        maintain_order: bool = True, include_key: bool = True, as_dict: t.Literal[True] = ...\n    ) -> t.Dict[t.Any, Self]:\n        ...\n\n    def partition_by(\n        self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n        maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n    ) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n        \"\"\"\n        Group by the given columns and partition into separate dataframes.\n\n        Return the partitions as a dictionary by specifying `as_dict=True`.\n        \"\"\"\n        if as_dict:\n            d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n            return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n        return [\n            self.with_atoms(Atoms(df, _unchecked=True))\n            for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n        ]\n\n    # column-wise operations\n\n    @_fwd_frame(polars.DataFrame.select)\n    def select(\n        self,\n        *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        **named_exprs: IntoExpr,\n    ) -> polars.DataFrame:\n        \"\"\"\n        Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n        Expressions may either be columns or expressions of columns.\n\n        [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n        \"\"\"\n        ...\n\n    # some helpers we add\n\n    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n        \"\"\"\n        Select columns from `self` and cast to the given schema.\n        Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n        \"\"\"\n        return _select_schema(self, schema)\n\n    def select_props(\n        self,\n        *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        **named_exprs: IntoExpr\n    ) -> Self:\n        \"\"\"\n        Select `exprs` from `self`, while keeping required columns.\n\n        Returns:\n          A [`HasAtoms`][atomlib.atoms.HasAtoms] filtered to contain the\n          specified properties (as well as required columns).\n        \"\"\"\n        props = self._get_frame().lazy().select(*exprs, **named_exprs).drop(_REQUIRED_COLUMNS, strict=False).collect(_eager=True)\n        return self.with_atoms(\n            Atoms(self._get_frame().select(_REQUIRED_COLUMNS).hstack(props), _unchecked=False)\n        )\n\n    def try_select(\n        self,\n        *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n        **named_exprs: IntoExpr,\n    ) -> t.Optional[polars.DataFrame]:\n        \"\"\"\n        Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n        Expressions may either be columns or expressions of columns. Returns `None` if any\n        columns are missing.\n\n        [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n        \"\"\"\n        try:\n            return self._get_frame().select(*exprs, **named_exprs)\n        except polars.ColumnNotFoundError:\n            return None\n\n    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n        \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n        try:\n            return self.get_column(name)\n        except polars.exceptions.ColumnNotFoundError:\n            return None\n\n    def assert_equal(self, other: t.Any):\n        assert isinstance(other, HasAtoms)\n        assert dict(self.schema) == dict(other.schema)\n        for col in self.schema.keys():\n            polars.testing.assert_series_equal(self[col], other[col], check_names=False, rtol=1e-3, atol=1e-8)\n\n    # dunders\n\n    @_fwd_frame(polars.DataFrame.__len__)\n    def __len__(self) -> int:\n        \"\"\"Return the number of atoms in `self`.\"\"\"\n        ...\n\n    @_fwd_frame(polars.DataFrame.__contains__)\n    def __contains__(self, key: str) -> bool:\n        \"\"\"Return whether `self` contains the given column.\"\"\"\n        ...\n\n    def __add__(self, other: IntoAtoms) -> HasAtoms:\n        return self.__class__.concat((self, other), how='inner')\n\n    def __radd__(self, other: IntoAtoms) -> HasAtoms:\n        return self.__class__.concat((other, self), how='inner')\n\n    def __getitem__(self, column: str) -> polars.Series:\n        try:\n            return self.get_column(column)\n        except polars.exceptions.ColumnNotFoundError:\n            if column in ('x', 'y', 'z'):\n                return self.select(_coord_expr(column)).to_series()\n            raise\n\n    @_fwd_frame(polars.DataFrame.__dataframe__)\n    def __dataframe__(self, nan_as_null: bool = False, allow_copy: bool = True) -> polars.interchange.dataframe.PolarsDataFrame:\n        ...\n\n    # atoms-specific methods\n\n    def bbox_atoms(self) -> BBox3D:\n        \"\"\"Return the bounding box of all the atoms in ``self``.\"\"\"\n        return BBox3D.from_pts(self.coords())\n\n    bbox = bbox_atoms\n\n    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                        transform_velocities: bool = False) -> Self:\n        \"\"\"\n        Transform the atoms in `self` by `transform`.\n        If `selection` is given, only transform the atoms in `selection`.\n        \"\"\"\n        transform = Transform3D.make(transform)\n        selection = _selection_to_numpy(self, selection)\n        transformed = self.with_coords(Transform3D.make(transform) @ self.coords(selection), selection)\n        # try to transform velocities as well\n        if transform_velocities and (velocities := self.velocities(selection)) is not None:\n            return transformed.with_velocity(transform.transform_vec(velocities), selection)\n        return transformed\n\n    transform = transform_atoms\n\n    def round_near_zero(self, tol: float = 1e-14) -> Self:\n        \"\"\"\n        Round atom position values near zero to zero.\n        \"\"\"\n        return self.with_columns(coords=polars.concat_list(\n            polars.when(_coord_expr(col).abs() >= tol).then(_coord_expr(col)).otherwise(polars.lit(0.))\n            for col in range(3)\n        ).list.to_array(3))\n\n    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n             y_min: float = -numpy.inf, y_max: float = numpy.inf,\n             z_min: float = -numpy.inf, z_max: float = numpy.inf) -> Self:\n        \"\"\"\n        Crop, removing all atoms outside of the specified region, inclusive.\n        \"\"\"\n\n        return self.filter(\n            self.x().is_between(x_min, x_max, closed='both'),\n            self.y().is_between(y_min, y_max, closed='both'),\n            self.z().is_between(z_min, z_max, closed='both'),\n        )\n\n    crop_atoms = crop\n\n    def _wrap(self, eps: float = 1e-5) -> Self:\n        coords = (self.coords() + eps) % 1. - eps\n        return self.with_coords(coords)\n\n    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                    keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n        \"\"\"\n        De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n        to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n        `keep` (defaults to the first atom).\n\n        If `subset` is specified, only those columns will be included while assessing duplicates.\n        Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n        \"\"\"\n        import scipy.spatial\n\n        cols = set((subset,) if isinstance(subset, str) else subset)\n\n        indices = numpy.arange(len(self))\n\n        spatial_cols = cols.intersection(('x', 'y', 'z'))\n        cols -= spatial_cols\n        if len(spatial_cols) > 0:\n            coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n            tree = scipy.spatial.KDTree(coords)\n\n            # TODO This is a bad algorithm\n            while True:\n                changed = False\n                for (i, j) in tree.query_pairs(tol, 2.):\n                    # whenever we encounter a pair, ensure their index matches\n                    i_i, i_j = indices[[i, j]]\n                    if i_i != i_j:\n                        indices[i] = indices[j] = min(i_i, i_j)\n                        changed = True\n                if not changed:\n                    break\n\n            self = self.with_column(polars.Series('_unique_pts', indices))\n            cols.add('_unique_pts')\n\n        frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n        if len(spatial_cols) > 0:\n            frame = frame.drop('_unique_pts')\n\n        return self.with_atoms(Atoms(frame, _unchecked=True))\n\n    unique = deduplicate\n\n    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n        \"\"\"\n        Return a periodic cell with the given orthogonal cell dimensions.\n\n        If cell_size is not specified, it will be assumed (and may be incorrect).\n        \"\"\"\n        # TODO: test this\n        from .atomcell import AtomCell\n\n        if cell_size is None:\n            warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n            cell_size = self.bbox().size\n            cell_origin = self.bbox().min\n\n        # TODO test this origin code\n        cell = Cell.from_unit_cell(cell_size)\n        if cell_origin is not None:\n            cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n        return AtomCell(self.get_atoms(), cell, frame='local')\n\n    # property getters and setters\n\n    def coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> NDArray[numpy.float64]:\n        \"\"\"Return a `(N, 3)` ndarray of atom coordinates (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n        df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n        return df.get_column('coords').to_numpy().astype(numpy.float64)\n\n    def x(self) -> polars.Expr:\n        return polars.col('coords').arr.get(0).alias('x')\n\n    def y(self) -> polars.Expr:\n        return polars.col('coords').arr.get(1).alias('y')\n\n    def z(self) -> polars.Expr:\n        return polars.col('coords').arr.get(2).alias('z')\n\n    def velocities(self, selection: t.Optional[AtomSelection] = None) -> t.Optional[NDArray[numpy.float64]]:\n        \"\"\"Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n        if 'velocity' not in self:\n            return None\n\n        df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n        return df.get_column('velocity').to_numpy().astype(numpy.float64)\n\n    def types(self) -> t.Optional[polars.Series]:\n        \"\"\"\n        Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        return self.try_get_column('type')\n\n    def masses(self) -> t.Optional[polars.Series]:\n        \"\"\"\n        Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n        [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n        \"\"\"\n        return self.try_get_column('mass')\n\n    @t.overload\n    def add_atom(self, elem: t.Union[int, str], x: ArrayLike, /, *,\n                 y: None = None, z: None = None,\n                 **kwargs: t.Any) -> Self:\n        ...\n\n    @t.overload\n    def add_atom(self, elem: t.Union[int, str], /,\n                 x: float, y: float, z: float,\n                 **kwargs: t.Any) -> Self:\n        ...\n\n    def add_atom(self, elem: t.Union[int, str], /,\n                 x: t.Union[ArrayLike, float],\n                 y: t.Optional[float] = None,\n                 z: t.Optional[float] = None,\n                 **kwargs: t.Any) -> Self:\n        \"\"\"\n        Return a copy of `self` with an extra atom.\n\n        By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n        Try to avoid calling this in a loop (Use [`HasAtoms.concat`][atomlib.atoms.HasAtoms.concat] instead).\n        \"\"\"\n        if isinstance(elem, int):\n            kwargs.update(elem=elem)\n        else:\n            kwargs.update(symbol=elem)\n        if hasattr(x, '__len__') and len(x) > 1:  # type: ignore\n            (x, y, z) = to_vec3(x)\n        elif y is None or z is None:\n            raise ValueError(\"Must specify vector of positions or x, y, & z.\")\n\n        sym = get_sym(elem) if isinstance(elem, int) else elem\n        d: t.Dict[str, t.Any] = {'x': x, 'y': y, 'z': z, 'symbol': sym, **kwargs}\n        return self.concat(\n            (self, Atoms(d).select_schema(self.schema)),\n            how='vertical'\n        )\n\n    @t.overload\n    def pos(self, x: t.Sequence[t.Optional[float]], /, *,\n            y: None = None, z: None = None,\n            tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n        ...\n\n    @t.overload\n    def pos(self, x: t.Optional[float] = None, y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n            tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n        ...\n\n    def pos(self,\n            x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n            y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n            tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n        \"\"\"\n        Select all atoms at a given position.\n\n        Formally, returns all atoms within a cube of radius ``tol``\n        centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n        Additional parameters given as ``kwargs`` will be checked\n        as additional parameters (with strict equality).\n        \"\"\"\n\n        if isinstance(x, t.Sequence):\n            (x, y, z) = x\n\n        tol = abs(float(tol))\n        selection = polars.lit(True)\n        if x is not None:\n            selection &= self.x().is_between(x - tol, x + tol, closed='none')\n        if y is not None:\n            selection &= self.y().is_between(y - tol, y + tol, closed='none')\n        if z is not None:\n            selection &= self.z().is_between(z - tol, z + tol, closed='none')\n        for (col, val) in kwargs.items():\n            selection &= (polars.col(col) == val)\n\n        return selection\n\n    def with_index(self, index: t.Optional[AtomValues] = None) -> Self:\n        \"\"\"\n        Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n        If `index` is not specified, defaults to an existing index or a new index.\n        \"\"\"\n        if index is None and 'i' in self.columns:\n            return self\n        if index is None:\n            index = numpy.arange(len(self), dtype=numpy.int64)\n        return self.with_column(_values_to_expr(self, index, polars.Int64).alias('i'))\n\n    def with_wobble(self, wobble: t.Optional[AtomValues] = None) -> Self:\n        \"\"\"\n        Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n        If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n        \"\"\"\n        if wobble is None and 'wobble' in self.columns:\n            return self\n        wobble = 0. if wobble is None else wobble\n        return self.with_column(_values_to_expr(self, wobble, polars.Float64).alias('wobble'))\n\n    def with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None) -> Self:\n        \"\"\"\n        Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n        If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n        \"\"\"\n        if frac_occupancy is None and 'frac_occupancy' in self.columns:\n            return self\n        frac_occupancy = 1. if frac_occupancy is None else frac_occupancy\n        return self.with_column(_values_to_expr(self, frac_occupancy, polars.Float64).alias('frac_occupancy'))\n\n    def apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n        \"\"\"\n        Displace the atoms in `self` by the amount in the `wobble` column.\n        `wobble` is interpretated as a mean-squared displacement, which is distributed\n        equally over each axis.\n        \"\"\"\n        if 'wobble' not in self.columns:\n            return self\n        rng = numpy.random.default_rng(seed=rng)\n\n        stddev = self.select((polars.col('wobble') / 3.).sqrt()).to_series().to_numpy()\n        coords = self.coords()\n        coords += stddev[:, None] * rng.standard_normal(coords.shape)\n        return self.with_coords(coords)\n\n    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n        \"\"\"\n        For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n        \"\"\"\n        if 'frac_occupancy' not in self.columns:\n            return self\n        rng = numpy.random.default_rng(seed=rng)\n\n        frac = self.select('frac_occupancy').to_series().to_numpy()\n        choice = rng.binomial(1, frac).astype(numpy.bool_)\n        return self.filter(polars.lit(choice))\n\n    def with_type(self, types: t.Optional[AtomValues] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atom types in column 'type'.\n        If `types` is not specified, use the already existing types or auto-assign them.\n\n        When auto-assigning, each symbol is given a unique value, case-sensitive.\n        Values are assigned from lowest atomic number to highest.\n        For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n        \"\"\"\n        if types is not None:\n            return self.with_columns(type=_values_to_expr(self, types, polars.Int32))\n        if 'type' in self.columns:\n            return self\n\n        unique = Atoms(self._get_frame().unique(maintain_order=False, subset=['elem', 'symbol']).sort(['elem', 'symbol']), _unchecked=True)\n        new = self.with_column(polars.Series('type', values=numpy.zeros(len(self)), dtype=polars.Int32))\n\n        logging.warning(\"Auto-assigning element types\")\n        for (i, (elem, sym)) in enumerate(unique.select(('elem', 'symbol')).rows()):\n            print(f\"Assigning type {i+1} to element '{sym}'\")\n            new = new.with_column(polars.when((polars.col('elem') == elem) & (polars.col('symbol') == sym))\n                                        .then(polars.lit(i+1))\n                                        .otherwise(polars.col('type'))\n                                        .alias('type'))\n\n        assert (new.get_column('type') == 0).sum() == 0\n        return new\n\n    def with_mass(self, mass: t.Optional[ArrayLike] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atom masses in column `'mass'`.\n        If `mass` is not specified, use the already existing masses or auto-assign them.\n        \"\"\"\n        if mass is not None:\n            return self.with_column(_values_to_expr(self, mass, polars.Float32).alias('mass'))\n        if 'mass' in self.columns:\n            return self\n\n        unique_elems = self.get_column('elem').unique()\n        new = self.with_column(polars.Series('mass', values=numpy.zeros(len(self)), dtype=polars.Float32))\n\n        logging.warning(\"Auto-assigning element masses\")\n        for elem in unique_elems:\n            new = new.with_column(polars.when(polars.col('elem') == elem)\n                                        .then(polars.lit(get_mass(elem)))\n                                        .otherwise(polars.col('mass'))\n                                        .alias('mass'))\n\n        assert (new.get_column('mass').abs() < 1e-10).sum() == 0\n        return new\n\n    def with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None) -> Self:\n        \"\"\"\n        Return `self` with the given atomic symbols.\n        \"\"\"\n        if selection is not None:\n            selection = _selection_to_numpy(self, selection)\n            new_symbols = self.get_column('symbol')\n            new_symbols[selection] = polars.Series(list(numpy.broadcast_to(symbols, len(selection))), dtype=polars.Utf8)\n            symbols = new_symbols\n\n        # TODO better cast here\n        symbols = polars.Series('symbol', list(numpy.broadcast_to(symbols, len(self))), dtype=polars.Utf8)\n        return self.with_columns((symbols, get_elem(symbols)))\n\n    def with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> Self:\n        \"\"\"\n        Return `self` replaced with the given atomic positions.\n        \"\"\"\n        if selection is not None:\n            selection = _selection_to_numpy(self, selection)\n            new_pts = self.coords()\n            pts = numpy.atleast_2d(pts)\n            assert pts.shape[-1] == 3\n            new_pts[selection] = pts\n            pts = new_pts\n\n        # https://github.com/pola-rs/polars/issues/18369\n        pts = numpy.broadcast_to(pts, (len(self), 3)) if len(self) else []\n        return self.with_columns(polars.Series('coords', pts, polars.Array(polars.Float64, 3)))\n\n    def with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                      selection: t.Optional[AtomSelection] = None) -> Self:\n        \"\"\"\n        Return `self` replaced with the given atomic velocities.\n        If `pts` is not specified, use the already existing velocities or zero.\n        \"\"\"\n        if pts is None:\n            if 'velocity' in self:\n                return self\n            all_pts = numpy.zeros((len(self), 3))\n        else:\n            all_pts = self['velocity'].to_numpy()\n\n        if selection is None:\n            all_pts = pts or all_pts\n        elif pts is not None:\n            selection = _selection_to_numpy(self, selection)\n            all_pts = numpy.require(all_pts, requirements=['WRITEABLE'])\n            pts = numpy.atleast_2d(pts)\n            assert pts.shape[-1] == 3\n            all_pts[selection] = pts\n\n        all_pts = numpy.broadcast_to(all_pts, (len(self), 3))\n        return self.with_columns(polars.Series('velocity', all_pts, polars.Array(polars.Float64, 3)))\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.columns","title":"columns property","text":"
    columns: List[str]\n

    Return the column names in self.

    RETURNS DESCRIPTION List[str]

    A sequence of column names

    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.dtypes","title":"dtypes property","text":"
    dtypes: List[DataType]\n

    Return the datatypes in self.

    RETURNS DESCRIPTION List[DataType]

    A sequence of column DataTypes

    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.schema","title":"schema property","text":"
    schema: Schema\n

    Return the schema of self.

    RETURNS DESCRIPTION Schema

    A dictionary of column names and DataTypes

    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_column","title":"with_column class-attribute instance-attribute","text":"
    with_column = with_columns\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.bbox","title":"bbox class-attribute instance-attribute","text":"
    bbox = bbox_atoms\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.transform","title":"transform class-attribute instance-attribute","text":"
    transform = transform_atoms\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.crop_atoms","title":"crop_atoms class-attribute instance-attribute","text":"
    crop_atoms = crop\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.unique","title":"unique class-attribute instance-attribute","text":"
    unique = deduplicate\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.get_atoms","title":"get_atoms abstractmethod","text":"
    get_atoms(frame: Literal['local'] = 'local') -> Atoms\n

    Get atoms contained in self. This should be a low cost method.

    PARAMETER DESCRIPTION frame

    Coordinate frame to return atoms in. For a plain HasAtoms, only 'local' is supported.

    TYPE: Literal['local'] DEFAULT: 'local'

    Return

    The contained atoms

    Source code in atomlib/atoms.py
    @abc.abstractmethod\ndef get_atoms(self, frame: t.Literal['local'] = 'local') -> Atoms:\n    \"\"\"\n    Get atoms contained in `self`. This should be a low cost method.\n\n    Args:\n      frame: Coordinate frame to return atoms in. For a plain [`HasAtoms`][atomlib.atoms.HasAtoms],\n             only `'local'` is supported.\n\n    Return:\n      The contained atoms\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_atoms","title":"with_atoms abstractmethod","text":"
    with_atoms(\n    atoms: HasAtoms, frame: Literal[\"local\"] = \"local\"\n) -> Self\n

    Return a copy of self with the inner Atoms replaced.

    PARAMETER DESCRIPTION atoms

    HasAtoms to replace these with.

    TYPE: HasAtoms

    frame

    Coordinate frame inside atoms are in. For a plain HasAtoms, only 'local' is supported.

    TYPE: Literal['local'] DEFAULT: 'local'

    Return

    A copy of self updated with the given atoms

    Source code in atomlib/atoms.py
    @abc.abstractmethod\ndef with_atoms(self, atoms: HasAtoms, frame: t.Literal['local'] = 'local') -> Self:\n    \"\"\"\n    Return a copy of self with the inner [`Atoms`][atomlib.atoms.Atoms] replaced.\n\n    Args:\n      atoms: [`HasAtoms`][atomlib.atoms.HasAtoms] to replace these with.\n      frame: Coordinate frame inside atoms are in. For a plain [`HasAtoms`][atomlib.atoms.HasAtoms],\n             only `'local'` is supported.\n\n    Return:\n      A copy of `self` updated with the given atoms\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.describe","title":"describe","text":"
    describe(\n    percentiles: Union[Sequence[float], float, None] = (\n        0.25,\n        0.5,\n        0.75,\n    ),\n    *,\n    interpolation: RollingInterpolationMethod = \"nearest\"\n) -> DataFrame\n

    Return summary statistics for self. See DataFrame.describe for more information.

    PARAMETER DESCRIPTION percentiles

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), 50% (median), and 75% (third quartile).

    TYPE: Union[Sequence[float], float, None] DEFAULT: (0.25, 0.5, 0.75)

    RETURNS DESCRIPTION DataFrame

    A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.describe)\ndef describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n             interpolation: RollingInterpolationMethod = 'nearest') -> polars.DataFrame:\n    \"\"\"\n    Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n    Args:\n      percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                   50% (median), and 75% (third quartile).\n\n    Returns:\n      A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_columns","title":"with_columns","text":"
    with_columns(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Return a copy of self with the given columns added.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef with_columns(self,\n                 *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 **named_exprs: IntoExpr) -> polars.DataFrame:\n    \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n    return self._get_frame().with_columns(*exprs, **named_exprs)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.insert_column","title":"insert_column","text":"
    insert_column(index: int, column: Series) -> DataFrame\n
    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n    return self._get_frame().insert_column(index, column)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.get_column","title":"get_column","text":"
    get_column(name: str) -> Series\n

    Get the specified column from self, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(lambda df, name: df.get_column(name))\ndef get_column(self, name: str) -> polars.Series:\n    \"\"\"\n    Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.get_columns","title":"get_columns","text":"
    get_columns() -> List[Series]\n

    Return all columns from self as a list of Series.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_columns)\ndef get_columns(self) -> t.List[polars.Series]:\n    \"\"\"\n    Return all columns from `self` as a list of [`Series`][polars.Series].\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.get_column_index","title":"get_column_index","text":"
    get_column_index(name: str) -> int\n

    Get the index of a column by name, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_column_index)\ndef get_column_index(self, name: str) -> int:\n    \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.group_by","title":"group_by","text":"
    group_by(\n    *by: Union[IntoExpr, Iterable[IntoExpr]],\n    maintain_order: bool = False,\n    **named_by: IntoExpr\n) -> GroupBy\n

    Start a group by operation. See DataFrame.group_by for more information.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.group_by)\ndef group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]], maintain_order: bool = False,\n             **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n    \"\"\"\n    Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.pipe","title":"pipe","text":"
    pipe(\n    function: Callable[Concatenate[HasAtomsT, P], T],\n    *args: args,\n    **kwargs: kwargs\n) -> T\n

    Apply function to self (in method-call syntax).

    Source code in atomlib/atoms.py
    def pipe(self: HasAtomsT, function: t.Callable[Concatenate[HasAtomsT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n    \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n    return function(self, *args, **kwargs)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.clone","title":"clone","text":"
    clone() -> DataFrame\n

    Return a copy of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef clone(self) -> polars.DataFrame:\n    \"\"\"Return a copy of `self`.\"\"\"\n    return self._get_frame().clone()\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.drop","title":"drop","text":"
    drop(\n    *columns: Union[str, Iterable[str]], strict: bool = True\n) -> DataFrame\n

    Return self with the specified columns removed.

    Source code in atomlib/atoms.py
    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n    \"\"\"Return `self` with the specified columns removed.\"\"\"\n    return self._get_frame().drop(*columns, strict=strict)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.filter","title":"filter","text":"
    filter(\n    *predicates: Union[\n        None,\n        IntoExprColumn,\n        Iterable[IntoExprColumn],\n        bool,\n        List[bool],\n        ndarray,\n    ],\n    **constraints: Any\n) -> Self\n

    Filter self, removing rows which evaluate to False.

    Source code in atomlib/atoms.py
    def filter(\n    self,\n    *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n    **constraints: t.Any,\n) -> Self:\n    \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n    # TODO clean up\n    preds_not_none = tuple(filter(lambda p: p is not None, predicates))\n    if not len(preds_not_none) and not len(constraints):\n        return self\n    return self.with_atoms(Atoms(self._get_frame().filter(*preds_not_none, **constraints), _unchecked=True))  # type: ignore\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.sort","title":"sort","text":"
    sort(\n    by: Union[IntoExpr, Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: Union[bool, Sequence[bool]] = False,\n    nulls_last: bool = False\n) -> DataFrame\n

    Sort the atoms in self by the given columns/expressions.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef sort(\n    self,\n    by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: t.Union[bool, t.Sequence[bool]] = False,\n    nulls_last: bool = False,\n) -> polars.DataFrame:\n    \"\"\"\n    Sort the atoms in `self` by the given columns/expressions.\n    \"\"\"\n    return self._get_frame().sort(\n        by, *more_by, descending=descending, nulls_last=nulls_last\n    )\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.slice","title":"slice","text":"
    slice(\n    offset: int, length: Optional[int] = None\n) -> DataFrame\n

    Return a slice of the rows in self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef slice(self, offset: int, length: t.Optional[int] = None) -> polars.DataFrame:\n    \"\"\"Return a slice of the rows in `self`.\"\"\"\n    return self._get_frame().slice(offset, length)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.head","title":"head","text":"
    head(n: int = 5) -> DataFrame\n

    Return the first n rows of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef head(self, n: int = 5) -> polars.DataFrame:\n    \"\"\"Return the first `n` rows of `self`.\"\"\"\n    return self._get_frame().head(n)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.tail","title":"tail","text":"
    tail(n: int = 5) -> DataFrame\n

    Return the last n rows of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef tail(self, n: int = 5) -> polars.DataFrame:\n    \"\"\"Return the last `n` rows of `self`.\"\"\"\n    return self._get_frame().tail(n)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.drop_nulls","title":"drop_nulls","text":"
    drop_nulls(\n    subset: Union[str, Collection[str], None] = None\n) -> DataFrame\n

    Drop rows that contain nulls in any of columns subset.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n    \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n    return self._get_frame().drop_nulls(subset)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.fill_null","title":"fill_null","text":"
    fill_null(\n    value: Any = None,\n    strategy: Optional[FillNullStrategy] = None,\n    limit: Optional[int] = None,\n    matches_supertype: bool = True,\n) -> DataFrame\n

    Fill null values in self, using the specified value or strategy.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef fill_null(\n    self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n    limit: t.Optional[int] = None, matches_supertype: bool = True,\n) -> polars.DataFrame:\n    \"\"\"Fill null values in `self`, using the specified value or strategy.\"\"\"\n    return self._get_frame().fill_null(value, strategy, limit, matches_supertype=matches_supertype)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.fill_nan","title":"fill_nan","text":"
    fill_nan(value: Union[Expr, int, float, None]) -> DataFrame\n

    Fill floating-point NaN values in self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef fill_nan(self, value: t.Union[polars.Expr, int, float, None]) -> polars.DataFrame:\n    \"\"\"Fill floating-point NaN values in `self`.\"\"\"\n    return self._get_frame().fill_nan(value)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.concat","title":"concat classmethod","text":"
    concat(\n    atoms: Union[\n        HasAtomsT,\n        IntoAtoms,\n        Iterable[Union[HasAtomsT, IntoAtoms]],\n    ],\n    *,\n    rechunk: bool = True,\n    how: ConcatMethod = \"vertical\"\n) -> HasAtomsT\n

    Concatenate multiple Atoms together, handling metadata appropriately.

    Source code in atomlib/atoms.py
    @classmethod\ndef concat(cls: t.Type[HasAtomsT],\n           atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n           rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n    \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n    # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n    # same type as ``cls``.\n    if _is_abstract(cls):\n        raise TypeError(\"concat() must be called on a concrete class.\")\n\n    if isinstance(atoms, HasAtoms):\n        atoms = (atoms,)\n    dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n    representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n    if len(dfs) == 0:\n        return representative.with_atoms(Atoms.empty(), 'local')\n\n    if how in ('vertical', 'vertical_relaxed'):\n        # get order from first member\n        cols = dfs[0].columns\n        dfs = [df.select(cols) for df in dfs]\n    elif how == 'inner':\n        cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n        schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n        if len(schema) == 0:\n            raise ValueError(\"Atoms have no columns in common\")\n\n        dfs = [_select_schema(df, schema) for df in dfs]\n        how = 'vertical'\n\n    return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.partition_by","title":"partition_by","text":"
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: Literal[False] = False\n) -> List[Self]\n
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: Literal[True] = ...\n) -> Dict[Any, Self]\n
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: bool = False\n) -> Union[List[Self], Dict[Any, Self]]\n

    Group by the given columns and partition into separate dataframes.

    Return the partitions as a dictionary by specifying as_dict=True.

    Source code in atomlib/atoms.py
    def partition_by(\n    self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n    maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n    \"\"\"\n    Group by the given columns and partition into separate dataframes.\n\n    Return the partitions as a dictionary by specifying `as_dict=True`.\n    \"\"\"\n    if as_dict:\n        d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n        return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n    return [\n        self.with_atoms(Atoms(df, _unchecked=True))\n        for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n    ]\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.select","title":"select","text":"
    select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.select)\ndef select(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr,\n) -> polars.DataFrame:\n    \"\"\"\n    Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.select_schema","title":"select_schema","text":"
    select_schema(schema: SchemaDict) -> DataFrame\n

    Select columns from self and cast to the given schema. Raises TypeError if a column is not found or if it can't be cast.

    Source code in atomlib/atoms.py
    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n    \"\"\"\n    Select columns from `self` and cast to the given schema.\n    Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n    \"\"\"\n    return _select_schema(self, schema)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.select_props","title":"select_props","text":"
    select_props(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Self\n

    Select exprs from self, while keeping required columns.

    RETURNS DESCRIPTION Self

    A HasAtoms filtered to contain the

    Self

    specified properties (as well as required columns).

    Source code in atomlib/atoms.py
    def select_props(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Self:\n    \"\"\"\n    Select `exprs` from `self`, while keeping required columns.\n\n    Returns:\n      A [`HasAtoms`][atomlib.atoms.HasAtoms] filtered to contain the\n      specified properties (as well as required columns).\n    \"\"\"\n    props = self._get_frame().lazy().select(*exprs, **named_exprs).drop(_REQUIRED_COLUMNS, strict=False).collect(_eager=True)\n    return self.with_atoms(\n        Atoms(self._get_frame().select(_REQUIRED_COLUMNS).hstack(props), _unchecked=False)\n    )\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.try_select","title":"try_select","text":"
    try_select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Optional[DataFrame]\n

    Try to select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns. Returns None if any columns are missing.

    Source code in atomlib/atoms.py
    def try_select(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr,\n) -> t.Optional[polars.DataFrame]:\n    \"\"\"\n    Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns. Returns `None` if any\n    columns are missing.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    try:\n        return self._get_frame().select(*exprs, **named_exprs)\n    except polars.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.try_get_column","title":"try_get_column","text":"
    try_get_column(name: str) -> Optional[Series]\n

    Try to get a column from self, returning None if it doesn't exist.

    Source code in atomlib/atoms.py
    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n    \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n    try:\n        return self.get_column(name)\n    except polars.exceptions.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n
    Source code in atomlib/atoms.py
    def assert_equal(self, other: t.Any):\n    assert isinstance(other, HasAtoms)\n    assert dict(self.schema) == dict(other.schema)\n    for col in self.schema.keys():\n        polars.testing.assert_series_equal(self[col], other[col], check_names=False, rtol=1e-3, atol=1e-8)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.bbox_atoms","title":"bbox_atoms","text":"
    bbox_atoms() -> BBox3D\n

    Return the bounding box of all the atoms in self.

    Source code in atomlib/atoms.py
    def bbox_atoms(self) -> BBox3D:\n    \"\"\"Return the bounding box of all the atoms in ``self``.\"\"\"\n    return BBox3D.from_pts(self.coords())\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.transform_atoms","title":"transform_atoms","text":"
    transform_atoms(\n    transform: IntoTransform3D,\n    selection: Optional[AtomSelection] = None,\n    *,\n    transform_velocities: bool = False\n) -> Self\n

    Transform the atoms in self by transform. If selection is given, only transform the atoms in selection.

    Source code in atomlib/atoms.py
    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                    transform_velocities: bool = False) -> Self:\n    \"\"\"\n    Transform the atoms in `self` by `transform`.\n    If `selection` is given, only transform the atoms in `selection`.\n    \"\"\"\n    transform = Transform3D.make(transform)\n    selection = _selection_to_numpy(self, selection)\n    transformed = self.with_coords(Transform3D.make(transform) @ self.coords(selection), selection)\n    # try to transform velocities as well\n    if transform_velocities and (velocities := self.velocities(selection)) is not None:\n        return transformed.with_velocity(transform.transform_vec(velocities), selection)\n    return transformed\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.round_near_zero","title":"round_near_zero","text":"
    round_near_zero(tol: float = 1e-14) -> Self\n

    Round atom position values near zero to zero.

    Source code in atomlib/atoms.py
    def round_near_zero(self, tol: float = 1e-14) -> Self:\n    \"\"\"\n    Round atom position values near zero to zero.\n    \"\"\"\n    return self.with_columns(coords=polars.concat_list(\n        polars.when(_coord_expr(col).abs() >= tol).then(_coord_expr(col)).otherwise(polars.lit(0.))\n        for col in range(3)\n    ).list.to_array(3))\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.crop","title":"crop","text":"
    crop(\n    x_min: float = -inf,\n    x_max: float = inf,\n    y_min: float = -inf,\n    y_max: float = inf,\n    z_min: float = -inf,\n    z_max: float = inf,\n) -> Self\n

    Crop, removing all atoms outside of the specified region, inclusive.

    Source code in atomlib/atoms.py
    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf) -> Self:\n    \"\"\"\n    Crop, removing all atoms outside of the specified region, inclusive.\n    \"\"\"\n\n    return self.filter(\n        self.x().is_between(x_min, x_max, closed='both'),\n        self.y().is_between(y_min, y_max, closed='both'),\n        self.z().is_between(z_min, z_max, closed='both'),\n    )\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.deduplicate","title":"deduplicate","text":"
    deduplicate(\n    tol: float = 0.001,\n    subset: Iterable[str] = (\"x\", \"y\", \"z\", \"symbol\"),\n    keep: UniqueKeepStrategy = \"first\",\n    maintain_order: bool = True,\n) -> Self\n

    De-duplicate atoms in self. Atoms of the same symbol that are closer than tolerance to each other (by Euclidian distance) will be removed, leaving only the atom specified by keep (defaults to the first atom).

    If subset is specified, only those columns will be included while assessing duplicates. Floating point columns other than 'x', 'y', and 'z' will not by toleranced.

    Source code in atomlib/atoms.py
    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n    \"\"\"\n    De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n    to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n    `keep` (defaults to the first atom).\n\n    If `subset` is specified, only those columns will be included while assessing duplicates.\n    Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n    \"\"\"\n    import scipy.spatial\n\n    cols = set((subset,) if isinstance(subset, str) else subset)\n\n    indices = numpy.arange(len(self))\n\n    spatial_cols = cols.intersection(('x', 'y', 'z'))\n    cols -= spatial_cols\n    if len(spatial_cols) > 0:\n        coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n        tree = scipy.spatial.KDTree(coords)\n\n        # TODO This is a bad algorithm\n        while True:\n            changed = False\n            for (i, j) in tree.query_pairs(tol, 2.):\n                # whenever we encounter a pair, ensure their index matches\n                i_i, i_j = indices[[i, j]]\n                if i_i != i_j:\n                    indices[i] = indices[j] = min(i_i, i_j)\n                    changed = True\n            if not changed:\n                break\n\n        self = self.with_column(polars.Series('_unique_pts', indices))\n        cols.add('_unique_pts')\n\n    frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n    if len(spatial_cols) > 0:\n        frame = frame.drop('_unique_pts')\n\n    return self.with_atoms(Atoms(frame, _unchecked=True))\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_bounds","title":"with_bounds","text":"
    with_bounds(\n    cell_size: Optional[VecLike] = None,\n    cell_origin: Optional[VecLike] = None,\n) -> \"AtomCell\"\n

    Return a periodic cell with the given orthogonal cell dimensions.

    If cell_size is not specified, it will be assumed (and may be incorrect).

    Source code in atomlib/atoms.py
    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n    \"\"\"\n    Return a periodic cell with the given orthogonal cell dimensions.\n\n    If cell_size is not specified, it will be assumed (and may be incorrect).\n    \"\"\"\n    # TODO: test this\n    from .atomcell import AtomCell\n\n    if cell_size is None:\n        warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n        cell_size = self.bbox().size\n        cell_origin = self.bbox().min\n\n    # TODO test this origin code\n    cell = Cell.from_unit_cell(cell_size)\n    if cell_origin is not None:\n        cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n    return AtomCell(self.get_atoms(), cell, frame='local')\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.coords","title":"coords","text":"
    coords(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Literal[\"local\"] = \"local\"\n) -> NDArray[float64]\n

    Return a (N, 3) ndarray of atom coordinates (dtype numpy.float64).

    Source code in atomlib/atoms.py
    def coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> NDArray[numpy.float64]:\n    \"\"\"Return a `(N, 3)` ndarray of atom coordinates (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n    df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n    return df.get_column('coords').to_numpy().astype(numpy.float64)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.x","title":"x","text":"
    x() -> Expr\n
    Source code in atomlib/atoms.py
    def x(self) -> polars.Expr:\n    return polars.col('coords').arr.get(0).alias('x')\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.y","title":"y","text":"
    y() -> Expr\n
    Source code in atomlib/atoms.py
    def y(self) -> polars.Expr:\n    return polars.col('coords').arr.get(1).alias('y')\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.z","title":"z","text":"
    z() -> Expr\n
    Source code in atomlib/atoms.py
    def z(self) -> polars.Expr:\n    return polars.col('coords').arr.get(2).alias('z')\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.velocities","title":"velocities","text":"
    velocities(\n    selection: Optional[AtomSelection] = None,\n) -> Optional[NDArray[float64]]\n

    Return a (N, 3) ndarray of atom velocities (dtype numpy.float64).

    Source code in atomlib/atoms.py
    def velocities(self, selection: t.Optional[AtomSelection] = None) -> t.Optional[NDArray[numpy.float64]]:\n    \"\"\"Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n    if 'velocity' not in self:\n        return None\n\n    df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n    return df.get_column('velocity').to_numpy().astype(numpy.float64)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.types","title":"types","text":"
    types() -> Optional[Series]\n

    Returns a Series of atom types (dtype polars.Int32).

    Source code in atomlib/atoms.py
    def types(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('type')\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.masses","title":"masses","text":"
    masses() -> Optional[Series]\n

    Returns a Series of atom masses (dtype polars.Float32).

    Source code in atomlib/atoms.py
    def masses(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('mass')\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.add_atom","title":"add_atom","text":"
    add_atom(\n    elem: Union[int, str],\n    x: ArrayLike,\n    /,\n    *,\n    y: None = None,\n    z: None = None,\n    **kwargs: Any,\n) -> Self\n
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: float,\n    y: float,\n    z: float,\n    **kwargs: Any,\n) -> Self\n
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: Union[ArrayLike, float],\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    **kwargs: Any,\n) -> Self\n

    Return a copy of self with an extra atom.

    By default, all extra columns present in self must be specified as **kwargs.

    Try to avoid calling this in a loop (Use HasAtoms.concat instead).

    Source code in atomlib/atoms.py
    def add_atom(self, elem: t.Union[int, str], /,\n             x: t.Union[ArrayLike, float],\n             y: t.Optional[float] = None,\n             z: t.Optional[float] = None,\n             **kwargs: t.Any) -> Self:\n    \"\"\"\n    Return a copy of `self` with an extra atom.\n\n    By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n    Try to avoid calling this in a loop (Use [`HasAtoms.concat`][atomlib.atoms.HasAtoms.concat] instead).\n    \"\"\"\n    if isinstance(elem, int):\n        kwargs.update(elem=elem)\n    else:\n        kwargs.update(symbol=elem)\n    if hasattr(x, '__len__') and len(x) > 1:  # type: ignore\n        (x, y, z) = to_vec3(x)\n    elif y is None or z is None:\n        raise ValueError(\"Must specify vector of positions or x, y, & z.\")\n\n    sym = get_sym(elem) if isinstance(elem, int) else elem\n    d: t.Dict[str, t.Any] = {'x': x, 'y': y, 'z': z, 'symbol': sym, **kwargs}\n    return self.concat(\n        (self, Atoms(d).select_schema(self.schema)),\n        how='vertical'\n    )\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.pos","title":"pos","text":"
    pos(\n    x: Sequence[Optional[float]],\n    /,\n    *,\n    y: None = None,\n    z: None = None,\n    tol: float = 1e-06,\n    **kwargs: Any,\n) -> Expr\n
    pos(\n    x: Optional[float] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n
    pos(\n    x: Union[Sequence[Optional[float]], float, None] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n

    Select all atoms at a given position.

    Formally, returns all atoms within a cube of radius tol centered at (x,y,z), exclusive of the cube's surface.

    Additional parameters given as kwargs will be checked as additional parameters (with strict equality).

    Source code in atomlib/atoms.py
    def pos(self,\n        x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n        y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n        tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n    \"\"\"\n    Select all atoms at a given position.\n\n    Formally, returns all atoms within a cube of radius ``tol``\n    centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n    Additional parameters given as ``kwargs`` will be checked\n    as additional parameters (with strict equality).\n    \"\"\"\n\n    if isinstance(x, t.Sequence):\n        (x, y, z) = x\n\n    tol = abs(float(tol))\n    selection = polars.lit(True)\n    if x is not None:\n        selection &= self.x().is_between(x - tol, x + tol, closed='none')\n    if y is not None:\n        selection &= self.y().is_between(y - tol, y + tol, closed='none')\n    if z is not None:\n        selection &= self.z().is_between(z - tol, z + tol, closed='none')\n    for (col, val) in kwargs.items():\n        selection &= (polars.col(col) == val)\n\n    return selection\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_index","title":"with_index","text":"
    with_index(index: Optional[AtomValues] = None) -> Self\n

    Returns self with a row index added in column 'i' (dtype polars.Int64). If index is not specified, defaults to an existing index or a new index.

    Source code in atomlib/atoms.py
    def with_index(self, index: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n    If `index` is not specified, defaults to an existing index or a new index.\n    \"\"\"\n    if index is None and 'i' in self.columns:\n        return self\n    if index is None:\n        index = numpy.arange(len(self), dtype=numpy.int64)\n    return self.with_column(_values_to_expr(self, index, polars.Int64).alias('i'))\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_wobble","title":"with_wobble","text":"
    with_wobble(wobble: Optional[AtomValues] = None) -> Self\n

    Return self with the given displacements in column 'wobble' (dtype polars.Float64). If wobble is not specified, defaults to the already-existing wobbles or 0.

    Source code in atomlib/atoms.py
    def with_wobble(self, wobble: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n    \"\"\"\n    if wobble is None and 'wobble' in self.columns:\n        return self\n    wobble = 0. if wobble is None else wobble\n    return self.with_column(_values_to_expr(self, wobble, polars.Float64).alias('wobble'))\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_occupancy","title":"with_occupancy","text":"
    with_occupancy(\n    frac_occupancy: Optional[AtomValues] = None,\n) -> Self\n

    Return self with the given fractional occupancies (dtype polars.Float64). If frac_occupancy is not specified, defaults to the already-existing occupancies or 1.

    Source code in atomlib/atoms.py
    def with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n    \"\"\"\n    if frac_occupancy is None and 'frac_occupancy' in self.columns:\n        return self\n    frac_occupancy = 1. if frac_occupancy is None else frac_occupancy\n    return self.with_column(_values_to_expr(self, frac_occupancy, polars.Float64).alias('frac_occupancy'))\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.apply_wobble","title":"apply_wobble","text":"
    apply_wobble(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    Displace the atoms in self by the amount in the wobble column. wobble is interpretated as a mean-squared displacement, which is distributed equally over each axis.

    Source code in atomlib/atoms.py
    def apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    Displace the atoms in `self` by the amount in the `wobble` column.\n    `wobble` is interpretated as a mean-squared displacement, which is distributed\n    equally over each axis.\n    \"\"\"\n    if 'wobble' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    stddev = self.select((polars.col('wobble') / 3.).sqrt()).to_series().to_numpy()\n    coords = self.coords()\n    coords += stddev[:, None] * rng.standard_normal(coords.shape)\n    return self.with_coords(coords)\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.apply_occupancy","title":"apply_occupancy","text":"
    apply_occupancy(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    For each atom in self, use its frac_occupancy to randomly decide whether to remove it.

    Source code in atomlib/atoms.py
    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n    \"\"\"\n    if 'frac_occupancy' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    frac = self.select('frac_occupancy').to_series().to_numpy()\n    choice = rng.binomial(1, frac).astype(numpy.bool_)\n    return self.filter(polars.lit(choice))\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_type","title":"with_type","text":"
    with_type(types: Optional[AtomValues] = None) -> Self\n

    Return self with the given atom types in column 'type'. If types is not specified, use the already existing types or auto-assign them.

    When auto-assigning, each symbol is given a unique value, case-sensitive. Values are assigned from lowest atomic number to highest. For instance: [\"Ag+\", \"Na\", \"H\", \"Ag\"] => [3, 11, 1, 2]

    Source code in atomlib/atoms.py
    def with_type(self, types: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom types in column 'type'.\n    If `types` is not specified, use the already existing types or auto-assign them.\n\n    When auto-assigning, each symbol is given a unique value, case-sensitive.\n    Values are assigned from lowest atomic number to highest.\n    For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n    \"\"\"\n    if types is not None:\n        return self.with_columns(type=_values_to_expr(self, types, polars.Int32))\n    if 'type' in self.columns:\n        return self\n\n    unique = Atoms(self._get_frame().unique(maintain_order=False, subset=['elem', 'symbol']).sort(['elem', 'symbol']), _unchecked=True)\n    new = self.with_column(polars.Series('type', values=numpy.zeros(len(self)), dtype=polars.Int32))\n\n    logging.warning(\"Auto-assigning element types\")\n    for (i, (elem, sym)) in enumerate(unique.select(('elem', 'symbol')).rows()):\n        print(f\"Assigning type {i+1} to element '{sym}'\")\n        new = new.with_column(polars.when((polars.col('elem') == elem) & (polars.col('symbol') == sym))\n                                    .then(polars.lit(i+1))\n                                    .otherwise(polars.col('type'))\n                                    .alias('type'))\n\n    assert (new.get_column('type') == 0).sum() == 0\n    return new\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_mass","title":"with_mass","text":"
    with_mass(mass: Optional[ArrayLike] = None) -> Self\n

    Return self with the given atom masses in column 'mass'. If mass is not specified, use the already existing masses or auto-assign them.

    Source code in atomlib/atoms.py
    def with_mass(self, mass: t.Optional[ArrayLike] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom masses in column `'mass'`.\n    If `mass` is not specified, use the already existing masses or auto-assign them.\n    \"\"\"\n    if mass is not None:\n        return self.with_column(_values_to_expr(self, mass, polars.Float32).alias('mass'))\n    if 'mass' in self.columns:\n        return self\n\n    unique_elems = self.get_column('elem').unique()\n    new = self.with_column(polars.Series('mass', values=numpy.zeros(len(self)), dtype=polars.Float32))\n\n    logging.warning(\"Auto-assigning element masses\")\n    for elem in unique_elems:\n        new = new.with_column(polars.when(polars.col('elem') == elem)\n                                    .then(polars.lit(get_mass(elem)))\n                                    .otherwise(polars.col('mass'))\n                                    .alias('mass'))\n\n    assert (new.get_column('mass').abs() < 1e-10).sum() == 0\n    return new\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_symbol","title":"with_symbol","text":"
    with_symbol(\n    symbols: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n) -> Self\n

    Return self with the given atomic symbols.

    Source code in atomlib/atoms.py
    def with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atomic symbols.\n    \"\"\"\n    if selection is not None:\n        selection = _selection_to_numpy(self, selection)\n        new_symbols = self.get_column('symbol')\n        new_symbols[selection] = polars.Series(list(numpy.broadcast_to(symbols, len(selection))), dtype=polars.Utf8)\n        symbols = new_symbols\n\n    # TODO better cast here\n    symbols = polars.Series('symbol', list(numpy.broadcast_to(symbols, len(self))), dtype=polars.Utf8)\n    return self.with_columns((symbols, get_elem(symbols)))\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_coords","title":"with_coords","text":"
    with_coords(\n    pts: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Literal[\"local\"] = \"local\"\n) -> Self\n

    Return self replaced with the given atomic positions.

    Source code in atomlib/atoms.py
    def with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic positions.\n    \"\"\"\n    if selection is not None:\n        selection = _selection_to_numpy(self, selection)\n        new_pts = self.coords()\n        pts = numpy.atleast_2d(pts)\n        assert pts.shape[-1] == 3\n        new_pts[selection] = pts\n        pts = new_pts\n\n    # https://github.com/pola-rs/polars/issues/18369\n    pts = numpy.broadcast_to(pts, (len(self), 3)) if len(self) else []\n    return self.with_columns(polars.Series('coords', pts, polars.Array(polars.Float64, 3)))\n
    "},{"location":"api/atoms/#atomlib.atoms.HasAtoms.with_velocity","title":"with_velocity","text":"
    with_velocity(\n    pts: Optional[ArrayLike] = None,\n    selection: Optional[AtomSelection] = None,\n) -> Self\n

    Return self replaced with the given atomic velocities. If pts is not specified, use the already existing velocities or zero.

    Source code in atomlib/atoms.py
    def with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                  selection: t.Optional[AtomSelection] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic velocities.\n    If `pts` is not specified, use the already existing velocities or zero.\n    \"\"\"\n    if pts is None:\n        if 'velocity' in self:\n            return self\n        all_pts = numpy.zeros((len(self), 3))\n    else:\n        all_pts = self['velocity'].to_numpy()\n\n    if selection is None:\n        all_pts = pts or all_pts\n    elif pts is not None:\n        selection = _selection_to_numpy(self, selection)\n        all_pts = numpy.require(all_pts, requirements=['WRITEABLE'])\n        pts = numpy.atleast_2d(pts)\n        assert pts.shape[-1] == 3\n        all_pts[selection] = pts\n\n    all_pts = numpy.broadcast_to(all_pts, (len(self), 3))\n    return self.with_columns(polars.Series('velocity', all_pts, polars.Array(polars.Float64, 3)))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms","title":"Atoms","text":"

    Bases: AtomsIOMixin, HasAtoms

    A collection of atoms, absent any implied coordinate system. Implemented as a wrapper around a polars.DataFrame.

    Must contain the following columns:

    • coords: array of [x, y, z] positions, float
    • elem: atomic number, int
    • symbol: atomic symbol (may contain charges)

    In addition, it commonly contains the following columns:

    • i: Initial atom number
    • wobble: Isotropic Debye-Waller mean-squared deviation (\\(\\left<u^2\\right> = B \\cdot \\frac{3}{8 \\pi^2}\\), dimensions of [Length^2])
    • frac_occupancy: Fractional occupancy, in the range [0., 1.]
    • mass: Atomic mass, in g/mol (approx. Da)
    • velocity: array of [x, y, z] velocities, float, dimensions of length/time
    • type: Numeric atom type, as used by programs like LAMMPS
    Source code in atomlib/atoms.py
    class Atoms(AtomsIOMixin, HasAtoms):\n    r\"\"\"\n    A collection of atoms, absent any implied coordinate system.\n    Implemented as a wrapper around a [`polars.DataFrame`][polars.DataFrame].\n\n    Must contain the following columns:\n\n    - coords: array of `[x, y, z]` positions, float\n    - elem: atomic number, int\n    - symbol: atomic symbol (may contain charges)\n\n    In addition, it commonly contains the following columns:\n\n    - i: Initial atom number\n    - wobble: Isotropic Debye-Waller mean-squared deviation ($\\left<u^2\\right> = B \\cdot \\frac{3}{8 \\pi^2}$, dimensions of [Length^2])\n    - frac_occupancy: Fractional occupancy, in the range [0., 1.]\n    - mass: Atomic mass, in g/mol (approx. Da)\n    - velocity: array of `[x, y, z]` velocities, float, dimensions of length/time\n    - type: Numeric atom type, as used by programs like LAMMPS\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n\n    def __init__(self, data: t.Optional[IntoAtoms] = None, columns: t.Optional[t.Sequence[str]] = None,\n                 orient: t.Union[t.Literal['row'], t.Literal['col'], None] = None,\n                 _unchecked: bool = False):\n        self._bbox: t.Optional[BBox3D] = None\n        self.inner: polars.DataFrame\n\n        if data is None:\n            assert columns is None\n            self.inner = polars.DataFrame([\n                polars.Series('coords', (), dtype=polars.Array(polars.Float64, 3)),\n                polars.Series('elem', (), dtype=polars.Int8),\n                polars.Series('symbol', (), dtype=polars.Utf8),\n            ])\n        elif isinstance(data, polars.DataFrame):\n            self.inner = data\n        elif isinstance(data, Atoms):\n            self.inner = data.inner\n            _unchecked = True\n        else:\n            self.inner = polars.DataFrame(data, schema=columns, orient=orient)\n\n        if not _unchecked:\n            # stack ('x', 'y', 'z') -> 'coords'\n            self.inner = _with_columns_stacked(self.inner, ('x', 'y', 'z'), 'coords')\n            self.inner = _with_columns_stacked(self.inner, ('v_x', 'v_y', 'v_z'), 'velocity')\n\n            missing: t.Tuple[str, ...] = tuple(set(['symbol', 'elem']) - set(self.columns))\n            if len(missing) > 1:\n                raise ValueError(\"'Atoms' missing columns 'elem' and/or 'symbol'.\")\n            # fill 'symbol' from 'elem' or vice-versa\n            if missing == ('symbol',):\n                self.inner = self.inner.with_columns(get_sym(self.inner['elem']))\n            elif missing == ('elem',):\n                # by convention, add before 'symbol' column\n                self.inner = self.inner.insert_column(\n                    self.inner.get_column_index('symbol'),\n                    get_elem(self.inner['symbol']),\n                )\n\n            # cast to standard dtypes\n            self.inner = self.inner.with_columns([\n                self.inner[col].cast(dtype)\n                for (col, dtype) in _COLUMN_DTYPES.items() if col in self.inner\n            ])\n\n            self._validate_atoms()\n\n    @staticmethod\n    def empty() -> Atoms:\n        \"\"\"\n        Return an empty Atoms with only the mandatory columns.\n        \"\"\"\n        return Atoms()\n\n    def _validate_atoms(self):\n        missing = [col for col in _REQUIRED_COLUMNS if col not in self.columns]\n        if len(missing):\n            raise ValueError(f\"'Atoms' missing column(s) {', '.join(map(repr, missing))}\")\n\n    def get_atoms(self, frame: t.Literal['local'] = 'local') -> Atoms:\n        if frame != 'local':\n            raise ValueError(f\"Atoms without a cell only support the 'local' coordinate frame, not '{frame}'.\")\n        return self\n\n    def with_atoms(self, atoms: HasAtoms, frame: t.Literal['local'] = 'local') -> Atoms:\n        if frame != 'local':\n            raise ValueError(f\"Atoms without a cell only support the 'local' coordinate frame, not '{frame}'.\")\n        return atoms.get_atoms()\n\n    @classmethod\n    def _combine_metadata(cls: t.Type[Atoms], *atoms: HasAtoms) -> Atoms:\n        return cls.empty()\n\n    def bbox(self) -> BBox3D:\n        \"\"\"Return the bounding box of all the points in `self`.\"\"\"\n        if self._bbox is None:\n            self._bbox = BBox3D.from_pts(self.coords())\n\n        return self._bbox\n\n    def __str__(self) -> str:\n        return f\"Atoms, {self.inner!s}\"\n\n    def __repr__(self) -> str:\n        buf = StringIO()\n        buf.write(\"Atoms([\\n\")\n\n        for series in self.inner.to_dict().values():\n            buf.write(f\"    Series({series.name!r}, {series.to_list()!r}, dtype={series.dtype!r}),\\n\")\n\n        buf.write(\"])\\n\")\n        return buf.getvalue()\n\n    def _repr_pretty_(self, p: t.Any, cycle: bool) -> None:\n        p.text('Atoms(...)') if cycle else p.text(str(self))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.columns","title":"columns property","text":"
    columns: List[str]\n

    Return the column names in self.

    RETURNS DESCRIPTION List[str]

    A sequence of column names

    "},{"location":"api/atoms/#atomlib.atoms.Atoms.dtypes","title":"dtypes property","text":"
    dtypes: List[DataType]\n

    Return the datatypes in self.

    RETURNS DESCRIPTION List[DataType]

    A sequence of column DataTypes

    "},{"location":"api/atoms/#atomlib.atoms.Atoms.schema","title":"schema property","text":"
    schema: Schema\n

    Return the schema of self.

    RETURNS DESCRIPTION Schema

    A dictionary of column names and DataTypes

    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_column","title":"with_column class-attribute instance-attribute","text":"
    with_column = with_columns\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.transform","title":"transform class-attribute instance-attribute","text":"
    transform = transform_atoms\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.crop_atoms","title":"crop_atoms class-attribute instance-attribute","text":"
    crop_atoms = crop\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.unique","title":"unique class-attribute instance-attribute","text":"
    unique = deduplicate\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.inner","title":"inner instance-attribute","text":"
    inner: DataFrame\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.describe","title":"describe","text":"
    describe(\n    percentiles: Union[Sequence[float], float, None] = (\n        0.25,\n        0.5,\n        0.75,\n    ),\n    *,\n    interpolation: RollingInterpolationMethod = \"nearest\"\n) -> DataFrame\n

    Return summary statistics for self. See DataFrame.describe for more information.

    PARAMETER DESCRIPTION percentiles

    List of percentiles/quantiles to include. Defaults to 25% (first quartile), 50% (median), and 75% (third quartile).

    TYPE: Union[Sequence[float], float, None] DEFAULT: (0.25, 0.5, 0.75)

    RETURNS DESCRIPTION DataFrame

    A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.describe)\ndef describe(self, percentiles: t.Union[t.Sequence[float], float, None] = (0.25, 0.5, 0.75), *,\n             interpolation: RollingInterpolationMethod = 'nearest') -> polars.DataFrame:\n    \"\"\"\n    Return summary statistics for `self`. See [`DataFrame.describe`][polars.DataFrame.describe] for more information.\n\n    Args:\n      percentiles: List of percentiles/quantiles to include. Defaults to 25% (first quartile),\n                   50% (median), and 75% (third quartile).\n\n    Returns:\n      A dataframe containing summary statistics (mean, std. deviation, percentiles, etc.) for each column.\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_columns","title":"with_columns","text":"
    with_columns(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Return a copy of self with the given columns added.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef with_columns(self,\n                 *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n                 **named_exprs: IntoExpr) -> polars.DataFrame:\n    \"\"\"Return a copy of `self` with the given columns added.\"\"\"\n    return self._get_frame().with_columns(*exprs, **named_exprs)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.insert_column","title":"insert_column","text":"
    insert_column(index: int, column: Series) -> DataFrame\n
    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef insert_column(self, index: int, column: polars.Series) -> polars.DataFrame:\n    return self._get_frame().insert_column(index, column)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.get_column","title":"get_column","text":"
    get_column(name: str) -> Series\n

    Get the specified column from self, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(lambda df, name: df.get_column(name))\ndef get_column(self, name: str) -> polars.Series:\n    \"\"\"\n    Get the specified column from `self`, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.get_columns","title":"get_columns","text":"
    get_columns() -> List[Series]\n

    Return all columns from self as a list of Series.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_columns)\ndef get_columns(self) -> t.List[polars.Series]:\n    \"\"\"\n    Return all columns from `self` as a list of [`Series`][polars.Series].\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.get_column_index","title":"get_column_index","text":"
    get_column_index(name: str) -> int\n

    Get the index of a column by name, raising polars.ColumnNotFoundError if it's not present.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.get_column_index)\ndef get_column_index(self, name: str) -> int:\n    \"\"\"Get the index of a column by name, raising [`polars.ColumnNotFoundError`][polars.exceptions.ColumnNotFoundError] if it's not present.\"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.group_by","title":"group_by","text":"
    group_by(\n    *by: Union[IntoExpr, Iterable[IntoExpr]],\n    maintain_order: bool = False,\n    **named_by: IntoExpr\n) -> GroupBy\n

    Start a group by operation. See DataFrame.group_by for more information.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.group_by)\ndef group_by(self, *by: t.Union[IntoExpr, t.Iterable[IntoExpr]], maintain_order: bool = False,\n             **named_by: IntoExpr) -> polars.dataframe.group_by.GroupBy:\n    \"\"\"\n    Start a group by operation. See [`DataFrame.group_by`][polars.DataFrame.group_by] for more information.\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.pipe","title":"pipe","text":"
    pipe(\n    function: Callable[Concatenate[HasAtomsT, P], T],\n    *args: args,\n    **kwargs: kwargs\n) -> T\n

    Apply function to self (in method-call syntax).

    Source code in atomlib/atoms.py
    def pipe(self: HasAtomsT, function: t.Callable[Concatenate[HasAtomsT, P], T], *args: P.args, **kwargs: P.kwargs) -> T:\n    \"\"\"Apply `function` to `self` (in method-call syntax).\"\"\"\n    return function(self, *args, **kwargs)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.clone","title":"clone","text":"
    clone() -> DataFrame\n

    Return a copy of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef clone(self) -> polars.DataFrame:\n    \"\"\"Return a copy of `self`.\"\"\"\n    return self._get_frame().clone()\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.drop","title":"drop","text":"
    drop(\n    *columns: Union[str, Iterable[str]], strict: bool = True\n) -> DataFrame\n

    Return self with the specified columns removed.

    Source code in atomlib/atoms.py
    def drop(self, *columns: t.Union[str, t.Iterable[str]], strict: bool = True) -> polars.DataFrame:\n    \"\"\"Return `self` with the specified columns removed.\"\"\"\n    return self._get_frame().drop(*columns, strict=strict)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.filter","title":"filter","text":"
    filter(\n    *predicates: Union[\n        None,\n        IntoExprColumn,\n        Iterable[IntoExprColumn],\n        bool,\n        List[bool],\n        ndarray,\n    ],\n    **constraints: Any\n) -> Self\n

    Filter self, removing rows which evaluate to False.

    Source code in atomlib/atoms.py
    def filter(\n    self,\n    *predicates: t.Union[None, IntoExprColumn, t.Iterable[IntoExprColumn], bool, t.List[bool], numpy.ndarray],\n    **constraints: t.Any,\n) -> Self:\n    \"\"\"Filter `self`, removing rows which evaluate to `False`.\"\"\"\n    # TODO clean up\n    preds_not_none = tuple(filter(lambda p: p is not None, predicates))\n    if not len(preds_not_none) and not len(constraints):\n        return self\n    return self.with_atoms(Atoms(self._get_frame().filter(*preds_not_none, **constraints), _unchecked=True))  # type: ignore\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.sort","title":"sort","text":"
    sort(\n    by: Union[IntoExpr, Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: Union[bool, Sequence[bool]] = False,\n    nulls_last: bool = False\n) -> DataFrame\n

    Sort the atoms in self by the given columns/expressions.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef sort(\n    self,\n    by: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    *more_by: IntoExpr,\n    descending: t.Union[bool, t.Sequence[bool]] = False,\n    nulls_last: bool = False,\n) -> polars.DataFrame:\n    \"\"\"\n    Sort the atoms in `self` by the given columns/expressions.\n    \"\"\"\n    return self._get_frame().sort(\n        by, *more_by, descending=descending, nulls_last=nulls_last\n    )\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.slice","title":"slice","text":"
    slice(\n    offset: int, length: Optional[int] = None\n) -> DataFrame\n

    Return a slice of the rows in self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef slice(self, offset: int, length: t.Optional[int] = None) -> polars.DataFrame:\n    \"\"\"Return a slice of the rows in `self`.\"\"\"\n    return self._get_frame().slice(offset, length)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.head","title":"head","text":"
    head(n: int = 5) -> DataFrame\n

    Return the first n rows of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef head(self, n: int = 5) -> polars.DataFrame:\n    \"\"\"Return the first `n` rows of `self`.\"\"\"\n    return self._get_frame().head(n)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.tail","title":"tail","text":"
    tail(n: int = 5) -> DataFrame\n

    Return the last n rows of self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef tail(self, n: int = 5) -> polars.DataFrame:\n    \"\"\"Return the last `n` rows of `self`.\"\"\"\n    return self._get_frame().tail(n)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.drop_nulls","title":"drop_nulls","text":"
    drop_nulls(\n    subset: Union[str, Collection[str], None] = None\n) -> DataFrame\n

    Drop rows that contain nulls in any of columns subset.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef drop_nulls(self, subset: t.Union[str, t.Collection[str], None] = None) -> polars.DataFrame:\n    \"\"\"Drop rows that contain nulls in any of columns `subset`.\"\"\"\n    return self._get_frame().drop_nulls(subset)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.fill_null","title":"fill_null","text":"
    fill_null(\n    value: Any = None,\n    strategy: Optional[FillNullStrategy] = None,\n    limit: Optional[int] = None,\n    matches_supertype: bool = True,\n) -> DataFrame\n

    Fill null values in self, using the specified value or strategy.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef fill_null(\n    self, value: t.Any = None, strategy: t.Optional[FillNullStrategy] = None,\n    limit: t.Optional[int] = None, matches_supertype: bool = True,\n) -> polars.DataFrame:\n    \"\"\"Fill null values in `self`, using the specified value or strategy.\"\"\"\n    return self._get_frame().fill_null(value, strategy, limit, matches_supertype=matches_supertype)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.fill_nan","title":"fill_nan","text":"
    fill_nan(value: Union[Expr, int, float, None]) -> DataFrame\n

    Fill floating-point NaN values in self.

    Source code in atomlib/atoms.py
    @_fwd_frame_map\ndef fill_nan(self, value: t.Union[polars.Expr, int, float, None]) -> polars.DataFrame:\n    \"\"\"Fill floating-point NaN values in `self`.\"\"\"\n    return self._get_frame().fill_nan(value)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.concat","title":"concat classmethod","text":"
    concat(\n    atoms: Union[\n        HasAtomsT,\n        IntoAtoms,\n        Iterable[Union[HasAtomsT, IntoAtoms]],\n    ],\n    *,\n    rechunk: bool = True,\n    how: ConcatMethod = \"vertical\"\n) -> HasAtomsT\n

    Concatenate multiple Atoms together, handling metadata appropriately.

    Source code in atomlib/atoms.py
    @classmethod\ndef concat(cls: t.Type[HasAtomsT],\n           atoms: t.Union[HasAtomsT, IntoAtoms, t.Iterable[t.Union[HasAtomsT, IntoAtoms]]], *,\n           rechunk: bool = True, how: ConcatMethod = 'vertical') -> HasAtomsT:\n    \"\"\"Concatenate multiple `Atoms` together, handling metadata appropriately.\"\"\"\n    # this method is tricky. It needs to accept raw Atoms, as well as HasAtoms of the\n    # same type as ``cls``.\n    if _is_abstract(cls):\n        raise TypeError(\"concat() must be called on a concrete class.\")\n\n    if isinstance(atoms, HasAtoms):\n        atoms = (atoms,)\n    dfs = [a.get_atoms('local').inner if isinstance(a, HasAtoms) else Atoms(t.cast(IntoAtoms, a)).inner for a in atoms]\n    representative = cls._combine_metadata(*(a for a in atoms if isinstance(a, HasAtoms)))\n\n    if len(dfs) == 0:\n        return representative.with_atoms(Atoms.empty(), 'local')\n\n    if how in ('vertical', 'vertical_relaxed'):\n        # get order from first member\n        cols = dfs[0].columns\n        dfs = [df.select(cols) for df in dfs]\n    elif how == 'inner':\n        cols = reduce(operator.and_, (df.schema.keys() for df in dfs))\n        schema = OrderedDict((col, dfs[0].schema[col]) for col in cols)\n        if len(schema) == 0:\n            raise ValueError(\"Atoms have no columns in common\")\n\n        dfs = [_select_schema(df, schema) for df in dfs]\n        how = 'vertical'\n\n    return representative.with_atoms(Atoms(polars.concat(dfs, rechunk=rechunk, how=how)), 'local')\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.partition_by","title":"partition_by","text":"
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: Literal[False] = False\n) -> List[Self]\n
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: Literal[True] = ...\n) -> Dict[Any, Self]\n
    partition_by(\n    by: Union[str, Sequence[str]],\n    *more_by: str,\n    maintain_order: bool = True,\n    include_key: bool = True,\n    as_dict: bool = False\n) -> Union[List[Self], Dict[Any, Self]]\n

    Group by the given columns and partition into separate dataframes.

    Return the partitions as a dictionary by specifying as_dict=True.

    Source code in atomlib/atoms.py
    def partition_by(\n    self, by: t.Union[str, t.Sequence[str]], *more_by: str,\n    maintain_order: bool = True, include_key: bool = True, as_dict: bool = False\n) -> t.Union[t.List[Self], t.Dict[t.Any, Self]]:\n    \"\"\"\n    Group by the given columns and partition into separate dataframes.\n\n    Return the partitions as a dictionary by specifying `as_dict=True`.\n    \"\"\"\n    if as_dict:\n        d = self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=True)\n        return {k: self.with_atoms(Atoms(df, _unchecked=True)) for (k, df) in d.items()}\n\n    return [\n        self.with_atoms(Atoms(df, _unchecked=True))\n        for df in self._get_frame().partition_by(by, *more_by, maintain_order=maintain_order, include_key=include_key, as_dict=False)\n    ]\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.select","title":"select","text":"
    select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> DataFrame\n

    Select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns.

    Source code in atomlib/atoms.py
    @_fwd_frame(polars.DataFrame.select)\ndef select(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr,\n) -> polars.DataFrame:\n    \"\"\"\n    Select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    ...\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.select_schema","title":"select_schema","text":"
    select_schema(schema: SchemaDict) -> DataFrame\n

    Select columns from self and cast to the given schema. Raises TypeError if a column is not found or if it can't be cast.

    Source code in atomlib/atoms.py
    def select_schema(self, schema: SchemaDict) -> polars.DataFrame:\n    \"\"\"\n    Select columns from `self` and cast to the given schema.\n    Raises [`TypeError`][TypeError] if a column is not found or if it can't be cast.\n    \"\"\"\n    return _select_schema(self, schema)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.select_props","title":"select_props","text":"
    select_props(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Self\n

    Select exprs from self, while keeping required columns.

    RETURNS DESCRIPTION Self

    A HasAtoms filtered to contain the

    Self

    specified properties (as well as required columns).

    Source code in atomlib/atoms.py
    def select_props(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Self:\n    \"\"\"\n    Select `exprs` from `self`, while keeping required columns.\n\n    Returns:\n      A [`HasAtoms`][atomlib.atoms.HasAtoms] filtered to contain the\n      specified properties (as well as required columns).\n    \"\"\"\n    props = self._get_frame().lazy().select(*exprs, **named_exprs).drop(_REQUIRED_COLUMNS, strict=False).collect(_eager=True)\n    return self.with_atoms(\n        Atoms(self._get_frame().select(_REQUIRED_COLUMNS).hstack(props), _unchecked=False)\n    )\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.try_select","title":"try_select","text":"
    try_select(\n    *exprs: Union[IntoExpr, Iterable[IntoExpr]],\n    **named_exprs: IntoExpr\n) -> Optional[DataFrame]\n

    Try to select exprs from self, and return as a polars.DataFrame.

    Expressions may either be columns or expressions of columns. Returns None if any columns are missing.

    Source code in atomlib/atoms.py
    def try_select(\n    self,\n    *exprs: t.Union[IntoExpr, t.Iterable[IntoExpr]],\n    **named_exprs: IntoExpr,\n) -> t.Optional[polars.DataFrame]:\n    \"\"\"\n    Try to select `exprs` from `self`, and return as a [`polars.DataFrame`][polars.DataFrame].\n\n    Expressions may either be columns or expressions of columns. Returns `None` if any\n    columns are missing.\n\n    [polars.DataFrame]: https://docs.pola.rs/py-polars/html/reference/dataframe/index.html\n    \"\"\"\n    try:\n        return self._get_frame().select(*exprs, **named_exprs)\n    except polars.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.try_get_column","title":"try_get_column","text":"
    try_get_column(name: str) -> Optional[Series]\n

    Try to get a column from self, returning None if it doesn't exist.

    Source code in atomlib/atoms.py
    def try_get_column(self, name: str) -> t.Optional[polars.Series]:\n    \"\"\"Try to get a column from `self`, returning `None` if it doesn't exist.\"\"\"\n    try:\n        return self.get_column(name)\n    except polars.exceptions.ColumnNotFoundError:\n        return None\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n
    Source code in atomlib/atoms.py
    def assert_equal(self, other: t.Any):\n    assert isinstance(other, HasAtoms)\n    assert dict(self.schema) == dict(other.schema)\n    for col in self.schema.keys():\n        polars.testing.assert_series_equal(self[col], other[col], check_names=False, rtol=1e-3, atol=1e-8)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.bbox_atoms","title":"bbox_atoms","text":"
    bbox_atoms() -> BBox3D\n

    Return the bounding box of all the atoms in self.

    Source code in atomlib/atoms.py
    def bbox_atoms(self) -> BBox3D:\n    \"\"\"Return the bounding box of all the atoms in ``self``.\"\"\"\n    return BBox3D.from_pts(self.coords())\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.transform_atoms","title":"transform_atoms","text":"
    transform_atoms(\n    transform: IntoTransform3D,\n    selection: Optional[AtomSelection] = None,\n    *,\n    transform_velocities: bool = False\n) -> Self\n

    Transform the atoms in self by transform. If selection is given, only transform the atoms in selection.

    Source code in atomlib/atoms.py
    def transform_atoms(self, transform: IntoTransform3D, selection: t.Optional[AtomSelection] = None, *,\n                    transform_velocities: bool = False) -> Self:\n    \"\"\"\n    Transform the atoms in `self` by `transform`.\n    If `selection` is given, only transform the atoms in `selection`.\n    \"\"\"\n    transform = Transform3D.make(transform)\n    selection = _selection_to_numpy(self, selection)\n    transformed = self.with_coords(Transform3D.make(transform) @ self.coords(selection), selection)\n    # try to transform velocities as well\n    if transform_velocities and (velocities := self.velocities(selection)) is not None:\n        return transformed.with_velocity(transform.transform_vec(velocities), selection)\n    return transformed\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.round_near_zero","title":"round_near_zero","text":"
    round_near_zero(tol: float = 1e-14) -> Self\n

    Round atom position values near zero to zero.

    Source code in atomlib/atoms.py
    def round_near_zero(self, tol: float = 1e-14) -> Self:\n    \"\"\"\n    Round atom position values near zero to zero.\n    \"\"\"\n    return self.with_columns(coords=polars.concat_list(\n        polars.when(_coord_expr(col).abs() >= tol).then(_coord_expr(col)).otherwise(polars.lit(0.))\n        for col in range(3)\n    ).list.to_array(3))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.crop","title":"crop","text":"
    crop(\n    x_min: float = -inf,\n    x_max: float = inf,\n    y_min: float = -inf,\n    y_max: float = inf,\n    z_min: float = -inf,\n    z_max: float = inf,\n) -> Self\n

    Crop, removing all atoms outside of the specified region, inclusive.

    Source code in atomlib/atoms.py
    def crop(self, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf) -> Self:\n    \"\"\"\n    Crop, removing all atoms outside of the specified region, inclusive.\n    \"\"\"\n\n    return self.filter(\n        self.x().is_between(x_min, x_max, closed='both'),\n        self.y().is_between(y_min, y_max, closed='both'),\n        self.z().is_between(z_min, z_max, closed='both'),\n    )\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.deduplicate","title":"deduplicate","text":"
    deduplicate(\n    tol: float = 0.001,\n    subset: Iterable[str] = (\"x\", \"y\", \"z\", \"symbol\"),\n    keep: UniqueKeepStrategy = \"first\",\n    maintain_order: bool = True,\n) -> Self\n

    De-duplicate atoms in self. Atoms of the same symbol that are closer than tolerance to each other (by Euclidian distance) will be removed, leaving only the atom specified by keep (defaults to the first atom).

    If subset is specified, only those columns will be included while assessing duplicates. Floating point columns other than 'x', 'y', and 'z' will not by toleranced.

    Source code in atomlib/atoms.py
    def deduplicate(self, tol: float = 1e-3, subset: t.Iterable[str] = ('x', 'y', 'z', 'symbol'),\n                keep: UniqueKeepStrategy = 'first', maintain_order: bool = True) -> Self:\n    \"\"\"\n    De-duplicate atoms in `self`. Atoms of the same `symbol` that are closer than `tolerance`\n    to each other (by Euclidian distance) will be removed, leaving only the atom specified by\n    `keep` (defaults to the first atom).\n\n    If `subset` is specified, only those columns will be included while assessing duplicates.\n    Floating point columns other than 'x', 'y', and 'z' will not by toleranced.\n    \"\"\"\n    import scipy.spatial\n\n    cols = set((subset,) if isinstance(subset, str) else subset)\n\n    indices = numpy.arange(len(self))\n\n    spatial_cols = cols.intersection(('x', 'y', 'z'))\n    cols -= spatial_cols\n    if len(spatial_cols) > 0:\n        coords = self.select([_coord_expr(col).alias(col) for col in spatial_cols]).to_numpy()\n        tree = scipy.spatial.KDTree(coords)\n\n        # TODO This is a bad algorithm\n        while True:\n            changed = False\n            for (i, j) in tree.query_pairs(tol, 2.):\n                # whenever we encounter a pair, ensure their index matches\n                i_i, i_j = indices[[i, j]]\n                if i_i != i_j:\n                    indices[i] = indices[j] = min(i_i, i_j)\n                    changed = True\n            if not changed:\n                break\n\n        self = self.with_column(polars.Series('_unique_pts', indices))\n        cols.add('_unique_pts')\n\n    frame = self._get_frame().unique(subset=list(cols), keep=keep, maintain_order=maintain_order)\n    if len(spatial_cols) > 0:\n        frame = frame.drop('_unique_pts')\n\n    return self.with_atoms(Atoms(frame, _unchecked=True))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_bounds","title":"with_bounds","text":"
    with_bounds(\n    cell_size: Optional[VecLike] = None,\n    cell_origin: Optional[VecLike] = None,\n) -> \"AtomCell\"\n

    Return a periodic cell with the given orthogonal cell dimensions.

    If cell_size is not specified, it will be assumed (and may be incorrect).

    Source code in atomlib/atoms.py
    def with_bounds(self, cell_size: t.Optional[VecLike] = None, cell_origin: t.Optional[VecLike] = None) -> 'AtomCell':\n    \"\"\"\n    Return a periodic cell with the given orthogonal cell dimensions.\n\n    If cell_size is not specified, it will be assumed (and may be incorrect).\n    \"\"\"\n    # TODO: test this\n    from .atomcell import AtomCell\n\n    if cell_size is None:\n        warnings.warn(\"Cell boundary unknown. Defaulting to cell BBox\")\n        cell_size = self.bbox().size\n        cell_origin = self.bbox().min\n\n    # TODO test this origin code\n    cell = Cell.from_unit_cell(cell_size)\n    if cell_origin is not None:\n        cell = cell.transform_cell(AffineTransform3D.translate(to_vec3(cell_origin)))\n\n    return AtomCell(self.get_atoms(), cell, frame='local')\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.coords","title":"coords","text":"
    coords(\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Literal[\"local\"] = \"local\"\n) -> NDArray[float64]\n

    Return a (N, 3) ndarray of atom coordinates (dtype numpy.float64).

    Source code in atomlib/atoms.py
    def coords(self, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> NDArray[numpy.float64]:\n    \"\"\"Return a `(N, 3)` ndarray of atom coordinates (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n    df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n    return df.get_column('coords').to_numpy().astype(numpy.float64)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.x","title":"x","text":"
    x() -> Expr\n
    Source code in atomlib/atoms.py
    def x(self) -> polars.Expr:\n    return polars.col('coords').arr.get(0).alias('x')\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.y","title":"y","text":"
    y() -> Expr\n
    Source code in atomlib/atoms.py
    def y(self) -> polars.Expr:\n    return polars.col('coords').arr.get(1).alias('y')\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.z","title":"z","text":"
    z() -> Expr\n
    Source code in atomlib/atoms.py
    def z(self) -> polars.Expr:\n    return polars.col('coords').arr.get(2).alias('z')\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.velocities","title":"velocities","text":"
    velocities(\n    selection: Optional[AtomSelection] = None,\n) -> Optional[NDArray[float64]]\n

    Return a (N, 3) ndarray of atom velocities (dtype numpy.float64).

    Source code in atomlib/atoms.py
    def velocities(self, selection: t.Optional[AtomSelection] = None) -> t.Optional[NDArray[numpy.float64]]:\n    \"\"\"Return a `(N, 3)` ndarray of atom velocities (dtype [`numpy.float64`][numpy.float64]).\"\"\"\n    if 'velocity' not in self:\n        return None\n\n    df = self if selection is None else self.filter(_selection_to_expr(self, selection))\n    return df.get_column('velocity').to_numpy().astype(numpy.float64)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.types","title":"types","text":"
    types() -> Optional[Series]\n

    Returns a Series of atom types (dtype polars.Int32).

    Source code in atomlib/atoms.py
    def types(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom types (dtype [`polars.Int32`][polars.datatypes.Int32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('type')\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.masses","title":"masses","text":"
    masses() -> Optional[Series]\n

    Returns a Series of atom masses (dtype polars.Float32).

    Source code in atomlib/atoms.py
    def masses(self) -> t.Optional[polars.Series]:\n    \"\"\"\n    Returns a [`Series`][polars.Series] of atom masses (dtype [`polars.Float32`][polars.datatypes.Float32]).\n\n    [polars.Series]: https://docs.pola.rs/py-polars/html/reference/series/index.html\n    \"\"\"\n    return self.try_get_column('mass')\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.add_atom","title":"add_atom","text":"
    add_atom(\n    elem: Union[int, str],\n    x: ArrayLike,\n    /,\n    *,\n    y: None = None,\n    z: None = None,\n    **kwargs: Any,\n) -> Self\n
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: float,\n    y: float,\n    z: float,\n    **kwargs: Any,\n) -> Self\n
    add_atom(\n    elem: Union[int, str],\n    /,\n    x: Union[ArrayLike, float],\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    **kwargs: Any,\n) -> Self\n

    Return a copy of self with an extra atom.

    By default, all extra columns present in self must be specified as **kwargs.

    Try to avoid calling this in a loop (Use HasAtoms.concat instead).

    Source code in atomlib/atoms.py
    def add_atom(self, elem: t.Union[int, str], /,\n             x: t.Union[ArrayLike, float],\n             y: t.Optional[float] = None,\n             z: t.Optional[float] = None,\n             **kwargs: t.Any) -> Self:\n    \"\"\"\n    Return a copy of `self` with an extra atom.\n\n    By default, all extra columns present in `self` must be specified as `**kwargs`.\n\n    Try to avoid calling this in a loop (Use [`HasAtoms.concat`][atomlib.atoms.HasAtoms.concat] instead).\n    \"\"\"\n    if isinstance(elem, int):\n        kwargs.update(elem=elem)\n    else:\n        kwargs.update(symbol=elem)\n    if hasattr(x, '__len__') and len(x) > 1:  # type: ignore\n        (x, y, z) = to_vec3(x)\n    elif y is None or z is None:\n        raise ValueError(\"Must specify vector of positions or x, y, & z.\")\n\n    sym = get_sym(elem) if isinstance(elem, int) else elem\n    d: t.Dict[str, t.Any] = {'x': x, 'y': y, 'z': z, 'symbol': sym, **kwargs}\n    return self.concat(\n        (self, Atoms(d).select_schema(self.schema)),\n        how='vertical'\n    )\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.pos","title":"pos","text":"
    pos(\n    x: Sequence[Optional[float]],\n    /,\n    *,\n    y: None = None,\n    z: None = None,\n    tol: float = 1e-06,\n    **kwargs: Any,\n) -> Expr\n
    pos(\n    x: Optional[float] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n
    pos(\n    x: Union[Sequence[Optional[float]], float, None] = None,\n    y: Optional[float] = None,\n    z: Optional[float] = None,\n    *,\n    tol: float = 1e-06,\n    **kwargs: Any\n) -> Expr\n

    Select all atoms at a given position.

    Formally, returns all atoms within a cube of radius tol centered at (x,y,z), exclusive of the cube's surface.

    Additional parameters given as kwargs will be checked as additional parameters (with strict equality).

    Source code in atomlib/atoms.py
    def pos(self,\n        x: t.Union[t.Sequence[t.Optional[float]], float, None] = None,\n        y: t.Optional[float] = None, z: t.Optional[float] = None, *,\n        tol: float = 1e-6, **kwargs: t.Any) -> polars.Expr:\n    \"\"\"\n    Select all atoms at a given position.\n\n    Formally, returns all atoms within a cube of radius ``tol``\n    centered at ``(x,y,z)``, exclusive of the cube's surface.\n\n    Additional parameters given as ``kwargs`` will be checked\n    as additional parameters (with strict equality).\n    \"\"\"\n\n    if isinstance(x, t.Sequence):\n        (x, y, z) = x\n\n    tol = abs(float(tol))\n    selection = polars.lit(True)\n    if x is not None:\n        selection &= self.x().is_between(x - tol, x + tol, closed='none')\n    if y is not None:\n        selection &= self.y().is_between(y - tol, y + tol, closed='none')\n    if z is not None:\n        selection &= self.z().is_between(z - tol, z + tol, closed='none')\n    for (col, val) in kwargs.items():\n        selection &= (polars.col(col) == val)\n\n    return selection\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_index","title":"with_index","text":"
    with_index(index: Optional[AtomValues] = None) -> Self\n

    Returns self with a row index added in column 'i' (dtype polars.Int64). If index is not specified, defaults to an existing index or a new index.

    Source code in atomlib/atoms.py
    def with_index(self, index: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Returns `self` with a row index added in column 'i' (dtype [`polars.Int64`][polars.datatypes.Int64]).\n    If `index` is not specified, defaults to an existing index or a new index.\n    \"\"\"\n    if index is None and 'i' in self.columns:\n        return self\n    if index is None:\n        index = numpy.arange(len(self), dtype=numpy.int64)\n    return self.with_column(_values_to_expr(self, index, polars.Int64).alias('i'))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_wobble","title":"with_wobble","text":"
    with_wobble(wobble: Optional[AtomValues] = None) -> Self\n

    Return self with the given displacements in column 'wobble' (dtype polars.Float64). If wobble is not specified, defaults to the already-existing wobbles or 0.

    Source code in atomlib/atoms.py
    def with_wobble(self, wobble: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return `self` with the given displacements in column 'wobble' (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `wobble` is not specified, defaults to the already-existing wobbles or 0.\n    \"\"\"\n    if wobble is None and 'wobble' in self.columns:\n        return self\n    wobble = 0. if wobble is None else wobble\n    return self.with_column(_values_to_expr(self, wobble, polars.Float64).alias('wobble'))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_occupancy","title":"with_occupancy","text":"
    with_occupancy(\n    frac_occupancy: Optional[AtomValues] = None,\n) -> Self\n

    Return self with the given fractional occupancies (dtype polars.Float64). If frac_occupancy is not specified, defaults to the already-existing occupancies or 1.

    Source code in atomlib/atoms.py
    def with_occupancy(self, frac_occupancy: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return self with the given fractional occupancies (dtype [`polars.Float64`][polars.datatypes.Float64]).\n    If `frac_occupancy` is not specified, defaults to the already-existing occupancies or 1.\n    \"\"\"\n    if frac_occupancy is None and 'frac_occupancy' in self.columns:\n        return self\n    frac_occupancy = 1. if frac_occupancy is None else frac_occupancy\n    return self.with_column(_values_to_expr(self, frac_occupancy, polars.Float64).alias('frac_occupancy'))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.apply_wobble","title":"apply_wobble","text":"
    apply_wobble(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    Displace the atoms in self by the amount in the wobble column. wobble is interpretated as a mean-squared displacement, which is distributed equally over each axis.

    Source code in atomlib/atoms.py
    def apply_wobble(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    Displace the atoms in `self` by the amount in the `wobble` column.\n    `wobble` is interpretated as a mean-squared displacement, which is distributed\n    equally over each axis.\n    \"\"\"\n    if 'wobble' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    stddev = self.select((polars.col('wobble') / 3.).sqrt()).to_series().to_numpy()\n    coords = self.coords()\n    coords += stddev[:, None] * rng.standard_normal(coords.shape)\n    return self.with_coords(coords)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.apply_occupancy","title":"apply_occupancy","text":"
    apply_occupancy(\n    rng: Union[Generator, int, None] = None\n) -> Self\n

    For each atom in self, use its frac_occupancy to randomly decide whether to remove it.

    Source code in atomlib/atoms.py
    def apply_occupancy(self, rng: t.Union[numpy.random.Generator, int, None] = None) -> Self:\n    \"\"\"\n    For each atom in `self`, use its `frac_occupancy` to randomly decide whether to remove it.\n    \"\"\"\n    if 'frac_occupancy' not in self.columns:\n        return self\n    rng = numpy.random.default_rng(seed=rng)\n\n    frac = self.select('frac_occupancy').to_series().to_numpy()\n    choice = rng.binomial(1, frac).astype(numpy.bool_)\n    return self.filter(polars.lit(choice))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_type","title":"with_type","text":"
    with_type(types: Optional[AtomValues] = None) -> Self\n

    Return self with the given atom types in column 'type'. If types is not specified, use the already existing types or auto-assign them.

    When auto-assigning, each symbol is given a unique value, case-sensitive. Values are assigned from lowest atomic number to highest. For instance: [\"Ag+\", \"Na\", \"H\", \"Ag\"] => [3, 11, 1, 2]

    Source code in atomlib/atoms.py
    def with_type(self, types: t.Optional[AtomValues] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom types in column 'type'.\n    If `types` is not specified, use the already existing types or auto-assign them.\n\n    When auto-assigning, each symbol is given a unique value, case-sensitive.\n    Values are assigned from lowest atomic number to highest.\n    For instance: `[\"Ag+\", \"Na\", \"H\", \"Ag\"]` => `[3, 11, 1, 2]`\n    \"\"\"\n    if types is not None:\n        return self.with_columns(type=_values_to_expr(self, types, polars.Int32))\n    if 'type' in self.columns:\n        return self\n\n    unique = Atoms(self._get_frame().unique(maintain_order=False, subset=['elem', 'symbol']).sort(['elem', 'symbol']), _unchecked=True)\n    new = self.with_column(polars.Series('type', values=numpy.zeros(len(self)), dtype=polars.Int32))\n\n    logging.warning(\"Auto-assigning element types\")\n    for (i, (elem, sym)) in enumerate(unique.select(('elem', 'symbol')).rows()):\n        print(f\"Assigning type {i+1} to element '{sym}'\")\n        new = new.with_column(polars.when((polars.col('elem') == elem) & (polars.col('symbol') == sym))\n                                    .then(polars.lit(i+1))\n                                    .otherwise(polars.col('type'))\n                                    .alias('type'))\n\n    assert (new.get_column('type') == 0).sum() == 0\n    return new\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_mass","title":"with_mass","text":"
    with_mass(mass: Optional[ArrayLike] = None) -> Self\n

    Return self with the given atom masses in column 'mass'. If mass is not specified, use the already existing masses or auto-assign them.

    Source code in atomlib/atoms.py
    def with_mass(self, mass: t.Optional[ArrayLike] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atom masses in column `'mass'`.\n    If `mass` is not specified, use the already existing masses or auto-assign them.\n    \"\"\"\n    if mass is not None:\n        return self.with_column(_values_to_expr(self, mass, polars.Float32).alias('mass'))\n    if 'mass' in self.columns:\n        return self\n\n    unique_elems = self.get_column('elem').unique()\n    new = self.with_column(polars.Series('mass', values=numpy.zeros(len(self)), dtype=polars.Float32))\n\n    logging.warning(\"Auto-assigning element masses\")\n    for elem in unique_elems:\n        new = new.with_column(polars.when(polars.col('elem') == elem)\n                                    .then(polars.lit(get_mass(elem)))\n                                    .otherwise(polars.col('mass'))\n                                    .alias('mass'))\n\n    assert (new.get_column('mass').abs() < 1e-10).sum() == 0\n    return new\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_symbol","title":"with_symbol","text":"
    with_symbol(\n    symbols: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n) -> Self\n

    Return self with the given atomic symbols.

    Source code in atomlib/atoms.py
    def with_symbol(self, symbols: ArrayLike, selection: t.Optional[AtomSelection] = None) -> Self:\n    \"\"\"\n    Return `self` with the given atomic symbols.\n    \"\"\"\n    if selection is not None:\n        selection = _selection_to_numpy(self, selection)\n        new_symbols = self.get_column('symbol')\n        new_symbols[selection] = polars.Series(list(numpy.broadcast_to(symbols, len(selection))), dtype=polars.Utf8)\n        symbols = new_symbols\n\n    # TODO better cast here\n    symbols = polars.Series('symbol', list(numpy.broadcast_to(symbols, len(self))), dtype=polars.Utf8)\n    return self.with_columns((symbols, get_elem(symbols)))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_coords","title":"with_coords","text":"
    with_coords(\n    pts: ArrayLike,\n    selection: Optional[AtomSelection] = None,\n    *,\n    frame: Literal[\"local\"] = \"local\"\n) -> Self\n

    Return self replaced with the given atomic positions.

    Source code in atomlib/atoms.py
    def with_coords(self, pts: ArrayLike, selection: t.Optional[AtomSelection] = None, *, frame: t.Literal['local'] = 'local') -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic positions.\n    \"\"\"\n    if selection is not None:\n        selection = _selection_to_numpy(self, selection)\n        new_pts = self.coords()\n        pts = numpy.atleast_2d(pts)\n        assert pts.shape[-1] == 3\n        new_pts[selection] = pts\n        pts = new_pts\n\n    # https://github.com/pola-rs/polars/issues/18369\n    pts = numpy.broadcast_to(pts, (len(self), 3)) if len(self) else []\n    return self.with_columns(polars.Series('coords', pts, polars.Array(polars.Float64, 3)))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_velocity","title":"with_velocity","text":"
    with_velocity(\n    pts: Optional[ArrayLike] = None,\n    selection: Optional[AtomSelection] = None,\n) -> Self\n

    Return self replaced with the given atomic velocities. If pts is not specified, use the already existing velocities or zero.

    Source code in atomlib/atoms.py
    def with_velocity(self, pts: t.Optional[ArrayLike] = None,\n                  selection: t.Optional[AtomSelection] = None) -> Self:\n    \"\"\"\n    Return `self` replaced with the given atomic velocities.\n    If `pts` is not specified, use the already existing velocities or zero.\n    \"\"\"\n    if pts is None:\n        if 'velocity' in self:\n            return self\n        all_pts = numpy.zeros((len(self), 3))\n    else:\n        all_pts = self['velocity'].to_numpy()\n\n    if selection is None:\n        all_pts = pts or all_pts\n    elif pts is not None:\n        selection = _selection_to_numpy(self, selection)\n        all_pts = numpy.require(all_pts, requirements=['WRITEABLE'])\n        pts = numpy.atleast_2d(pts)\n        assert pts.shape[-1] == 3\n        all_pts[selection] = pts\n\n    all_pts = numpy.broadcast_to(all_pts, (len(self), 3))\n    return self.with_columns(polars.Series('velocity', all_pts, polars.Array(polars.Float64, 3)))\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.read","title":"read classmethod","text":"
    read(path: FileOrPath, ty: FileType) -> HasAtomsT\n
    read(\n    path: Union[str, Path, TextIO], ty: Literal[None] = None\n) -> HasAtomsT\n
    read(\n    path: FileOrPath, ty: Optional[FileType] = None\n) -> HasAtomsT\n

    Read a structure from a file.

    Supported types can be found in the io module. If no ty is specified, it is inferred from the file's extension.

    Source code in atomlib/mixins.py
    @classmethod\ndef read(cls: t.Type[HasAtomsT], path: FileOrPath, ty: t.Optional[FileType] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a file.\n\n    Supported types can be found in the [io][atomlib.io] module.\n    If no `ty` is specified, it is inferred from the file's extension.\n    \"\"\"\n    from .io import read\n    return _cast_atoms(read(path, ty), cls)  # type: ignore\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.read_cif","title":"read_cif classmethod","text":"
    read_cif(\n    f: Union[FileOrPath, CIF, CIFDataBlock],\n    block: Union[int, str, None] = None,\n) -> HasAtomsT\n

    Read a structure from a CIF file.

    If block is specified, read data from the given block of the CIF file (index or name).

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cif(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CIF, CIFDataBlock], block: t.Union[int, str, None] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a CIF file.\n\n    If `block` is specified, read data from the given block of the CIF file (index or name).\n    \"\"\"\n    from .io import read_cif\n    return _cast_atoms(read_cif(f, block), cls)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.read_xyz","title":"read_xyz classmethod","text":"
    read_xyz(f: Union[FileOrPath, XYZ]) -> HasAtomsT\n

    Read a structure from an XYZ file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xyz(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XYZ]) -> HasAtomsT:\n    \"\"\"Read a structure from an XYZ file.\"\"\"\n    from .io import read_xyz\n    return _cast_atoms(read_xyz(f), cls)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.read_xsf","title":"read_xsf classmethod","text":"
    read_xsf(f: Union[FileOrPath, XSF]) -> HasAtomsT\n

    Read a structure from an XSF file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xsf(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XSF]) -> HasAtomsT:\n    \"\"\"Read a structure from an XSF file.\"\"\"\n    from .io import read_xsf\n    return _cast_atoms(read_xsf(f), cls)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.read_cfg","title":"read_cfg classmethod","text":"
    read_cfg(f: Union[FileOrPath, CFG]) -> HasAtomsT\n

    Read a structure from a CFG file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cfg(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CFG]) -> HasAtomsT:\n    \"\"\"Read a structure from a CFG file.\"\"\"\n    from .io import read_cfg\n    return _cast_atoms(read_cfg(f), cls)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.read_lmp","title":"read_lmp classmethod","text":"
    read_lmp(\n    f: Union[FileOrPath, LMP],\n    type_map: Optional[Dict[int, Union[str, int]]] = None,\n) -> HasAtomsT\n

    Read a structure from a LAAMPS data file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_lmp(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, LMP], type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> HasAtomsT:\n    \"\"\"Read a structure from a LAAMPS data file.\"\"\"\n    from .io import read_lmp\n    return _cast_atoms(read_lmp(f, type_map=type_map), cls)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.write_cif","title":"write_cif","text":"
    write_cif(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cif(self, f: FileOrPath):\n    from .io import write_cif\n    write_cif(self, f)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.write_xyz","title":"write_xyz","text":"
    write_xyz(f: FileOrPath, fmt: XYZFormat = 'exyz')\n
    Source code in atomlib/mixins.py
    def write_xyz(self, f: FileOrPath, fmt: XYZFormat = 'exyz'):\n    from .io import write_xyz\n    write_xyz(self, f, fmt)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.write_xsf","title":"write_xsf","text":"
    write_xsf(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_xsf(self, f: FileOrPath):\n    from .io import write_xsf\n    write_xsf(self, f)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.write_cfg","title":"write_cfg","text":"
    write_cfg(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cfg(self, f: FileOrPath):\n    from .io import write_cfg\n    write_cfg(self, f)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.write_lmp","title":"write_lmp","text":"
    write_lmp(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_lmp(self, f: FileOrPath):\n    from .io import write_lmp\n    write_lmp(self, f)\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.write","title":"write","text":"
    write(path: FileOrPath, ty: FileType)\n
    write(\n    path: Union[str, Path, TextIO], ty: Literal[None] = None\n)\n
    write(path: FileOrPath, ty: Optional[FileType] = None)\n

    Write this structure to a file.

    A file type may be specified using ty. If no ty is specified, it is inferred from the path's extension.

    Source code in atomlib/mixins.py
    def write(self, path: FileOrPath, ty: t.Optional[FileType] = None):\n    \"\"\"\n    Write this structure to a file.\n\n    A file type may be specified using `ty`.\n    If no `ty` is specified, it is inferred from the path's extension.\n    \"\"\"\n    from .io import write\n    write(self, path, ty)  # type: ignore\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.empty","title":"empty staticmethod","text":"
    empty() -> Atoms\n

    Return an empty Atoms with only the mandatory columns.

    Source code in atomlib/atoms.py
    @staticmethod\ndef empty() -> Atoms:\n    \"\"\"\n    Return an empty Atoms with only the mandatory columns.\n    \"\"\"\n    return Atoms()\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.get_atoms","title":"get_atoms","text":"
    get_atoms(frame: Literal['local'] = 'local') -> Atoms\n
    Source code in atomlib/atoms.py
    def get_atoms(self, frame: t.Literal['local'] = 'local') -> Atoms:\n    if frame != 'local':\n        raise ValueError(f\"Atoms without a cell only support the 'local' coordinate frame, not '{frame}'.\")\n    return self\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.with_atoms","title":"with_atoms","text":"
    with_atoms(\n    atoms: HasAtoms, frame: Literal[\"local\"] = \"local\"\n) -> Atoms\n
    Source code in atomlib/atoms.py
    def with_atoms(self, atoms: HasAtoms, frame: t.Literal['local'] = 'local') -> Atoms:\n    if frame != 'local':\n        raise ValueError(f\"Atoms without a cell only support the 'local' coordinate frame, not '{frame}'.\")\n    return atoms.get_atoms()\n
    "},{"location":"api/atoms/#atomlib.atoms.Atoms.bbox","title":"bbox","text":"
    bbox() -> BBox3D\n

    Return the bounding box of all the points in self.

    Source code in atomlib/atoms.py
    def bbox(self) -> BBox3D:\n    \"\"\"Return the bounding box of all the points in `self`.\"\"\"\n    if self._bbox is None:\n        self._bbox = BBox3D.from_pts(self.coords())\n\n    return self._bbox\n
    "},{"location":"api/bbox/","title":"atomlib.bbox","text":"

    Bounding boxes

    "},{"location":"api/bbox/#atomlib.bbox.BBox3D","title":"BBox3D","text":"

    3D axis-aligned bounding box, with corners min and max.

    Source code in atomlib/bbox.py
    class BBox3D:\n    \"\"\"\n    3D axis-aligned bounding box, with corners `min` and `max`.\n    \"\"\"\n\n    def __init__(self, min: VecLike, max: VecLike):\n        # shape: (3, 2)\n        # self._inner[1, 0]: min of y\n        min, max = to_vec3(min), to_vec3(max)\n        self.inner: numpy.ndarray = numpy.stack((min, max), axis=-1)\n\n    @classmethod\n    def from_pts(cls, pts: t.Union[numpy.ndarray, t.Sequence[Vec3]]) -> BBox3D:\n        \"\"\"Construct the minimum bounding box containing the points `pts`.\"\"\"\n        pts = numpy.atleast_2d(pts).reshape(-1, 3)\n        return cls(numpy.nanmin(pts, axis=0), numpy.nanmax(pts, axis=0))\n\n    @classmethod\n    def unit(cls) -> BBox3D:\n        \"\"\"Return a unit bbox (cube from [0,0,0] to [1,1,1]).\"\"\"\n        return cls([0., 0., 0.], [1., 1., 1.])\n\n    def transform_from_unit(self) -> AffineTransform3D:\n        \"\"\"Return the transform which transforms a unit bbox to `self`.\"\"\"\n        from .transform import AffineTransform3D\n        return AffineTransform3D.translate(self.min).scale(self.max - self.min)\n\n    def transform_to_unit(self) -> AffineTransform3D:\n        \"\"\"Return the transform which transforms `self` to a unit bbox.\"\"\"\n        return self.transform_from_unit().inverse()\n\n    @property\n    def min(self) -> Vec3:\n        \"\"\"Return the minimum corner `[xmin, ymin, zmin]`.\"\"\"\n        return self.inner[:, 0]\n\n    @property\n    def max(self) -> Vec3:\n        \"\"\"Return the minimum corner `[xmax, ymax, zmax]`.\"\"\"\n        return self.inner[:, 1]\n\n    @property\n    def x(self) -> numpy.ndarray:\n        \"\"\"Return the interval `[xmin, xmax]`.\"\"\"\n        return self.inner[0]\n\n    @property\n    def y(self) -> numpy.ndarray:\n        \"\"\"Return the interval `[ymin, ymax]`.\"\"\"\n        return self.inner[1]\n\n    @property\n    def z(self) -> numpy.ndarray:\n        \"\"\"Return the interval `[zmin, zmax]`.\"\"\"\n        return self.inner[2]\n\n    @property\n    def size(self) -> Vec3:\n        \"\"\"Return the size `[xsize, ysize, zsize]`.\"\"\"\n        return self.max - self.min\n\n    def volume(self) -> float:\n        \"\"\"Return the volume of the bbox.\"\"\"\n        return float(numpy.prod(self.size))\n\n    def corners(self) -> numpy.ndarray:\n        \"\"\"Return a (8, 3) ndarray containing the corners of the bbox.\"\"\"\n        return numpy.stack(list(map(numpy.ravel, numpy.meshgrid(*self.inner))), axis=-1)\n\n    def pad(self, amount: t.Union[float, VecLike]) -> BBox3D:\n        \"\"\"\n        Pad the given bbox by `amount`. If a vector `[x, y, z]` is given, pad each axis by the given amount.\n        \"\"\"\n        amount_v = numpy.broadcast_to(amount, 3)\n\n        return type(self)(\n            self.min - amount_v,\n            self.max + amount_v\n        )\n\n    def __or__(self, other: t.Union[Vec3, BBox3D]) -> BBox3D:\n        \"\"\"\n        Union this bbox with another point or bbox.\n        \"\"\"\n        if isinstance(other, numpy.ndarray):\n            return self.from_pts((self.min, self.max, other))\n\n        return type(self)(\n            numpy.nanmin(((self.min, other.min)), axis=0),\n            numpy.nanmax(((self.max, other.max)), axis=0),\n        )\n\n    __ror__ = __or__\n\n    def __and__(self, other: BBox3D) -> BBox3D:\n        \"\"\"\n        Intersect this bbox with another point or bbox.\n\n        Undefined if there is no overlap between the two.\n        \"\"\"\n        return type(self)(\n            numpy.nanmax(((self.min, other.min)), axis=0),\n            numpy.nanmin(((self.max, other.max)), axis=0),\n        )\n\n    def __repr__(self) -> str:\n        return f\"BBox({self.min}, {self.max})\"\n
    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.inner","title":"inner instance-attribute","text":"
    inner: ndarray = stack((min, max), axis=-1)\n
    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.min","title":"min property","text":"
    min: Vec3\n

    Return the minimum corner [xmin, ymin, zmin].

    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.max","title":"max property","text":"
    max: Vec3\n

    Return the minimum corner [xmax, ymax, zmax].

    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.x","title":"x property","text":"
    x: ndarray\n

    Return the interval [xmin, xmax].

    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.y","title":"y property","text":"
    y: ndarray\n

    Return the interval [ymin, ymax].

    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.z","title":"z property","text":"
    z: ndarray\n

    Return the interval [zmin, zmax].

    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.size","title":"size property","text":"
    size: Vec3\n

    Return the size [xsize, ysize, zsize].

    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.from_pts","title":"from_pts classmethod","text":"
    from_pts(pts: Union[ndarray, Sequence[Vec3]]) -> BBox3D\n

    Construct the minimum bounding box containing the points pts.

    Source code in atomlib/bbox.py
    @classmethod\ndef from_pts(cls, pts: t.Union[numpy.ndarray, t.Sequence[Vec3]]) -> BBox3D:\n    \"\"\"Construct the minimum bounding box containing the points `pts`.\"\"\"\n    pts = numpy.atleast_2d(pts).reshape(-1, 3)\n    return cls(numpy.nanmin(pts, axis=0), numpy.nanmax(pts, axis=0))\n
    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.unit","title":"unit classmethod","text":"
    unit() -> BBox3D\n

    Return a unit bbox (cube from [0,0,0] to [1,1,1]).

    Source code in atomlib/bbox.py
    @classmethod\ndef unit(cls) -> BBox3D:\n    \"\"\"Return a unit bbox (cube from [0,0,0] to [1,1,1]).\"\"\"\n    return cls([0., 0., 0.], [1., 1., 1.])\n
    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.transform_from_unit","title":"transform_from_unit","text":"
    transform_from_unit() -> AffineTransform3D\n

    Return the transform which transforms a unit bbox to self.

    Source code in atomlib/bbox.py
    def transform_from_unit(self) -> AffineTransform3D:\n    \"\"\"Return the transform which transforms a unit bbox to `self`.\"\"\"\n    from .transform import AffineTransform3D\n    return AffineTransform3D.translate(self.min).scale(self.max - self.min)\n
    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.transform_to_unit","title":"transform_to_unit","text":"
    transform_to_unit() -> AffineTransform3D\n

    Return the transform which transforms self to a unit bbox.

    Source code in atomlib/bbox.py
    def transform_to_unit(self) -> AffineTransform3D:\n    \"\"\"Return the transform which transforms `self` to a unit bbox.\"\"\"\n    return self.transform_from_unit().inverse()\n
    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.volume","title":"volume","text":"
    volume() -> float\n

    Return the volume of the bbox.

    Source code in atomlib/bbox.py
    def volume(self) -> float:\n    \"\"\"Return the volume of the bbox.\"\"\"\n    return float(numpy.prod(self.size))\n
    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.corners","title":"corners","text":"
    corners() -> ndarray\n

    Return a (8, 3) ndarray containing the corners of the bbox.

    Source code in atomlib/bbox.py
    def corners(self) -> numpy.ndarray:\n    \"\"\"Return a (8, 3) ndarray containing the corners of the bbox.\"\"\"\n    return numpy.stack(list(map(numpy.ravel, numpy.meshgrid(*self.inner))), axis=-1)\n
    "},{"location":"api/bbox/#atomlib.bbox.BBox3D.pad","title":"pad","text":"
    pad(amount: Union[float, VecLike]) -> BBox3D\n

    Pad the given bbox by amount. If a vector [x, y, z] is given, pad each axis by the given amount.

    Source code in atomlib/bbox.py
    def pad(self, amount: t.Union[float, VecLike]) -> BBox3D:\n    \"\"\"\n    Pad the given bbox by `amount`. If a vector `[x, y, z]` is given, pad each axis by the given amount.\n    \"\"\"\n    amount_v = numpy.broadcast_to(amount, 3)\n\n    return type(self)(\n        self.min - amount_v,\n        self.max + amount_v\n    )\n
    "},{"location":"api/cell/","title":"atomlib.cell","text":"

    Crystallographic unit cell.

    This module defines HasCell and the concrete Cell, the core types for dealing with crystallographic unit cells and their associated coordinate frames.

    "},{"location":"api/cell/#atomlib.cell.CoordinateFrame","title":"CoordinateFrame module-attribute","text":"
    CoordinateFrame: TypeAlias = Literal[\n    \"cell\",\n    \"cell_frac\",\n    \"cell_box\",\n    \"ortho\",\n    \"ortho_frac\",\n    \"ortho_box\",\n    \"linear\",\n    \"local\",\n    \"global\",\n]\n

    A coordinate frame to use.

    • cell: Real-space units along crystal axes
    • cell_frac: Fraction of unit cells
    • cell_box: Fraction of cell box
    • ortho: Real-space units along orthogonal cell
    • ortho_frac: Fraction of orthogonal cell
    • ortho_box: Fraction of orthogonal box
    • linear: Angstroms in local coordinate system (without affine transformation)
    • local: Angstroms in local coordinate system (with affine transformation)
    • global: Angstroms in global coordinate system (with all transformations)

    For more information, see the documentation at Coordinate systems, or the example notebook at examples/coords.ipynb.

    "},{"location":"api/cell/#atomlib.cell.HasCellT","title":"HasCellT module-attribute","text":"
    HasCellT = TypeVar('HasCellT', bound='HasCell')\n
    "},{"location":"api/cell/#atomlib.cell.HasCell","title":"HasCell","text":"Source code in atomlib/cell.py
    class HasCell:\n    # abstract methods\n\n    @abc.abstractmethod\n    def get_cell(self) -> Cell:\n        \"\"\"Get the cell contained in ``self``. This should be a low cost method.\"\"\"\n        ...\n\n    @abc.abstractmethod\n    def with_cell(self: HasCellT, cell: Cell) -> HasCellT:\n        \"\"\"Replace the cell in ``self`` with ``cell``.\"\"\"\n        ...\n\n    # getters\n\n    @property\n    def affine(self) -> AffineTransform3D:\n        \"\"\"\n        Affine transformation. Holds transformation from 'ortho' to 'local' coordinates,\n        including rotation away from the standard crystal orientation.\n        \"\"\"\n        return self.get_cell()._affine\n\n    @property\n    def ortho(self) -> LinearTransform3D:\n        \"\"\"\n        Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.\n        \"\"\"\n        return self.get_cell()._ortho\n\n    @property\n    def metric(self) -> LinearTransform3D:\n        r\"\"\"\n        Cell metric tensor\n\n        Returns the dot product between every combination of basis vectors.\n        :math:`\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j`\n        \"\"\"\n        ortho = self.get_cell()._ortho.scale(self.cell_size)\n        return ortho.T @ ortho\n\n    @property\n    def cell_size(self) -> NDArray[numpy.float64]:\n        \"\"\"Unit cell size.\"\"\"\n        return self.get_cell()._cell_size\n\n    @property\n    def cell_angle(self) -> NDArray[numpy.float64]:\n        \"\"\"Unit cell angles, in radians.\"\"\"\n        return self.get_cell()._cell_angle\n\n    @property\n    def n_cells(self) -> NDArray[numpy.int_]:\n        \"\"\"Number of unit cells.\"\"\"\n        return self.get_cell()._n_cells\n\n    @property\n    def pbc(self) -> NDArray[numpy.bool_]:\n        \"\"\"Flags indicating the presence of periodic boundary conditions along each axis.\"\"\"\n        return self.get_cell()._pbc\n\n    @property\n    def ortho_size(self) -> NDArray[numpy.float64]:\n        \"\"\"\n        Return size of orthogonal unit cell.\n\n        Equivalent to the diagonal of the orthogonalization matrix.\n        \"\"\"\n        return self.cell_size * numpy.diag(self.ortho.inner)\n\n    @property\n    def box_size(self) -> NDArray[numpy.float64]:\n        \"\"\"\n        Return size of the cell box.\n\n        Equivalent to ``self.n_cells * self.cell_size``.\n        \"\"\"\n        return self.n_cells * self.cell_size\n\n    # get transforms\n\n    def _get_transform_to_local(self, frame: CoordinateFrame) -> AffineTransform3D:\n        \"\"\"Get the transform from `frame` to local coordinates.\"\"\"\n        frame = t.cast(CoordinateFrame, frame.lower())\n\n        if frame == 'local' or frame == 'global':\n            return LinearTransform3D()\n\n        if frame == 'linear':\n            return self.affine.to_translation()\n\n        if frame.startswith('cell'):\n            transform = self.affine @ self.ortho\n            cell_size = self.cell_size\n        elif frame.startswith('ortho'):\n            transform = self.affine\n            cell_size = self.ortho_size\n        else:\n            raise ValueError(f\"Unknown coordinate frame '{frame}'\")\n\n        if '_' not in frame:\n            return transform\n        end = frame.split('_', 2)[1]\n        if end == 'frac':\n            return transform @ LinearTransform3D.scale(cell_size)\n        if end == 'box':\n            return transform @ LinearTransform3D.scale(cell_size * self.n_cells)\n        raise ValueError(f\"Unknown coordinate frame '{frame}'\")\n\n    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n        \"\"\"\n        In the two-argument form, get the transform to `frame_to` from `frame_from`.\n        In the one-argument form, get the transform from local coordinates to 'frame'.\n        \"\"\"\n        transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n        transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n        if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n            return AffineTransform3D()\n        return transform_to.inverse() @ transform_from\n\n    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n        corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n        return self.get_transform(frame, 'cell_box') @ corners\n\n    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n        \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n        return BBox3D.from_pts(self.corners(frame))\n\n    bbox = bbox_cell\n\n    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n        \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n        return self.ortho.is_diagonal(tol=tol)\n\n    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n        \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n        transform = (self.affine @ self.ortho).to_linear()\n        if not transform.is_scaled_orthogonal(tol):\n            return False\n        normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n        # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n        return all(\n            any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n            for row in normed\n        )\n\n    def _cell_size_in_local(self) -> Vec3:\n        \"\"\"Calculate cell_size in the local coordinate system. Assumes `self.is_orthogonal_in_local()`.\"\"\"\n        return numpy.abs(self.get_transform('local', 'ortho').transform_vec(self.cell_size))\n\n    def _box_size_in_local(self) -> Vec3:\n        \"\"\"Calculate box_size in the local coordinate system. Assumes `self.is_orthogonal_in_local()`.\"\"\"\n        return numpy.abs(self.get_transform('local', 'ortho').transform_vec(self.box_size))\n\n    def _n_cells_in_local(self) -> NDArray[numpy.int_]:\n        \"\"\"Calculate n_cells after any local rotation. Assumes `self.is_orthogonal_in_local()`.\"\"\"\n        return numpy.abs(numpy.round(self.get_transform('local', 'ortho').transform_vec(self.n_cells)).astype(int))\n\n    def to_ortho(self) -> AffineTransform3D:\n        return self.get_transform('local', 'cell_box')\n\n    def transform_cell(self: HasCellT, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> HasCellT:\n        \"\"\"\n        Apply the given transform to the unit cell, and return a new `Cell`.\n        The transform is applied in coordinate frame 'frame'.\n        Orthogonal and affine transformations are applied to the affine matrix component,\n        while skew and scaling is applied to the orthogonalization matrix/cell_size.\n        \"\"\"\n        transform = t.cast(AffineTransform3D, self.change_transform(transform, 'local', frame))\n        if not transform.to_linear().is_orthogonal():\n            raise NotImplementedError()\n        return self.with_cell(Cell(\n            affine=transform @ self.affine,\n            ortho=self.ortho,\n            cell_size=self.cell_size,\n            cell_angle=self.cell_angle,\n            n_cells=self.n_cells,\n            pbc=self.pbc,\n        ))\n\n    def strain_orthogonal(self: HasCellT) -> HasCellT:\n        \"\"\"\n        Orthogonalize using strain.\n\n        Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n        For small displacements, no hydrostatic strain is applied (volume is conserved).\n        \"\"\"\n        return self.with_cell(Cell(\n            affine=self.affine,\n            ortho=LinearTransform3D(),\n            cell_size=self.cell_size,\n            n_cells=self.n_cells,\n            pbc=self.pbc,\n        ))\n\n    def repeat(self: HasCellT, n: t.Union[int, VecLike]) -> HasCellT:\n        \"\"\"Tile the cell by `n` in each dimension.\"\"\"\n        ns = numpy.broadcast_to(n, 3)\n        if not numpy.issubdtype(ns.dtype, numpy.integer):\n            raise ValueError(\"repeat() argument must be an integer or integer array.\")\n        return self.with_cell(Cell(\n            affine=self.affine,\n            ortho=self.ortho,\n            cell_size=self.cell_size,\n            cell_angle=self.cell_angle,\n            n_cells=self.n_cells * numpy.broadcast_to(n, 3),\n            pbc = self.pbc | (ns > 1)  # assume periodic along repeated directions\n        ))\n\n    def explode(self: HasCellT) -> HasCellT:\n        \"\"\"Materialize repeated cells as one supercell.\"\"\"\n        return self.with_cell(Cell(\n            affine=self.affine,\n            ortho=self.ortho,\n            cell_size=self.cell_size*self.n_cells,\n            cell_angle=self.cell_angle,\n            pbc=self.pbc,\n        ))\n\n    def explode_z(self: HasCellT) -> HasCellT:\n        \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n        return self.with_cell(Cell(\n            affine=self.affine,\n            ortho=self.ortho,\n            cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n            n_cells=[*self.n_cells[:2], 1],\n            cell_angle=self.cell_angle,\n            pbc=self.pbc,\n        ))\n\n    def crop(self: HasCellT, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n             y_min: float = -numpy.inf, y_max: float = numpy.inf,\n             z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n             frame: CoordinateFrame = 'local') -> HasCellT:\n        \"\"\"\n        Crop `self` to the given extents. For a non-orthogonal\n        cell, this must be specified in cell coordinates. This\n        function implicity `explode`s the cell as well.\n        \"\"\"\n\n        if not frame.lower().startswith('cell'):\n            if not self.is_orthogonal():\n                raise ValueError(\"Cannot crop a non-orthogonal cell in orthogonal coordinates. Use crop_atoms instead.\")\n\n        min = to_vec3([x_min, y_min, z_min])\n        max = to_vec3([x_max, y_max, z_max])\n        (min, max) = self.get_transform('cell_box', frame).transform([min, max])\n        new_box = BBox3D(min, max) & BBox3D.unit()\n        cropped = (new_box.min > 0.) | (new_box.max < 1.)\n\n        return self.with_cell(Cell(\n            affine=self.affine @ AffineTransform3D.translate(-new_box.min),\n            ortho=self.ortho,\n            cell_size=new_box.size * self.cell_size * numpy.where(cropped, self.n_cells, 1),\n            n_cells=numpy.where(cropped, 1, self.n_cells),\n            cell_angle=self.cell_angle,\n            pbc=self.pbc & ~cropped  # remove periodicity along cropped directions\n        ))\n\n    @t.overload\n    def change_transform(self, transform: AffineTransform3D,\n                         frame_to: t.Optional[CoordinateFrame] = None,\n                         frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n        ...\n\n    @t.overload\n    def change_transform(self, transform: Transform3D,\n                         frame_to: t.Optional[CoordinateFrame] = None,\n                         frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n        ...\n\n    def change_transform(self, transform: Transform3D,\n                         frame_to: t.Optional[CoordinateFrame] = None,\n                         frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n        \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n        if frame_to == frame_from and frame_to is not None:\n            return transform\n        coord_change = self.get_transform(frame_to, frame_from)\n        return coord_change @ transform @ coord_change.inverse()\n\n    def assert_equal(self, other: t.Any):\n        assert isinstance(other, HasCell) and type(self) is type(other)\n        numpy.testing.assert_array_almost_equal(self.affine.inner, other.affine.inner, 6)\n        numpy.testing.assert_array_almost_equal(self.ortho.inner, other.ortho.inner, 6)\n        numpy.testing.assert_array_almost_equal(self.cell_size, other.cell_size, 6)\n        numpy.testing.assert_array_equal(self.n_cells, other.n_cells)\n        numpy.testing.assert_array_equal(self.pbc, other.pbc)\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.affine","title":"affine property","text":"
    affine: AffineTransform3D\n

    Affine transformation. Holds transformation from 'ortho' to 'local' coordinates, including rotation away from the standard crystal orientation.

    "},{"location":"api/cell/#atomlib.cell.HasCell.ortho","title":"ortho property","text":"
    ortho: LinearTransform3D\n

    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.

    "},{"location":"api/cell/#atomlib.cell.HasCell.metric","title":"metric property","text":"
    metric: LinearTransform3D\n

    Cell metric tensor

    Returns the dot product between every combination of basis vectors. :math:\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j

    "},{"location":"api/cell/#atomlib.cell.HasCell.cell_size","title":"cell_size property","text":"
    cell_size: NDArray[float64]\n

    Unit cell size.

    "},{"location":"api/cell/#atomlib.cell.HasCell.cell_angle","title":"cell_angle property","text":"
    cell_angle: NDArray[float64]\n

    Unit cell angles, in radians.

    "},{"location":"api/cell/#atomlib.cell.HasCell.n_cells","title":"n_cells property","text":"
    n_cells: NDArray[int_]\n

    Number of unit cells.

    "},{"location":"api/cell/#atomlib.cell.HasCell.pbc","title":"pbc property","text":"
    pbc: NDArray[bool_]\n

    Flags indicating the presence of periodic boundary conditions along each axis.

    "},{"location":"api/cell/#atomlib.cell.HasCell.ortho_size","title":"ortho_size property","text":"
    ortho_size: NDArray[float64]\n

    Return size of orthogonal unit cell.

    Equivalent to the diagonal of the orthogonalization matrix.

    "},{"location":"api/cell/#atomlib.cell.HasCell.box_size","title":"box_size property","text":"
    box_size: NDArray[float64]\n

    Return size of the cell box.

    Equivalent to self.n_cells * self.cell_size.

    "},{"location":"api/cell/#atomlib.cell.HasCell.bbox","title":"bbox class-attribute instance-attribute","text":"
    bbox = bbox_cell\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.get_cell","title":"get_cell abstractmethod","text":"
    get_cell() -> Cell\n

    Get the cell contained in self. This should be a low cost method.

    Source code in atomlib/cell.py
    @abc.abstractmethod\ndef get_cell(self) -> Cell:\n    \"\"\"Get the cell contained in ``self``. This should be a low cost method.\"\"\"\n    ...\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.with_cell","title":"with_cell abstractmethod","text":"
    with_cell(cell: Cell) -> HasCellT\n

    Replace the cell in self with cell.

    Source code in atomlib/cell.py
    @abc.abstractmethod\ndef with_cell(self: HasCellT, cell: Cell) -> HasCellT:\n    \"\"\"Replace the cell in ``self`` with ``cell``.\"\"\"\n    ...\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.get_transform","title":"get_transform","text":"
    get_transform(\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n

    In the two-argument form, get the transform to frame_to from frame_from. In the one-argument form, get the transform from local coordinates to 'frame'.

    Source code in atomlib/cell.py
    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n    \"\"\"\n    In the two-argument form, get the transform to `frame_to` from `frame_from`.\n    In the one-argument form, get the transform from local coordinates to 'frame'.\n    \"\"\"\n    transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n    transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n    if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n        return AffineTransform3D()\n    return transform_to.inverse() @ transform_from\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.corners","title":"corners","text":"
    corners(frame: CoordinateFrame = 'local') -> ndarray\n
    Source code in atomlib/cell.py
    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n    corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n    return self.get_transform(frame, 'cell_box') @ corners\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.bbox_cell","title":"bbox_cell","text":"
    bbox_cell(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the bounding box of the cell box in the given coordinate system.

    Source code in atomlib/cell.py
    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n    return BBox3D.from_pts(self.corners(frame))\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.is_orthogonal","title":"is_orthogonal","text":"
    is_orthogonal(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal (axes are at right angles.)

    Source code in atomlib/cell.py
    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n    return self.ortho.is_diagonal(tol=tol)\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.is_orthogonal_in_local","title":"is_orthogonal_in_local","text":"
    is_orthogonal_in_local(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal and aligned with the local coordinate system.

    Source code in atomlib/cell.py
    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n    transform = (self.affine @ self.ortho).to_linear()\n    if not transform.is_scaled_orthogonal(tol):\n        return False\n    normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n    # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n    return all(\n        any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n        for row in normed\n    )\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.to_ortho","title":"to_ortho","text":"
    to_ortho() -> AffineTransform3D\n
    Source code in atomlib/cell.py
    def to_ortho(self) -> AffineTransform3D:\n    return self.get_transform('local', 'cell_box')\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.transform_cell","title":"transform_cell","text":"
    transform_cell(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> HasCellT\n

    Apply the given transform to the unit cell, and return a new Cell. The transform is applied in coordinate frame 'frame'. Orthogonal and affine transformations are applied to the affine matrix component, while skew and scaling is applied to the orthogonalization matrix/cell_size.

    Source code in atomlib/cell.py
    def transform_cell(self: HasCellT, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> HasCellT:\n    \"\"\"\n    Apply the given transform to the unit cell, and return a new `Cell`.\n    The transform is applied in coordinate frame 'frame'.\n    Orthogonal and affine transformations are applied to the affine matrix component,\n    while skew and scaling is applied to the orthogonalization matrix/cell_size.\n    \"\"\"\n    transform = t.cast(AffineTransform3D, self.change_transform(transform, 'local', frame))\n    if not transform.to_linear().is_orthogonal():\n        raise NotImplementedError()\n    return self.with_cell(Cell(\n        affine=transform @ self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size,\n        cell_angle=self.cell_angle,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.strain_orthogonal","title":"strain_orthogonal","text":"
    strain_orthogonal() -> HasCellT\n

    Orthogonalize using strain.

    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane. For small displacements, no hydrostatic strain is applied (volume is conserved).

    Source code in atomlib/cell.py
    def strain_orthogonal(self: HasCellT) -> HasCellT:\n    \"\"\"\n    Orthogonalize using strain.\n\n    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n    For small displacements, no hydrostatic strain is applied (volume is conserved).\n    \"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=LinearTransform3D(),\n        cell_size=self.cell_size,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.repeat","title":"repeat","text":"
    repeat(n: Union[int, VecLike]) -> HasCellT\n

    Tile the cell by n in each dimension.

    Source code in atomlib/cell.py
    def repeat(self: HasCellT, n: t.Union[int, VecLike]) -> HasCellT:\n    \"\"\"Tile the cell by `n` in each dimension.\"\"\"\n    ns = numpy.broadcast_to(n, 3)\n    if not numpy.issubdtype(ns.dtype, numpy.integer):\n        raise ValueError(\"repeat() argument must be an integer or integer array.\")\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size,\n        cell_angle=self.cell_angle,\n        n_cells=self.n_cells * numpy.broadcast_to(n, 3),\n        pbc = self.pbc | (ns > 1)  # assume periodic along repeated directions\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.explode","title":"explode","text":"
    explode() -> HasCellT\n

    Materialize repeated cells as one supercell.

    Source code in atomlib/cell.py
    def explode(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*self.n_cells,\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.explode_z","title":"explode_z","text":"
    explode_z() -> HasCellT\n

    Materialize repeated cells as one supercell in z.

    Source code in atomlib/cell.py
    def explode_z(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n        n_cells=[*self.n_cells[:2], 1],\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.crop","title":"crop","text":"
    crop(\n    x_min: float = -inf,\n    x_max: float = inf,\n    y_min: float = -inf,\n    y_max: float = inf,\n    z_min: float = -inf,\n    z_max: float = inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> HasCellT\n

    Crop self to the given extents. For a non-orthogonal cell, this must be specified in cell coordinates. This function implicity explodes the cell as well.

    Source code in atomlib/cell.py
    def crop(self: HasCellT, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n         frame: CoordinateFrame = 'local') -> HasCellT:\n    \"\"\"\n    Crop `self` to the given extents. For a non-orthogonal\n    cell, this must be specified in cell coordinates. This\n    function implicity `explode`s the cell as well.\n    \"\"\"\n\n    if not frame.lower().startswith('cell'):\n        if not self.is_orthogonal():\n            raise ValueError(\"Cannot crop a non-orthogonal cell in orthogonal coordinates. Use crop_atoms instead.\")\n\n    min = to_vec3([x_min, y_min, z_min])\n    max = to_vec3([x_max, y_max, z_max])\n    (min, max) = self.get_transform('cell_box', frame).transform([min, max])\n    new_box = BBox3D(min, max) & BBox3D.unit()\n    cropped = (new_box.min > 0.) | (new_box.max < 1.)\n\n    return self.with_cell(Cell(\n        affine=self.affine @ AffineTransform3D.translate(-new_box.min),\n        ortho=self.ortho,\n        cell_size=new_box.size * self.cell_size * numpy.where(cropped, self.n_cells, 1),\n        n_cells=numpy.where(cropped, 1, self.n_cells),\n        cell_angle=self.cell_angle,\n        pbc=self.pbc & ~cropped  # remove periodicity along cropped directions\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.change_transform","title":"change_transform","text":"
    change_transform(\n    transform: AffineTransform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n

    Coordinate-change a transformation from frame_from into frame_to.

    Source code in atomlib/cell.py
    def change_transform(self, transform: Transform3D,\n                     frame_to: t.Optional[CoordinateFrame] = None,\n                     frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n    \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n    if frame_to == frame_from and frame_to is not None:\n        return transform\n    coord_change = self.get_transform(frame_to, frame_from)\n    return coord_change @ transform @ coord_change.inverse()\n
    "},{"location":"api/cell/#atomlib.cell.HasCell.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n
    Source code in atomlib/cell.py
    def assert_equal(self, other: t.Any):\n    assert isinstance(other, HasCell) and type(self) is type(other)\n    numpy.testing.assert_array_almost_equal(self.affine.inner, other.affine.inner, 6)\n    numpy.testing.assert_array_almost_equal(self.ortho.inner, other.ortho.inner, 6)\n    numpy.testing.assert_array_almost_equal(self.cell_size, other.cell_size, 6)\n    numpy.testing.assert_array_equal(self.n_cells, other.n_cells)\n    numpy.testing.assert_array_equal(self.pbc, other.pbc)\n
    "},{"location":"api/cell/#atomlib.cell.Cell","title":"Cell dataclass","text":"

    Bases: HasCell

    Internal class for representing the coordinate systems of a crystal.

    The overall transformation from crystal coordinates to real-space coordinates is is split into four transformations, applied from bottom to top. First is n_cells, which scales from fractions of a unit cell to fractions of a supercell. Next is cell_size, which scales to real-space units. ortho is an orthogonalization matrix, a det = 1 upper-triangular matrix which transforms crystal axes to an orthogonal coordinate system. Finally, affine contains any remaining transformations to the local coordinate system, which atoms are stored in.

    Source code in atomlib/cell.py
    @dataclass(frozen=True, init=False)\nclass Cell(HasCell):\n    \"\"\"\n    Internal class for representing the coordinate systems of a crystal.\n\n    The overall transformation from crystal coordinates to real-space coordinates is\n    is split into four transformations, applied from bottom to top. First is `n_cells`,\n    which scales from fractions of a unit cell to fractions of a supercell. Next is\n    `cell_size`, which scales to real-space units. `ortho` is an orthogonalization\n    matrix, a det = 1 upper-triangular matrix which transforms crystal axes to\n    an orthogonal coordinate system. Finally, `affine` contains any remaining\n    transformations to the local coordinate system, which atoms are stored in.\n    \"\"\"\n\n    def get_cell(self) -> Cell:\n        return self\n\n    def with_cell(self: Cell, cell: Cell) -> Cell:\n        return cell\n\n    _affine: AffineTransform3D = AffineTransform3D()\n    \"\"\"\n    Affine transformation. Holds transformation from `'ortho'` to `'local'` coordinates,\n    including rotation away from the standard crystal orientation.\n    \"\"\"\n\n    _ortho: LinearTransform3D = LinearTransform3D()\n    \"\"\"\n    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.\n    \"\"\"\n\n    _cell_size: NDArray[numpy.float64]\n    \"\"\"Unit cell size.\"\"\"\n    _cell_angle: NDArray[numpy.float64] = field(default_factory=lambda: numpy.full(3, numpy.pi/2.))\n    \"\"\"Unit cell angles, in radians.\"\"\"\n    _n_cells: NDArray[numpy.int64] = field(default_factory=lambda: numpy.ones(3, numpy.int64))\n    \"\"\"Number of unit cells.\"\"\"\n    _pbc: NDArray[numpy.bool_] = field(default_factory=lambda: numpy.ones(3, numpy.bool_))\n    \"\"\"Flags indicating the presence of periodic boundary conditions along each axis.\"\"\"\n\n    def __init__(self, *,\n        affine: t.Optional[AffineTransform3D] = None, ortho: t.Optional[LinearTransform3D] = None,\n        cell_size: VecLike, cell_angle: t.Optional[VecLike] = None,\n        n_cells: t.Optional[VecLike] = None, pbc: t.Optional[VecLike] = None):\n\n        object.__setattr__(self, '_affine', AffineTransform3D() if affine is None else affine)\n        object.__setattr__(self, '_ortho', LinearTransform3D() if ortho is None else ortho)\n        object.__setattr__(self, '_cell_size', to_vec3(cell_size))\n        object.__setattr__(self, '_cell_angle', numpy.full(3, numpy.pi/2.) if cell_angle is None else to_vec3(cell_angle))\n        object.__setattr__(self, '_n_cells', numpy.ones(3, numpy.int_) if n_cells is None else to_vec3(n_cells, numpy.int64))\n        object.__setattr__(self, '_pbc', numpy.ones(3, numpy.bool_) if pbc is None else to_vec3(pbc, numpy.bool_))\n\n    @staticmethod\n    def from_unit_cell(cell_size: VecLike, cell_angle: t.Optional[VecLike] = None, n_cells: t.Optional[VecLike] = None,\n                       pbc: t.Optional[VecLike] = None):\n        return Cell(\n            ortho=cell_to_ortho([1.]*3, cell_angle),\n            n_cells=to_vec3([1]*3 if n_cells is None else n_cells, numpy.int_),\n            cell_size=to_vec3(cell_size),\n            cell_angle=to_vec3([numpy.pi/2.]*3 if cell_angle is None else cell_angle),\n            pbc=pbc\n        )\n\n    @staticmethod\n    def from_ortho(ortho: AffineTransform3D, n_cells: t.Optional[VecLike] = None, pbc: t.Optional[VecLike] = None):\n        lin = ortho.to_linear()\n        # decompose into orthogonal and upper triangular\n        q, r = numpy.linalg.qr(lin.inner)\n\n        # flip QR decomposition so R has positive diagonals\n        signs = numpy.sign(numpy.diagonal(r))\n        # multiply flips to columns of Q, rows of R\n        q = q * signs\n        r = r * signs[:, None]\n        #numpy.testing.assert_allclose(q @ r, lin.inner)\n        if numpy.linalg.det(q) < 0:\n            warn(\"Crystal is left-handed. This is currently unsupported, and may cause errors.\")\n            # currently, behavior is to leave `ortho` proper, and move the inversion into the affine transform\n\n        cell_size, cell_angle = ortho_to_cell(lin)\n        return Cell(\n            affine=LinearTransform3D(q).translate(ortho.translation()),\n            ortho=LinearTransform3D(r / cell_size).round_near_zero(),\n            cell_size=cell_size, cell_angle=cell_angle,\n            n_cells=to_vec3([1]*3 if n_cells is None else n_cells, numpy.int_),\n            pbc=pbc,\n        )\n\n    def __str__(self) -> str:\n        return \"\\n\".join((\n            self.__class__.__name__,\n            f\"Cell size: {self.cell_size!r}\",\n            f\"Cell angle: {self.cell_angle!r}\",\n            f\"# cells: {self.n_cells!r}\",\n            f\"pbc: {self.pbc!r}\",\n        ))\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}(\"\n            f\"ortho={self.ortho}, affine={self.affine}, cell_size={self.cell_size}, \"\n            f\"cell_angle={self.cell_angle}, n_cells={self.n_cells}, pbc={self.pbc})\"\n        )\n\n    def _repr_pretty_(self, p: t.Any, cycle: bool) -> None:\n        p.text(f\"{self.__class__.__name__}(...)\") if cycle else p.text(str(self))\n
    "},{"location":"api/cell/#atomlib.cell.Cell.affine","title":"affine property","text":"
    affine: AffineTransform3D\n

    Affine transformation. Holds transformation from 'ortho' to 'local' coordinates, including rotation away from the standard crystal orientation.

    "},{"location":"api/cell/#atomlib.cell.Cell.ortho","title":"ortho property","text":"
    ortho: LinearTransform3D\n

    Orthogonalization transformation. Skews but does not scale the crystal axes to cartesian axes.

    "},{"location":"api/cell/#atomlib.cell.Cell.metric","title":"metric property","text":"
    metric: LinearTransform3D\n

    Cell metric tensor

    Returns the dot product between every combination of basis vectors. :math:\\mathbf{a} \\cdot \\mathbf{b} = a_i M_ij b_j

    "},{"location":"api/cell/#atomlib.cell.Cell.cell_size","title":"cell_size property","text":"
    cell_size: NDArray[float64]\n

    Unit cell size.

    "},{"location":"api/cell/#atomlib.cell.Cell.cell_angle","title":"cell_angle property","text":"
    cell_angle: NDArray[float64]\n

    Unit cell angles, in radians.

    "},{"location":"api/cell/#atomlib.cell.Cell.n_cells","title":"n_cells property","text":"
    n_cells: NDArray[int_]\n

    Number of unit cells.

    "},{"location":"api/cell/#atomlib.cell.Cell.pbc","title":"pbc property","text":"
    pbc: NDArray[bool_]\n

    Flags indicating the presence of periodic boundary conditions along each axis.

    "},{"location":"api/cell/#atomlib.cell.Cell.ortho_size","title":"ortho_size property","text":"
    ortho_size: NDArray[float64]\n

    Return size of orthogonal unit cell.

    Equivalent to the diagonal of the orthogonalization matrix.

    "},{"location":"api/cell/#atomlib.cell.Cell.box_size","title":"box_size property","text":"
    box_size: NDArray[float64]\n

    Return size of the cell box.

    Equivalent to self.n_cells * self.cell_size.

    "},{"location":"api/cell/#atomlib.cell.Cell.bbox","title":"bbox class-attribute instance-attribute","text":"
    bbox = bbox_cell\n
    "},{"location":"api/cell/#atomlib.cell.Cell.get_transform","title":"get_transform","text":"
    get_transform(\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n

    In the two-argument form, get the transform to frame_to from frame_from. In the one-argument form, get the transform from local coordinates to 'frame'.

    Source code in atomlib/cell.py
    def get_transform(self, frame_to: t.Optional[CoordinateFrame] = None, frame_from: t.Optional[CoordinateFrame] = None) -> AffineTransform3D:\n    \"\"\"\n    In the two-argument form, get the transform to `frame_to` from `frame_from`.\n    In the one-argument form, get the transform from local coordinates to 'frame'.\n    \"\"\"\n    transform_from = self._get_transform_to_local(frame_from) if frame_from is not None else AffineTransform3D()\n    transform_to = self._get_transform_to_local(frame_to) if frame_to is not None else AffineTransform3D()\n    if frame_from is not None and frame_to is not None and frame_from.lower() == frame_to.lower():\n        return AffineTransform3D()\n    return transform_to.inverse() @ transform_from\n
    "},{"location":"api/cell/#atomlib.cell.Cell.corners","title":"corners","text":"
    corners(frame: CoordinateFrame = 'local') -> ndarray\n
    Source code in atomlib/cell.py
    def corners(self, frame: CoordinateFrame = 'local') -> numpy.ndarray:\n    corners = numpy.array(list(itertools.product((0., 1.), repeat=3)))\n    return self.get_transform(frame, 'cell_box') @ corners\n
    "},{"location":"api/cell/#atomlib.cell.Cell.bbox_cell","title":"bbox_cell","text":"
    bbox_cell(frame: CoordinateFrame = 'local') -> BBox3D\n

    Return the bounding box of the cell box in the given coordinate system.

    Source code in atomlib/cell.py
    def bbox_cell(self, frame: CoordinateFrame = 'local') -> BBox3D:\n    \"\"\"Return the bounding box of the cell box in the given coordinate system.\"\"\"\n    return BBox3D.from_pts(self.corners(frame))\n
    "},{"location":"api/cell/#atomlib.cell.Cell.is_orthogonal","title":"is_orthogonal","text":"
    is_orthogonal(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal (axes are at right angles.)

    Source code in atomlib/cell.py
    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal (axes are at right angles.)\"\"\"\n    return self.ortho.is_diagonal(tol=tol)\n
    "},{"location":"api/cell/#atomlib.cell.Cell.is_orthogonal_in_local","title":"is_orthogonal_in_local","text":"
    is_orthogonal_in_local(tol: float = 1e-08) -> bool\n

    Returns whether this cell is orthogonal and aligned with the local coordinate system.

    Source code in atomlib/cell.py
    def is_orthogonal_in_local(self, tol: float = 1e-8) -> bool:\n    \"\"\"Returns whether this cell is orthogonal and aligned with the local coordinate system.\"\"\"\n    transform = (self.affine @ self.ortho).to_linear()\n    if not transform.is_scaled_orthogonal(tol):\n        return False\n    normed = transform.inner / numpy.linalg.norm(transform.inner, axis=-2, keepdims=True)\n    # every row of transform must be a +/- 1 times a basis vector (i, j, or k)\n    return all(\n        any(numpy.isclose(numpy.abs(numpy.dot(row, v)), 1., atol=tol) for v in numpy.eye(3))\n        for row in normed\n    )\n
    "},{"location":"api/cell/#atomlib.cell.Cell.to_ortho","title":"to_ortho","text":"
    to_ortho() -> AffineTransform3D\n
    Source code in atomlib/cell.py
    def to_ortho(self) -> AffineTransform3D:\n    return self.get_transform('local', 'cell_box')\n
    "},{"location":"api/cell/#atomlib.cell.Cell.transform_cell","title":"transform_cell","text":"
    transform_cell(\n    transform: AffineTransform3D,\n    frame: CoordinateFrame = \"local\",\n) -> HasCellT\n

    Apply the given transform to the unit cell, and return a new Cell. The transform is applied in coordinate frame 'frame'. Orthogonal and affine transformations are applied to the affine matrix component, while skew and scaling is applied to the orthogonalization matrix/cell_size.

    Source code in atomlib/cell.py
    def transform_cell(self: HasCellT, transform: AffineTransform3D, frame: CoordinateFrame = 'local') -> HasCellT:\n    \"\"\"\n    Apply the given transform to the unit cell, and return a new `Cell`.\n    The transform is applied in coordinate frame 'frame'.\n    Orthogonal and affine transformations are applied to the affine matrix component,\n    while skew and scaling is applied to the orthogonalization matrix/cell_size.\n    \"\"\"\n    transform = t.cast(AffineTransform3D, self.change_transform(transform, 'local', frame))\n    if not transform.to_linear().is_orthogonal():\n        raise NotImplementedError()\n    return self.with_cell(Cell(\n        affine=transform @ self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size,\n        cell_angle=self.cell_angle,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.Cell.strain_orthogonal","title":"strain_orthogonal","text":"
    strain_orthogonal() -> HasCellT\n

    Orthogonalize using strain.

    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane. For small displacements, no hydrostatic strain is applied (volume is conserved).

    Source code in atomlib/cell.py
    def strain_orthogonal(self: HasCellT) -> HasCellT:\n    \"\"\"\n    Orthogonalize using strain.\n\n    Strain is applied such that the x-axis remains fixed, and the y-axis remains in the xy plane.\n    For small displacements, no hydrostatic strain is applied (volume is conserved).\n    \"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=LinearTransform3D(),\n        cell_size=self.cell_size,\n        n_cells=self.n_cells,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.Cell.repeat","title":"repeat","text":"
    repeat(n: Union[int, VecLike]) -> HasCellT\n

    Tile the cell by n in each dimension.

    Source code in atomlib/cell.py
    def repeat(self: HasCellT, n: t.Union[int, VecLike]) -> HasCellT:\n    \"\"\"Tile the cell by `n` in each dimension.\"\"\"\n    ns = numpy.broadcast_to(n, 3)\n    if not numpy.issubdtype(ns.dtype, numpy.integer):\n        raise ValueError(\"repeat() argument must be an integer or integer array.\")\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size,\n        cell_angle=self.cell_angle,\n        n_cells=self.n_cells * numpy.broadcast_to(n, 3),\n        pbc = self.pbc | (ns > 1)  # assume periodic along repeated directions\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.Cell.explode","title":"explode","text":"
    explode() -> HasCellT\n

    Materialize repeated cells as one supercell.

    Source code in atomlib/cell.py
    def explode(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*self.n_cells,\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.Cell.explode_z","title":"explode_z","text":"
    explode_z() -> HasCellT\n

    Materialize repeated cells as one supercell in z.

    Source code in atomlib/cell.py
    def explode_z(self: HasCellT) -> HasCellT:\n    \"\"\"Materialize repeated cells as one supercell in z.\"\"\"\n    return self.with_cell(Cell(\n        affine=self.affine,\n        ortho=self.ortho,\n        cell_size=self.cell_size*[1, 1, self.n_cells[2]],\n        n_cells=[*self.n_cells[:2], 1],\n        cell_angle=self.cell_angle,\n        pbc=self.pbc,\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.Cell.crop","title":"crop","text":"
    crop(\n    x_min: float = -inf,\n    x_max: float = inf,\n    y_min: float = -inf,\n    y_max: float = inf,\n    z_min: float = -inf,\n    z_max: float = inf,\n    *,\n    frame: CoordinateFrame = \"local\"\n) -> HasCellT\n

    Crop self to the given extents. For a non-orthogonal cell, this must be specified in cell coordinates. This function implicity explodes the cell as well.

    Source code in atomlib/cell.py
    def crop(self: HasCellT, x_min: float = -numpy.inf, x_max: float = numpy.inf,\n         y_min: float = -numpy.inf, y_max: float = numpy.inf,\n         z_min: float = -numpy.inf, z_max: float = numpy.inf, *,\n         frame: CoordinateFrame = 'local') -> HasCellT:\n    \"\"\"\n    Crop `self` to the given extents. For a non-orthogonal\n    cell, this must be specified in cell coordinates. This\n    function implicity `explode`s the cell as well.\n    \"\"\"\n\n    if not frame.lower().startswith('cell'):\n        if not self.is_orthogonal():\n            raise ValueError(\"Cannot crop a non-orthogonal cell in orthogonal coordinates. Use crop_atoms instead.\")\n\n    min = to_vec3([x_min, y_min, z_min])\n    max = to_vec3([x_max, y_max, z_max])\n    (min, max) = self.get_transform('cell_box', frame).transform([min, max])\n    new_box = BBox3D(min, max) & BBox3D.unit()\n    cropped = (new_box.min > 0.) | (new_box.max < 1.)\n\n    return self.with_cell(Cell(\n        affine=self.affine @ AffineTransform3D.translate(-new_box.min),\n        ortho=self.ortho,\n        cell_size=new_box.size * self.cell_size * numpy.where(cropped, self.n_cells, 1),\n        n_cells=numpy.where(cropped, 1, self.n_cells),\n        cell_angle=self.cell_angle,\n        pbc=self.pbc & ~cropped  # remove periodicity along cropped directions\n    ))\n
    "},{"location":"api/cell/#atomlib.cell.Cell.change_transform","title":"change_transform","text":"
    change_transform(\n    transform: AffineTransform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> AffineTransform3D\n
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n
    change_transform(\n    transform: Transform3D,\n    frame_to: Optional[CoordinateFrame] = None,\n    frame_from: Optional[CoordinateFrame] = None,\n) -> Transform3D\n

    Coordinate-change a transformation from frame_from into frame_to.

    Source code in atomlib/cell.py
    def change_transform(self, transform: Transform3D,\n                     frame_to: t.Optional[CoordinateFrame] = None,\n                     frame_from: t.Optional[CoordinateFrame] = None) -> Transform3D:\n    \"\"\"Coordinate-change a transformation from `frame_from` into `frame_to`.\"\"\"\n    if frame_to == frame_from and frame_to is not None:\n        return transform\n    coord_change = self.get_transform(frame_to, frame_from)\n    return coord_change @ transform @ coord_change.inverse()\n
    "},{"location":"api/cell/#atomlib.cell.Cell.assert_equal","title":"assert_equal","text":"
    assert_equal(other: Any)\n
    Source code in atomlib/cell.py
    def assert_equal(self, other: t.Any):\n    assert isinstance(other, HasCell) and type(self) is type(other)\n    numpy.testing.assert_array_almost_equal(self.affine.inner, other.affine.inner, 6)\n    numpy.testing.assert_array_almost_equal(self.ortho.inner, other.ortho.inner, 6)\n    numpy.testing.assert_array_almost_equal(self.cell_size, other.cell_size, 6)\n    numpy.testing.assert_array_equal(self.n_cells, other.n_cells)\n    numpy.testing.assert_array_equal(self.pbc, other.pbc)\n
    "},{"location":"api/cell/#atomlib.cell.Cell.get_cell","title":"get_cell","text":"
    get_cell() -> Cell\n
    Source code in atomlib/cell.py
    def get_cell(self) -> Cell:\n    return self\n
    "},{"location":"api/cell/#atomlib.cell.Cell.with_cell","title":"with_cell","text":"
    with_cell(cell: Cell) -> Cell\n
    Source code in atomlib/cell.py
    def with_cell(self: Cell, cell: Cell) -> Cell:\n    return cell\n
    "},{"location":"api/cell/#atomlib.cell.Cell.from_unit_cell","title":"from_unit_cell staticmethod","text":"
    from_unit_cell(\n    cell_size: VecLike,\n    cell_angle: Optional[VecLike] = None,\n    n_cells: Optional[VecLike] = None,\n    pbc: Optional[VecLike] = None,\n)\n
    Source code in atomlib/cell.py
    @staticmethod\ndef from_unit_cell(cell_size: VecLike, cell_angle: t.Optional[VecLike] = None, n_cells: t.Optional[VecLike] = None,\n                   pbc: t.Optional[VecLike] = None):\n    return Cell(\n        ortho=cell_to_ortho([1.]*3, cell_angle),\n        n_cells=to_vec3([1]*3 if n_cells is None else n_cells, numpy.int_),\n        cell_size=to_vec3(cell_size),\n        cell_angle=to_vec3([numpy.pi/2.]*3 if cell_angle is None else cell_angle),\n        pbc=pbc\n    )\n
    "},{"location":"api/cell/#atomlib.cell.Cell.from_ortho","title":"from_ortho staticmethod","text":"
    from_ortho(\n    ortho: AffineTransform3D,\n    n_cells: Optional[VecLike] = None,\n    pbc: Optional[VecLike] = None,\n)\n
    Source code in atomlib/cell.py
    @staticmethod\ndef from_ortho(ortho: AffineTransform3D, n_cells: t.Optional[VecLike] = None, pbc: t.Optional[VecLike] = None):\n    lin = ortho.to_linear()\n    # decompose into orthogonal and upper triangular\n    q, r = numpy.linalg.qr(lin.inner)\n\n    # flip QR decomposition so R has positive diagonals\n    signs = numpy.sign(numpy.diagonal(r))\n    # multiply flips to columns of Q, rows of R\n    q = q * signs\n    r = r * signs[:, None]\n    #numpy.testing.assert_allclose(q @ r, lin.inner)\n    if numpy.linalg.det(q) < 0:\n        warn(\"Crystal is left-handed. This is currently unsupported, and may cause errors.\")\n        # currently, behavior is to leave `ortho` proper, and move the inversion into the affine transform\n\n    cell_size, cell_angle = ortho_to_cell(lin)\n    return Cell(\n        affine=LinearTransform3D(q).translate(ortho.translation()),\n        ortho=LinearTransform3D(r / cell_size).round_near_zero(),\n        cell_size=cell_size, cell_angle=cell_angle,\n        n_cells=to_vec3([1]*3 if n_cells is None else n_cells, numpy.int_),\n        pbc=pbc,\n    )\n
    "},{"location":"api/cell/#atomlib.cell.cell_to_ortho","title":"cell_to_ortho","text":"
    cell_to_ortho(\n    cell_size: VecLike, cell_angle: Optional[VecLike] = None\n) -> LinearTransform3D\n

    Get orthogonalization transform from unit cell parameters (which turns fractional cell coordinates into real-space coordinates). .

    Source code in atomlib/cell.py
    def cell_to_ortho(cell_size: VecLike, cell_angle: t.Optional[VecLike] = None) -> LinearTransform3D:\n    \"\"\"\n    Get orthogonalization transform from unit cell parameters (which turns fractional cell coordinates into real-space coordinates).\n    .\"\"\"\n    (a, b, c) = _validate_cell_size(cell_size)\n    cell_angle = _validate_cell_angle(cell_angle)\n\n    if numpy.allclose(cell_angle.view(numpy.ndarray), numpy.pi/2.):\n        return LinearTransform3D.scale(a, b, c)\n\n    (alpha, beta, gamma) = cell_angle\n    alphastar = numpy.cos(beta) * numpy.cos(gamma) - numpy.cos(alpha)\n    alphastar /= numpy.sin(beta) * numpy.sin(gamma)\n    alphastar = numpy.arccos(alphastar)\n    assert not numpy.isnan(alphastar)\n\n    # aligns a axis along x\n    # aligns b axis in the x-y plane\n    return LinearTransform3D(numpy.array([\n        [a,  b * numpy.cos(gamma),  c * numpy.cos(beta)],\n        [0.,  b * numpy.sin(gamma), -c * numpy.sin(beta) * numpy.cos(alphastar)],\n        [0.,  0.,                     c * numpy.sin(beta) * numpy.sin(alphastar)],\n    ], dtype=float)).round_near_zero()\n
    "},{"location":"api/cell/#atomlib.cell.ortho_to_cell","title":"ortho_to_cell","text":"
    ortho_to_cell(\n    ortho: LinearTransform3D,\n) -> Tuple[Vec3, Vec3]\n

    Get unit cell parameters (cell_size, cell_angle) from orthogonalization transform.

    Source code in atomlib/cell.py
    def ortho_to_cell(ortho: LinearTransform3D) -> t.Tuple[Vec3, Vec3]:\n    \"\"\"Get unit cell parameters `(cell_size, cell_angle)` from orthogonalization transform.\"\"\"\n    # TODO suspect\n    cell_size = numpy.linalg.norm(ortho.inner, axis=-2)\n    cell_size = _validate_cell_size(cell_size)\n    normed = ortho.inner / cell_size\n    cosines = numpy.array([\n        numpy.dot(normed[..., 1], normed[..., 2]), # alpha\n        numpy.dot(normed[..., 2], normed[..., 0]), # beta\n        numpy.dot(normed[..., 0], normed[..., 1]), # gamma\n    ])\n    cell_angle = numpy.arccos(cosines)\n    cell_angle = _validate_cell_angle(cell_angle)\n\n    return (cell_size, cell_angle)\n
    "},{"location":"api/cell/#atomlib.cell.plane_to_zone","title":"plane_to_zone","text":"
    plane_to_zone(\n    metric: LinearTransform3D,\n    plane: VecLike,\n    reduce: bool = True,\n) -> Vec3\n

    Return the zone axis associated with a given crystallographic plane. If reduce is True, call reduce_vec before returning. Otherwise, return a unit vector.

    Source code in atomlib/cell.py
    def plane_to_zone(metric: LinearTransform3D, plane: VecLike, reduce: bool = True) -> Vec3:\n    \"\"\"\n    Return the zone axis associated with a given crystallographic plane.\n    If `reduce` is `True`, call `reduce_vec` before returning. Otherwise,\n    return a unit vector.\n    \"\"\"\n\n    plane = to_vec3(plane)\n    if metric.is_orthogonal():\n        return plane\n\n    # reciprocal lattice is transpose of inverse of real lattice\n    # [b1 b2 b3]^T = [a1 a2 a3]^-1\n    # so real indices [uvw] = O^-1 O^-1^T (hkl)\n    # O^-1 O^-1^T = (O^T O)^-1 = M^-1\n    # in other words, we are raising the index of the tensor,\n    # converting a covector into a vector\n    zone = metric.inverse() @ plane\n\n    if reduce:\n        return to_vec3(reduce_vec(zone))\n    # otherwise reduce to unit vector\n    return zone / float(numpy.linalg.norm(zone))\n
    "},{"location":"api/cell/#atomlib.cell.zone_to_plane","title":"zone_to_plane","text":"
    zone_to_plane(\n    metric: LinearTransform3D,\n    zone: VecLike,\n    reduce: bool = True,\n) -> Vec3\n

    Return the crystallographic plane associated with a given zone axis. If reduce is True, call reduce_vec before returning. Otherwise, return a unit vector.

    Source code in atomlib/cell.py
    def zone_to_plane(metric: LinearTransform3D, zone: VecLike, reduce: bool = True) -> Vec3:\n    \"\"\"\n    Return the crystallographic plane associated with a given zone axis.\n    If `reduce` is True, call `reduce_vec` before returning. Otherwise,\n    return a unit vector.\n    \"\"\"\n\n    zone = to_vec3(zone)\n    if metric.is_orthogonal():\n        return zone\n\n    plane = metric @ zone\n\n    if reduce:\n        return to_vec3(reduce_vec(plane))\n    # otherwise reduce to unit vector\n    return plane / float(numpy.linalg.norm(plane))\n
    "},{"location":"api/defect/","title":"atomlib.defect","text":"

    A collection of functions for inserting dislocations into structures.

    "},{"location":"api/defect/#atomlib.defect.CutType","title":"CutType module-attribute","text":"
    CutType: TypeAlias = Literal['shift', 'add', 'rm']\n

    Cut plane to use when creating a (non-screw) dislocation.

    "},{"location":"api/defect/#atomlib.defect.ellip_pi","title":"ellip_pi","text":"
    ellip_pi(\n    n: NDArray[float64], m: NDArray[float64]\n) -> NDArray[float64]\n

    Complete elliptic integral of the third kind, \\(\\Pi(n | m)\\).

    Follows the definition of Wolfram Mathworld.

    Source code in atomlib/defect.py
    def ellip_pi(n: NDArray[numpy.float64], m: NDArray[numpy.float64]) -> NDArray[numpy.float64]:\n    \"\"\"\n    Complete elliptic integral of the third kind, $\\\\Pi(n | m)$.\n\n    Follows the definition of [Wolfram Mathworld][wolfram_ellip_pi].\n\n    [wolfram_ellip_pi]: https://mathworld.wolfram.com/EllipticIntegraloftheThirdKind.html\n    \"\"\"\n    from scipy.special import elliprf, elliprj  # type: ignore\n\n    y = 1 - m\n    assert numpy.all(y > 0)\n\n    rf = elliprf(0, y, 1)\n    rj = elliprj(0, y, 1, 1 - n)\n    return rf + rj * n / 3\n
    "},{"location":"api/defect/#atomlib.defect.stacking_fault","title":"stacking_fault","text":"
    stacking_fault(\n    atoms: HasAtomsT,\n    pos: VecLike,\n    shift: VecLike,\n    plane: VecLike,\n) -> HasAtomsT\n

    Add a stacking fault to the structure.

    The fault plane will pass through the position pos, with normal plane. Atoms above plane will be shifted by the vector shift.

    If there is a component of shift parallel to plane, applying the shift will create (or remove) volume. In this case, atoms are added (or removed) accordingly. If shift \\(\\cdot\\) plane is positive, atoms are added.

    In general, adding a stacking fault will not preserve a cell's periodicity. Please verify this for your usecase.

    PARAMETER DESCRIPTION atoms

    Structure to add fault to

    TYPE: HasAtomsT

    pos

    Position on fault plane

    TYPE: VecLike

    shift

    Vector to shift by

    TYPE: VecLike

    plane

    Normal to fault plane

    TYPE: VecLike

    RETURNS DESCRIPTION HasAtomsT

    Structure with a stacking fault added

    Source code in atomlib/defect.py
    def stacking_fault(atoms: HasAtomsT, pos: VecLike, shift: VecLike, plane: VecLike) -> HasAtomsT:\n    \"\"\"\n    Add a stacking fault to the structure.\n\n    The fault plane will pass through the position `pos`, with normal `plane`.\n    Atoms above `plane` will be shifted by the vector `shift`.\n\n    If there is a component of `shift` parallel to `plane`, applying the shift\n    will create (or remove) volume. In this case, atoms are added (or removed)\n    accordingly. If `shift` $\\\\cdot$ `plane` is positive, atoms are added.\n\n    In general, adding a stacking fault will not preserve a cell's periodicity.\n    Please verify this for your usecase.\n\n    Args:\n      atoms: Structure to add fault to\n      pos: Position on fault plane\n      shift: Vector to shift by\n      plane: Normal to fault plane\n\n    Returns:\n      Structure with a stacking fault added\n    \"\"\"\n    pos = to_vec3(pos)\n    shift = to_vec3(shift)\n    plane = to_vec3(plane)\n    plane /= numpy.linalg.norm(plane)\n\n    coords = atoms.coords(frame='local')  # type: ignore\n\n    perp_dist = numpy.dot(coords - pos, plane)\n    atoms = atoms.with_columns(perp_dist=polars.Series(perp_dist)) \\\n        .with_columns(branch=polars.col('perp_dist') > 0.)\n\n    d = numpy.dot(shift, plane)\n    if -d > 1e-8:\n        logging.info(\"Removing atoms.\")\n        old_len = len(atoms)\n        atoms = atoms.filter(\n            (polars.col('perp_dist') < 0) | (polars.col('perp_dist') >= -d)\n        )\n        logging.info(f\"Removed {old_len - len(atoms)} atoms\")\n    if d > 1e-8:\n        logging.info(\"Duplicating atoms.\")\n        duplicate = atoms.filter(\n            (polars.col('perp_dist') >= -d) & (polars.col('perp_dist') < 0)\n        ).with_columns(~polars.col('branch'))\n        logging.info(f\"Duplicated {len(duplicate)} atoms\")\n\n        atoms = atoms.with_atoms(Atoms.concat((atoms, duplicate)))\n\n    coords = atoms.coords(frame='local')\n    branch = atoms['branch'].to_numpy()\n    atoms = atoms.with_coords(coords + shift * branch[:, None], frame='local')\n    return atoms.with_atoms(Atoms(atoms.drop('perp_dist', 'branch')))\n
    "},{"location":"api/defect/#atomlib.defect.disloc_edge","title":"disloc_edge","text":"
    disloc_edge(\n    atoms: HasAtomsT,\n    center: VecLike,\n    b: VecLike,\n    t: VecLike,\n    cut: Union[CutType, VecLike] = \"shift\",\n    *,\n    poisson: float = 0.25\n) -> HasAtomsT\n

    Add a Volterra edge dislocation to the structure.

    The dislocation will pass through center, with line vector t and Burgers vector b. t will be modified such that it is perpendicular to b.

    The cut parameter defines the cut (discontinuity) plane used to displace the atoms. By default, cut is 'shift', which defines the cut plane to contain b, ensuring no atoms need to be added or removed. Instead, 'add' or 'rm' may be specified, which defines the cut plane as containing \\(\\mathbf{b} \\times \\mathbf{t}\\). In this mode, atoms will be added or removed in the plane of b to create the dislocation. Alternatively, a vector cut may be supplied. The cut plane will contain this vector.

    In the coordinate system where t is along \\(z\\), and b is along \\(+x\\), the displacement due to the edge dislocation can be calculated as follows:

    \\[\\begin{align} u_x &= \\frac{b}{2\\pi} \\left( \\arctan(x, y) + \\frac{x y}{2(x^2 + y^2)(1-\\nu)} \\right) \\\\ u_y &= -\\frac{b}{4(1-\\nu)} \\left( (1-2\\nu) \\ln(x^2 + y^2) + \\frac{x^2 - y^2}{x^2 + y^2} \\right) \\end{align}\\]

    Where \\(x\\) and \\(y\\) are distances from the dislocation center. This creates a discontinuity along the \\(-x\\) axis. This coordinate system is rotated to support branches along an arbitrary axis.

    The dislocation is defined by the FS/RH convention: Performing one loop of positive sense relative to the tangent vector displaces the real crystal one b relative to a reference crystal.

    PARAMETER DESCRIPTION atoms

    Structure to add edge dislocation to

    TYPE: HasAtomsT

    center

    Position on dislocation line

    TYPE: VecLike

    b

    Burgers vector of dislocation (FS/RH convention)

    TYPE: VecLike

    t

    Tangent vector of dislocation

    TYPE: VecLike

    cut

    Cut plane to create dislocation on

    TYPE: Union[CutType, VecLike] DEFAULT: 'shift'

    poisson

    Poisson ratio of material

    TYPE: float DEFAULT: 0.25

    RETURNS DESCRIPTION HasAtomsT

    Structure with an edge dislocation added

    Source code in atomlib/defect.py
    def disloc_edge(atoms: HasAtomsT, center: VecLike, b: VecLike, t: VecLike, cut: t.Union[CutType, VecLike] = 'shift',\n                *, poisson: float = 0.25) -> HasAtomsT:\n    r\"\"\"\n    Add a Volterra edge dislocation to the structure.\n\n    The dislocation will pass through `center`, with line vector `t` and Burgers vector `b`.\n    `t` will be modified such that it is perpendicular to `b`.\n\n    The `cut` parameter defines the cut (discontinuity) plane used to displace the atoms.\n    By default, `cut` is `'shift'`, which defines the cut plane to contain `b`, ensuring no atoms\n    need to be added or removed.\n    Instead, `'add'` or `'rm'` may be specified, which defines the cut plane as containing\n    $\\mathbf{b} \\times \\mathbf{t}$. In this mode, atoms will be added or removed\n    in the plane of `b` to create the dislocation. Alternatively, a vector `cut`\n    may be supplied. The cut plane will contain this vector.\n\n    In the coordinate system where `t` is along $z$, and `b` is along $+x$, the displacement\n    due to the edge dislocation can be calculated as follows:\n\n    $$\\begin{align}\n       u_x &= \\frac{b}{2\\pi} \\left( \\arctan(x, y) + \\frac{x y}{2(x^2 + y^2)(1-\\nu)} \\right) \\\\\n       u_y &= -\\frac{b}{4(1-\\nu)} \\left( (1-2\\nu) \\ln(x^2 + y^2) + \\frac{x^2 - y^2}{x^2 + y^2} \\right)\n    \\end{align}$$\n\n    Where $x$ and $y$ are distances from the dislocation center. This creates a discontinuity along\n    the $-x$ axis. This coordinate system is rotated to support branches along an arbitrary axis.\n\n    The dislocation is defined by the FS/RH convention: Performing one loop of positive sense\n    relative to the tangent vector displaces the real crystal one `b` relative to a reference\n    crystal.\n\n    Args:\n      atoms: Structure to add edge dislocation to\n      center: Position on dislocation line\n      b: Burgers vector of dislocation (FS/RH convention)\n      t: Tangent vector of dislocation\n      cut: Cut plane to create dislocation on\n      poisson: Poisson ratio of material\n\n    Returns:\n      Structure with an edge dislocation added\n    \"\"\"\n\n    center = to_vec3(center)\n    b_vec = to_vec3(b)\n    #b_mag = norm(b_vec)\n\n    # get component of t perpendicular to b, normalize\n    t = to_vec3(t)\n    t = perp(t, b)\n    if norm(t) < 1e-10:\n        raise ValueError(\"`b` and `t` must be different.\")\n    t /= norm(t)\n\n    if isinstance(cut, str):\n        cut = cast(CutType, cut.lower())\n        if cut == 'shift':\n            plane_v = b_vec.copy()\n        elif cut == 'add':\n            # FS/RH convention: t x b points to extra half plane\n            plane_v = numpy.cross(t, b_vec)\n        elif cut == 'rm':\n            plane_v = -numpy.cross(t, b_vec)\n        else:\n            raise ValueError(f\"Unknown cut plane type `{cut}`. Expected 'shift', 'add', 'rm', or a vector.\")\n        plane_v /= norm(plane_v)\n    else:\n        plane_v = to_vec3(cut)\n        plane_v = plane_v / norm(plane_v)\n        if numpy.linalg.norm(numpy.cross(plane_v, t)) < 1e-10:\n            raise ValueError('`cut` and `t` must be different.')\n\n    # translate center to 0., and align t to [0, 0, 1], plane to +y\n    transform = AffineTransform3D.translate(center).inverse().compose(\n        LinearTransform3D.align_to(t, [0., 0., 1.], plane_v, [-1., 0., 0.])\n    )\n    frame = atoms.get_atoms('local').transform(transform)\n    b_vec = transform.transform_vec(b_vec)\n\n    d = numpy.dot(b_vec, [0., 1., 0.])\n    if -d > 1e-8:\n        logging.info(\"Removing atoms.\")\n        old_len = len(frame)\n        frame = frame.filter(~(\n            (polars.col('coords').arr.get(0) < 0)\n            & (polars.col('coords').arr.get(1) >= d/2.)\n            & (polars.col('coords').arr.get(1) <= -d/2.)\n        ))\n        logging.info(f\"Removed {old_len - len(frame)} atoms\")\n    if d > 1e-8:\n        logging.info(\"Duplicating atoms.\")\n        duplicate = frame.filter(\n            (polars.col('coords').arr.get(0) < 0)\n            & (polars.col('coords').arr.get(1) >= -d/2.)\n            & (polars.col('coords').arr.get(1) <= d/2.)\n        )\n        logging.info(f\"Duplicated {len(duplicate)} atoms\")\n\n        frame = Atoms.concat((frame, duplicate))\n        #atoms = atoms._replace_atoms(frame)\n        branch = numpy.ones(len(frame), dtype=float)\n        if len(duplicate) > 0:\n            branch[-len(duplicate):] *= -1  # flip branch of duplicated atoms\n    else:\n        branch = numpy.ones(len(frame), dtype=float)\n\n    pts = frame.coords()\n\n    x, y, _ = split_arr(pts, axis=-1)\n    r2 = x**2 + y**2\n\n    # displacement parallel and perpendicular to b\n    d_para = branch * numpy.arctan2(y, x) + x*y/(2*(1-poisson)*r2)\n    d_perp = -(1-2*poisson)/(4*(1-poisson)) * numpy.log(r2) + (y**2 - x**2)/(4*(1-poisson)*r2)\n\n    disps = numpy.stack([\n        d_para * b_vec[0] + d_perp * b_vec[1],\n        d_perp * b_vec[0] + d_para * b_vec[1],\n        numpy.zeros_like(x)\n    ], axis=-1) / (2*numpy.pi)\n\n    return atoms.with_atoms(frame.with_coords(pts + disps).transform(transform.inverse()), 'local')\n
    "},{"location":"api/defect/#atomlib.defect.disloc_screw","title":"disloc_screw","text":"
    disloc_screw(\n    atoms: HasAtomsT,\n    center: VecLike,\n    b: VecLike,\n    cut: Optional[VecLike] = None,\n    sign: bool = True,\n) -> HasAtomsT\n

    Add a Volterra screw dislocation to the structure.

    The dislocation will pass through center, with Burgers vector b.

    The cut parameter defines the cut (discontinuity) plane used to displace the atoms. By default, cut is chosen automtically, but it may also be specified as a vector which points from the dislocation core towards the cut plane (not normal to the cut plane!)

    The screw dislocation in an isotropic medium has a particularily simple form, given by:

    \\[ \\mathbf{u} = \\frac{\\mathbf{b}}{2\\pi} \\arctan(x, y) \\]

    for a dislocation along \\(+z\\) with cut plane along \\(-x\\). To support arbitrary cut planes, \\(x\\) and \\(y\\) are replaced by the components of \\(r\\) parallel and perpendicular to the cut plane, evaluated in the plane of b.

    The dislocation is defined by the FS/RH convention: Performing one loop of positive sense relative to the tangent vector displaces the real crystal one b relative to a reference crystal.

    PARAMETER DESCRIPTION atoms

    Structure to add screw dislocation to

    TYPE: HasAtomsT

    center

    Position on dislocation line

    TYPE: VecLike

    b

    Burgers vector of dislocation (FS/RH convention)

    TYPE: VecLike

    cut

    Cut plane to create dislocation on

    TYPE: Optional[VecLike] DEFAULT: None

    sign

    Sign of screw dislocation (True means positive)

    TYPE: bool DEFAULT: True

    RETURNS DESCRIPTION HasAtomsT

    Structure with a screw dislocation added

    Source code in atomlib/defect.py
    def disloc_screw(atoms: HasAtomsT, center: VecLike, b: VecLike, cut: t.Optional[VecLike] = None,\n                 sign: bool = True) -> HasAtomsT:\n    r\"\"\"\n    Add a Volterra screw dislocation to the structure.\n\n    The dislocation will pass through `center`, with Burgers vector `b`.\n\n    The `cut` parameter defines the cut (discontinuity) plane used to displace the atoms.\n    By default, `cut` is chosen automtically, but it may also be specified as a vector\n    which points from the dislocation core towards the cut plane (not normal to the cut plane!)\n\n    The screw dislocation in an isotropic medium has a particularily simple form, given by:\n\n    $$\n       \\mathbf{u} = \\frac{\\mathbf{b}}{2\\pi} \\arctan(x, y)\n    $$\n\n    for a dislocation along $+z$ with cut plane along $-x$. To support arbitrary cut planes,\n    $x$ and $y$ are replaced by the components of $r$ parallel and perpendicular to the cut plane,\n    evaluated in the plane of `b`.\n\n    The dislocation is defined by the FS/RH convention: Performing one loop of positive sense\n    relative to the tangent vector displaces the real crystal one `b` relative to a reference\n    crystal.\n\n    Args:\n      atoms: Structure to add screw dislocation to\n      center: Position on dislocation line\n      b: Burgers vector of dislocation (FS/RH convention)\n      cut: Cut plane to create dislocation on\n      sign: Sign of screw dislocation (True means positive)\n\n    Returns:\n      Structure with a screw dislocation added\n    \"\"\"\n\n    center = to_vec3(center)\n    b_vec = to_vec3(b)\n    t = b_vec / float(numpy.linalg.norm(b_vec))\n    t = -t if not sign else t\n    if cut is None:\n        if numpy.linalg.norm(numpy.cross(t, [1., 1., 1.])) < numpy.pi/4:\n            # near 111, choose x as cut plane direction\n            cut = to_vec3([1., 0., 0.])\n        else:\n            # otherwise find plane by rotating around 111\n            cut = cast(NDArray[numpy.float64], LinearTransform3D.rotate([1., 1., 1.], 2*numpy.pi/3).transform(t))\n    else:\n        cut = to_vec3(cut)\n        cut /= norm(cut)\n        if numpy.allclose(cut, t, atol=1e-2):\n            raise ValueError(\"`t` and `cut` must be different.\")\n\n    print(f\"Cut plane direction: {cut}\")\n\n    frame = atoms.get_atoms('local')\n    pts = frame.coords() - center\n\n    # components perpendicular to t\n    cut_perp = -perp(cut, t)\n    pts_perp = perp(pts, t)\n\n    # signed angle around dislocation\n    theta = numpy.arctan2(dot(t, numpy.cross(cut_perp, pts_perp)), dot(cut_perp, pts_perp))\n    # FS/RH convention\n    disp = b_vec * (theta / (2*numpy.pi))\n\n    return atoms.with_atoms(frame.with_coords(pts + center + disp), 'local')\n
    "},{"location":"api/defect/#atomlib.defect.disloc_loop_z","title":"disloc_loop_z","text":"
    disloc_loop_z(\n    atoms: HasAtomsT,\n    center: VecLike,\n    b: VecLike,\n    loop_r: float,\n    *,\n    poisson: float = 0.25\n) -> HasAtomsT\n

    Add a circular dislocation loop to the structure, assuming an elastically isotropic material.

    The loop will have radius loop_r, be centered at center, and oriented along the z-axis.

    The dislocation's sign is defined such that travelling upwards through the loop results in a displacement of b. poisson is the material's poisson ratio, which affects the shape of dislocations with an edge component.

    Adding the loop creates (or removes) a volume of \\(\\mathbf{b} \\cdot \\mathbf{n}A\\), where \\(\\mathbf{n}A\\) is the loop's normal times its area. Consequently, this function adds or remove atoms to effect this volume change.

    PARAMETER DESCRIPTION atoms

    Structure to add dislocation loop to

    TYPE: HasAtomsT

    center

    Center of dislocation loop

    TYPE: VecLike

    b

    Burgers vector of dislocation (defined so travelling upwards leads to a plastic displacment of b).

    TYPE: VecLike

    loop_r

    Radius of dislocation loop

    TYPE: float

    poisson

    Poisson ratio of material

    TYPE: float DEFAULT: 0.25

    RETURNS DESCRIPTION HasAtomsT

    Structure with a dislocation loop added

    Source code in atomlib/defect.py
    def disloc_loop_z(atoms: HasAtomsT, center: VecLike, b: VecLike,\n                  loop_r: float, *, poisson: float = 0.25) -> HasAtomsT:\n    r\"\"\"\n    Add a circular dislocation loop to the structure, assuming an elastically isotropic material.\n\n    The loop will have radius `loop_r`, be centered at `center`, and oriented along the z-axis.\n\n    The dislocation's sign is defined such that travelling upwards through the loop results in a displacement of `b`.\n    `poisson` is the material's poisson ratio, which affects the shape of dislocations with an edge component.\n\n    Adding the loop creates (or removes) a volume of $\\mathbf{b} \\cdot \\mathbf{n}A$, where $\\mathbf{n}A$ is the loop's\n    normal times its area. Consequently, this function adds or remove atoms to effect this volume change.\n\n    Args:\n      atoms: Structure to add dislocation loop to\n      center: Center of dislocation loop\n      b: Burgers vector of dislocation (defined so travelling upwards leads to a plastic displacment of `b`).\n      loop_r: Radius of dislocation loop\n      poisson: Poisson ratio of material\n\n    Returns:\n      Structure with a dislocation loop added\n    \"\"\"\n\n    center = to_vec3(center)\n    b_vec = to_vec3(b)\n\n    atoms = atoms.transform_atoms(AffineTransform3D.translate(center).inverse())\n    frame = atoms.get_atoms('local')\n    branch = None\n\n    d = numpy.dot(b_vec, [0, 0, 1])\n    if -d > 1e-8:\n        logging.info(\"Non-conservative dislocation. Removing atoms.\")\n        frame = frame.filter(~(\n            (frame.x()**2 + frame.y()**2 < loop_r**2)\n            & frame.z().is_between(d/2., -d/2., closed='both')\n        ))\n\n    if d > 1e-8:\n        logging.info(\"Non-conservative dislocation. Duplicating atoms.\")\n        duplicate = frame.filter(\n            (frame.x()**2 + frame.y()**2 < loop_r**2)\n            & frame.z().is_between(-d/2., d/2., closed='both')\n        )\n        logging.info(f\"Adding {len(duplicate)} atoms\")\n\n        frame = Atoms.concat((frame, duplicate))\n        #atoms = atoms._replace_atoms(frame)\n        branch = numpy.sign(frame['z'].to_numpy())\n        if len(duplicate) > 0:\n            branch[-len(duplicate):] *= -1  # flip branch of duplicated atoms\n\n    pts = frame.coords()\n    disps = _loop_disp_z(pts, b_vec, loop_r, poisson=poisson, branch=branch)\n\n    return atoms.with_atoms(frame.with_coords(pts + disps + center), 'local')\n
    "},{"location":"api/defect/#atomlib.defect.disloc_square_z","title":"disloc_square_z","text":"
    disloc_square_z(\n    atoms: HasAtomsT,\n    center: VecLike,\n    b: VecLike,\n    loop_r: float,\n    *,\n    poisson: float = 0.25\n) -> HasAtomsT\n

    Add a square dislocation loop to the structure, assuming an elastically isotropic material.

    The dislocation's sign is defined such that traveling upwards through the loop results in a displacement of b. poisson is the material's poisson ratio, which affects the shape of dislocations with an edge component.

    Adding the loop creates (or removes) a volume of \\(\\mathbf{b} \\cdot \\mathbf{n}A\\), where \\(\\mathbf{n}A\\) is the loop's normal times its area. Consequently, this function adds or remove atoms to effect this volume change.

    PARAMETER DESCRIPTION atoms

    Structure to add dislocation loop to

    TYPE: HasAtomsT

    center

    Center of dislocation loop

    TYPE: VecLike

    b

    Burgers vector of dislocation (defined so travelling upwards leads to a plastic displacment of b).

    TYPE: VecLike

    loop_r

    Radius (side length/2) of dislocation loop

    TYPE: float

    poisson

    Poisson ratio of material

    TYPE: float DEFAULT: 0.25

    RETURNS DESCRIPTION HasAtomsT

    Structure with a dislocation loop added

    Source code in atomlib/defect.py
    def disloc_square_z(atoms: HasAtomsT, center: VecLike, b: VecLike,\n                    loop_r: float, *, poisson: float = 0.25) -> HasAtomsT:\n    r\"\"\"\n    Add a square dislocation loop to the structure, assuming an elastically isotropic material.\n\n    The dislocation's sign is defined such that traveling upwards through the loop results in a displacement of `b`.\n    `poisson` is the material's poisson ratio, which affects the shape of dislocations with an edge component.\n\n    Adding the loop creates (or removes) a volume of $\\mathbf{b} \\cdot \\mathbf{n}A$, where $\\mathbf{n}A$ is the loop's\n    normal times its area. Consequently, this function adds or remove atoms to effect this volume change.\n\n    Args:\n      atoms: Structure to add dislocation loop to\n      center: Center of dislocation loop\n      b: Burgers vector of dislocation (defined so travelling upwards leads to a plastic displacment of `b`).\n      loop_r: Radius (side length/2) of dislocation loop\n      poisson: Poisson ratio of material\n\n    Returns:\n      Structure with a dislocation loop added\n    \"\"\"\n    poly = loop_r * numpy.array([(1, 1), (-1, 1), (-1, -1), (1, -1)])\n    return disloc_poly_z(atoms, b, poly, center, poisson=poisson)\n
    "},{"location":"api/defect/#atomlib.defect.disloc_poly_z","title":"disloc_poly_z","text":"
    disloc_poly_z(\n    atoms: HasAtomsT,\n    b: VecLike,\n    poly: ArrayLike,\n    center: Optional[VecLike] = None,\n    *,\n    poisson: float = 0.25\n) -> HasAtomsT\n

    Add a dislocation loop defined by the polygon poly, assuming an elastically isotropic material.

    poly should be a 2d polygon (shape (N, 2)). It will be placed at center, in the plane z=center[2]. For CCW winding order, traveling upwards through the plane of the loop results in a displacement of b. poisson is the material's poisson ratio, which affects the shape of dislocations with an edge component.

    Adding the loop creates (or removes) a volume of \\(\\mathbf{b} \\cdot \\mathbf{n}A\\), where \\(\\mathbf{n}A\\) is the loop's normal times its area. Consequently, this function adds or remove atoms to effect this volume change.

    Follows the solution in Hirth, J. P. & Lothe, J. (1982). Theory of Dislocations. ISBN 0-89464-617-6

    PARAMETER DESCRIPTION atoms

    Structure to add dislocation loop to

    TYPE: HasAtomsT

    b

    Burgers vector of dislocation (defined so travelling upwards leads to a plastic displacment of b).

    TYPE: VecLike

    poly

    2D polygon defining dislocation line. It is placed in the plane z=center[2].

    TYPE: ArrayLike

    center

    Center of dislocation loop (defaults to zero, applying no displacement to the gven polygon).

    TYPE: Optional[VecLike] DEFAULT: None

    poisson

    Poisson ratio of material

    TYPE: float DEFAULT: 0.25

    RETURNS DESCRIPTION HasAtomsT

    Structure with a dislocation loop added

    Source code in atomlib/defect.py
    def disloc_poly_z(atoms: HasAtomsT, b: VecLike, poly: ArrayLike, center: t.Optional[VecLike] = None,\n                  *, poisson: float = 0.25) -> HasAtomsT:\n    r\"\"\"\n    Add a dislocation loop defined by the polygon `poly`, assuming an elastically isotropic material.\n\n    `poly` should be a 2d polygon (shape `(N, 2)`). It will be placed at `center`, in the plane `z=center[2]`.\n    For CCW winding order, traveling upwards through the plane of the loop results in a displacement of `b`.\n    `poisson` is the material's poisson ratio, which affects the shape of dislocations with an edge component.\n\n    Adding the loop creates (or removes) a volume of $\\mathbf{b} \\cdot \\mathbf{n}A$, where $\\mathbf{n}A$ is the loop's\n    normal times its area. Consequently, this function adds or remove atoms to effect this volume change.\n\n    Follows the solution in [Hirth, J. P. & Lothe, J. (1982). Theory of Dislocations. ISBN 0-89464-617-6\n    ](https://www.google.com/books/edition/Theory_of_Dislocations/TAjwAAAAMAAJ)\n\n    Args:\n      atoms: Structure to add dislocation loop to\n      b: Burgers vector of dislocation (defined so travelling upwards leads to a plastic displacment of `b`).\n      poly: 2D polygon defining dislocation line. It is placed in the plane `z=center[2]`.\n      center: Center of dislocation loop (defaults to zero, applying no displacement to the gven polygon).\n      poisson: Poisson ratio of material\n\n    Returns:\n      Structure with a dislocation loop added\n    \"\"\"\n    center = to_vec3(center if center is not None else [0., 0., 0.])\n    b_vec = to_vec3(b)\n\n    poly = numpy.atleast_2d(poly)\n    if poly.ndim != 2 or poly.shape[-1] != 2:\n        raise ValueError(f\"Expected a 2d polygon. Instead got shape {poly.shape}\")\n\n    frame = atoms.get_atoms('local')\n    coords: NDArray[numpy.float64] = frame.coords() - center\n\n    branch = None\n    d = numpy.dot(b_vec, [0, 0, 1])\n    if abs(d) > 1e-8:\n        logging.info(\"Non-conservative dislocation.\")\n        windings = polygon_winding(poly, coords[..., :2])\n\n        z = coords[..., 2]\n        remove = (z >= windings * d/2.) & (z <= -windings * d/2.)\n        duplicate = (z >= -windings * d/2.) & (z <= windings * d/2.)\n\n        n_remove = numpy.sum(remove, dtype=int)\n        if n_remove:\n            logging.info(f\"Removing {n_remove} atoms\")\n            frame = frame.filter(polars.Series(~remove))\n            duplicate = duplicate[~remove]\n\n        n_duplicate = numpy.sum(duplicate, dtype=int)\n        if n_duplicate:\n            logging.info(f\"Duplicating {n_duplicate} atoms\")\n            frame = Atoms.concat((frame, frame.filter(polars.Series(duplicate))))\n\n            branch = numpy.ones(len(frame))\n            branch[-n_duplicate:] = -1  # flip branch of duplicated atoms\n\n        coords = frame.coords() - center\n\n    disp = _poly_disp_z(coords, b_vec, poly, poisson=poisson, branch=branch)\n\n    return atoms.with_atoms(frame.with_coords(coords + disp + center), 'local')\n
    "},{"location":"api/elem/","title":"atomlib.elem","text":""},{"location":"api/elem/#atomlib.elem.ELEMENTS","title":"ELEMENTS module-attribute","text":"
    ELEMENTS = {\n    \"h\": 1,\n    \"he\": 2,\n    \"li\": 3,\n    \"be\": 4,\n    \"b\": 5,\n    \"c\": 6,\n    \"n\": 7,\n    \"o\": 8,\n    \"f\": 9,\n    \"ne\": 10,\n    \"na\": 11,\n    \"mg\": 12,\n    \"al\": 13,\n    \"si\": 14,\n    \"p\": 15,\n    \"s\": 16,\n    \"cl\": 17,\n    \"ar\": 18,\n    \"k\": 19,\n    \"ca\": 20,\n    \"sc\": 21,\n    \"ti\": 22,\n    \"v\": 23,\n    \"cr\": 24,\n    \"mn\": 25,\n    \"fe\": 26,\n    \"co\": 27,\n    \"ni\": 28,\n    \"cu\": 29,\n    \"zn\": 30,\n    \"ga\": 31,\n    \"ge\": 32,\n    \"as\": 33,\n    \"se\": 34,\n    \"br\": 35,\n    \"kr\": 36,\n    \"rb\": 37,\n    \"sr\": 38,\n    \"y\": 39,\n    \"zr\": 40,\n    \"nb\": 41,\n    \"mo\": 42,\n    \"tc\": 43,\n    \"ru\": 44,\n    \"rh\": 45,\n    \"pd\": 46,\n    \"ag\": 47,\n    \"cd\": 48,\n    \"in\": 49,\n    \"sn\": 50,\n    \"sb\": 51,\n    \"te\": 52,\n    \"i\": 53,\n    \"xe\": 54,\n    \"cs\": 55,\n    \"ba\": 56,\n    \"la\": 57,\n    \"ce\": 58,\n    \"pr\": 59,\n    \"nd\": 60,\n    \"pm\": 61,\n    \"sm\": 62,\n    \"eu\": 63,\n    \"gd\": 64,\n    \"tb\": 65,\n    \"dy\": 66,\n    \"ho\": 67,\n    \"er\": 68,\n    \"tm\": 69,\n    \"yb\": 70,\n    \"lu\": 71,\n    \"hf\": 72,\n    \"ta\": 73,\n    \"w\": 74,\n    \"re\": 75,\n    \"os\": 76,\n    \"ir\": 77,\n    \"pt\": 78,\n    \"au\": 79,\n    \"hg\": 80,\n    \"tl\": 81,\n    \"pb\": 82,\n    \"bi\": 83,\n    \"po\": 84,\n    \"at\": 85,\n    \"rn\": 86,\n    \"fr\": 87,\n    \"ra\": 88,\n    \"ac\": 89,\n    \"th\": 90,\n    \"pa\": 91,\n    \"u\": 92,\n    \"np\": 93,\n    \"pu\": 94,\n    \"am\": 95,\n    \"cm\": 96,\n    \"bk\": 97,\n    \"cf\": 98,\n    \"es\": 99,\n    \"fm\": 100,\n    \"md\": 101,\n    \"no\": 102,\n    \"lr\": 103,\n    \"rf\": 104,\n    \"db\": 105,\n    \"sg\": 106,\n    \"bh\": 107,\n    \"hs\": 108,\n    \"mt\": 109,\n    \"ds\": 110,\n    \"rg\": 111,\n    \"cn\": 112,\n    \"nh\": 113,\n    \"fl\": 114,\n    \"mc\": 115,\n    \"lv\": 116,\n    \"ts\": 117,\n    \"og\": 118,\n}\n
    "},{"location":"api/elem/#atomlib.elem.ELEMENT_SYMBOLS","title":"ELEMENT_SYMBOLS module-attribute","text":"
    ELEMENT_SYMBOLS = [\n    \"H\",\n    \"He\",\n    \"Li\",\n    \"Be\",\n    \"B\",\n    \"C\",\n    \"N\",\n    \"O\",\n    \"F\",\n    \"Ne\",\n    \"Na\",\n    \"Mg\",\n    \"Al\",\n    \"Si\",\n    \"P\",\n    \"S\",\n    \"Cl\",\n    \"Ar\",\n    \"K\",\n    \"Ca\",\n    \"Sc\",\n    \"Ti\",\n    \"V\",\n    \"Cr\",\n    \"Mn\",\n    \"Fe\",\n    \"Co\",\n    \"Ni\",\n    \"Cu\",\n    \"Zn\",\n    \"Ga\",\n    \"Ge\",\n    \"As\",\n    \"Se\",\n    \"Br\",\n    \"Kr\",\n    \"Rb\",\n    \"Sr\",\n    \"Y\",\n    \"Zr\",\n    \"Nb\",\n    \"Mo\",\n    \"Tc\",\n    \"Ru\",\n    \"Rh\",\n    \"Pd\",\n    \"Ag\",\n    \"Cd\",\n    \"In\",\n    \"Sn\",\n    \"Sb\",\n    \"Te\",\n    \"I\",\n    \"Xe\",\n    \"Cs\",\n    \"Ba\",\n    \"La\",\n    \"Ce\",\n    \"Pr\",\n    \"Nd\",\n    \"Pm\",\n    \"Sm\",\n    \"Eu\",\n    \"Gd\",\n    \"Tb\",\n    \"Dy\",\n    \"Ho\",\n    \"Er\",\n    \"Tm\",\n    \"Yb\",\n    \"Lu\",\n    \"Hf\",\n    \"Ta\",\n    \"W\",\n    \"Re\",\n    \"Os\",\n    \"Ir\",\n    \"Pt\",\n    \"Au\",\n    \"Hg\",\n    \"Tl\",\n    \"Pb\",\n    \"Bi\",\n    \"Po\",\n    \"At\",\n    \"Rn\",\n    \"Fr\",\n    \"Ra\",\n    \"Ac\",\n    \"Th\",\n    \"Pa\",\n    \"U\",\n    \"Np\",\n    \"Pu\",\n    \"Am\",\n    \"Cm\",\n    \"Bk\",\n    \"Cf\",\n    \"Es\",\n    \"Fm\",\n    \"Md\",\n    \"No\",\n    \"Lr\",\n    \"Rf\",\n    \"Db\",\n    \"Sg\",\n    \"Bh\",\n    \"Hs\",\n    \"Mt\",\n    \"Ds\",\n    \"Rg\",\n    \"Cn\",\n    \"Nh\",\n    \"Fl\",\n    \"Mc\",\n    \"Lv\",\n    \"Ts\",\n    \"Og\",\n]\n
    "},{"location":"api/elem/#atomlib.elem.DATA_PATH","title":"DATA_PATH module-attribute","text":"
    DATA_PATH = files('atomlib.data')\n
    "},{"location":"api/elem/#atomlib.elem.get_elem","title":"get_elem","text":"
    get_elem(sym: ElemLike) -> int\n
    get_elem(sym: Series) -> Series\n
    get_elem(sym: Union[int, str, Series])\n

    Get the atomic number corresponding to a given symbol.

    "},{"location":"api/elem/#atomlib.elem.get_elem--examples","title":"Examples","text":"
    >>> get_elem(\"Gd\")\n62\n>>> get_elem(polars.Series([\"Gd\", \"Ce\", \"O\"]))\nshape: (3,)\nSeries: 'elem' [i8]\n[\n    64\n    58\n    8\n]\n
    Source code in atomlib/elem.py
    def get_elem(sym: t.Union[int, str, polars.Series]):\n    \"\"\"\n    Get the atomic number corresponding to a given symbol.\n\n    # Examples\n    ```python\n    >>> get_elem(\"Gd\")\n    62\n    >>> get_elem(polars.Series([\"Gd\", \"Ce\", \"O\"]))\n    shape: (3,)\n    Series: 'elem' [i8]\n    [\n        64\n        58\n        8\n    ]\n    ```\n    \"\"\"\n\n    if isinstance(sym, int):\n        if not 0 < sym < len(ELEMENTS):\n            raise ValueError(f\"Invalid atomic number {sym}\")\n        return sym\n\n    if isinstance(sym, polars.Series):\n        # TODO: this is a mess\n        elem = sym.cast(polars.Utf8).str.extract(_SYM_RE, 0).str.to_lowercase() \\\n            .replace_strict(\n                old=list(ELEMENTS.keys()), new=list(ELEMENTS.values()),\n                default=None, return_dtype=polars.Int8\n            ).alias('elem')\n\n        if (invalid := sym.filter(sym.is_not_null() & elem.is_null()).to_list()):\n            raise ValueError(f\"Invalid element symbol(s) '{', '.join(map(str, invalid))}'\")\n\n        return elem\n\n    sym_s = re.search(_SYM_RE, str(sym))\n    try:\n        return ELEMENTS[sym_s[0].lower()]  # type: ignore\n    except (KeyError, IndexError):\n        raise ValueError(f\"Invalid element symbol '{sym}'\")\n
    "},{"location":"api/elem/#atomlib.elem.get_elems","title":"get_elems","text":"
    get_elems(sym: ElemsLike) -> List[Tuple[int, float]]\n

    Get the elements and quantities corresponding to a formula unit.

    "},{"location":"api/elem/#atomlib.elem.get_elems--examples","title":"Examples","text":"
    >>> get_elems(\"AlN\")\n[(13, 1.0), (7, 1.0)]\n>>> get_elems(\"Al0.93Sc0.07N\")\n[(13, 0.93), (21, 0.07), (7, 1.0)]\n
    Source code in atomlib/elem.py
    def get_elems(sym: ElemsLike) -> t.List[t.Tuple[int, float]]:\n    \"\"\"\n    Get the elements and quantities corresponding to a formula unit.\n\n    # Examples\n    ```python\n    >>> get_elems(\"AlN\")\n    [(13, 1.0), (7, 1.0)]\n    >>> get_elems(\"Al0.93Sc0.07N\")\n    [(13, 0.93), (21, 0.07), (7, 1.0)]\n    ```\n    \"\"\"\n\n    if not isinstance(sym, str):\n        if isinstance(sym, int):\n            return [(sym, 1.0)]\n        return [\n            (get_elem(v[0]), float(v[1]))  # type: ignore\n                if (hasattr(v, '__len__') and not isinstance(v, str))\n                else (get_elem(v), 1.)  # type: ignore\n            for v in sym\n        ]\n\n    if len(sym) > 0:\n        sym = sym[0].upper() + sym[1:]\n    segments = [\n        (match[1], match[2]) for match in re.finditer(r'([A-Z][a-z]*)([0-9\\.]*[+-]?)', str(sym))\n    ]\n    if len(segments) == 0:\n        raise ValueError(f\"Invalid compound '{sym}'\")\n\n    elems = [ELEMENTS.get(seg[0].lower()) for seg in segments]\n\n    out = []\n    for ((elem_sym, num), elem) in zip(segments, elems):\n        if elem is None:\n            raise ValueError(f\"Unknown element '{elem_sym}' in '{sym}'. Compounds are case-sensitive.\")\n\n        try:\n            num = float(num) if len(num) and num[-1] not in ('+', '-') else 1.\n        except ValueError:\n            raise ValueError(f\"Unknown occupancy '{num}' for elem '{elem_sym}' in compound '{sym}'\")\n\n        out.append((elem, num))\n\n    return out\n
    "},{"location":"api/elem/#atomlib.elem.get_sym","title":"get_sym","text":"
    get_sym(elem: int) -> str\n
    get_sym(elem: Series) -> Series\n
    get_sym(elem: Union[int, Series])\n

    Get the symbol corresponding to an atomic number.

    "},{"location":"api/elem/#atomlib.elem.get_sym--examples","title":"Examples","text":"
    >>> get_sym(5)\n\"B\"\n
    Source code in atomlib/elem.py
    def get_sym(elem: t.Union[int, polars.Series]):\n    \"\"\"\n    Get the symbol corresponding to an atomic number.\n\n    # Examples\n    ```python\n    >>> get_sym(5)\n    \"B\"\n    ```\n    \"\"\"\n    if isinstance(elem, polars.Series):\n        sym = elem.cast(polars.Int64).replace_strict(\n            list(range(1, len(ELEMENT_SYMBOLS)+1)),\n            ELEMENT_SYMBOLS,\n            default=None,\n            return_dtype=polars.Utf8,\n        ).alias('symbol')\n\n        if (invalid := elem.filter(elem.is_not_null() & sym.is_null()).unique().to_list()):\n            raise ValueError(f\"Invalid atomic number(s) {', '.join(map(str, invalid))}\")\n\n        return sym\n\n    return _get_sym(elem)\n
    "},{"location":"api/elem/#atomlib.elem.get_mass","title":"get_mass","text":"
    get_mass(elem: int) -> float\n
    get_mass(elem: Series) -> Series\n
    get_mass(elem: Union[ndarray, Sequence[int]]) -> ndarray\n
    get_mass(elem: Union[int, Sequence[int], ndarray, Series])\n

    Get the standard atomic mass for the given element. Follows the 2021 IUPAC definitions [1].

    [1] 2021 table of the IUPAC Commission on Isotopic Abundances and Atomic Weights https://doi.org/10.1515/pac-2019-0603

    Source code in atomlib/elem.py
    def get_mass(elem: t.Union[int, t.Sequence[int], numpy.ndarray, polars.Series]):\n    \"\"\"\n    Get the standard atomic mass for the given element.\n    Follows the 2021 IUPAC definitions [1].\n\n    [1] 2021 table of the IUPAC Commission on Isotopic Abundances and Atomic Weights <https://doi.org/10.1515/pac-2019-0603>\n    \"\"\"\n    global _ELEMENT_MASSES\n\n    if _ELEMENT_MASSES is None:\n        with _open_binary_data('masses.npy') as f:\n            _ELEMENT_MASSES = numpy.load(f, allow_pickle=False)\n\n    if isinstance(elem, polars.Series):\n        return polars.Series(values=_ELEMENT_MASSES)[elem-1]\n\n    if isinstance(elem, (int, numpy.ndarray)):\n        return _ELEMENT_MASSES[elem-1]  # type: ignore\n    return _ELEMENT_MASSES[[e-1 for e in elem]]  # type: ignore\n
    "},{"location":"api/elem/#atomlib.elem.get_ionic_radius","title":"get_ionic_radius","text":"
    get_ionic_radius(elem: int, charge: int) -> float\n

    Get crystal ionic radius in angstroms for elem in charge state charge. Follows the values in [2].

    [2] R.D. Shannon, Acta Cryst. A32 (1976) https://doi.org/10.1107/S0567739476001551

    Source code in atomlib/elem.py
    def get_ionic_radius(elem: int, charge: int) -> float:\n    \"\"\"\n    Get crystal ionic radius in angstroms for `elem` in charge state `charge`.\n    Follows the values in [2].\n\n    [2] R.D. Shannon, Acta Cryst. A32 (1976) <https://doi.org/10.1107/S0567739476001551>\n    \"\"\"\n    global _ION_RADII\n\n    import json\n\n    if _ION_RADII is None:\n        with _open_text_data('ion_radii.json') as f:\n            _ION_RADII = json.load(f)\n        assert _ION_RADII is not None\n\n    s = f\"{get_sym(elem)}{charge:+d}\"\n\n    try:\n        return _ION_RADII[s]\n    except KeyError:\n        raise ValueError(f\"Unknown radius for ion '{s}'\") from None\n
    "},{"location":"api/elem/#atomlib.elem.get_radius","title":"get_radius","text":"
    get_radius(elem: int) -> float\n
    get_radius(elem: Series) -> Series\n
    get_radius(elem: Union[ndarray, Sequence[int]]) -> ndarray\n
    get_radius(\n    elem: Union[int, Sequence[int], ndarray, Series]\n)\n

    Get the neutral atomic radius for the given element(s), in angstroms. Follows the values in [3].

    [3] E. Clementi et. al, J. Chem. Phys. 47 (1967) https://doi.org/10.1063/1.1712084

    Source code in atomlib/elem.py
    def get_radius(elem: t.Union[int, t.Sequence[int], numpy.ndarray, polars.Series]):\n    \"\"\"\n    Get the neutral atomic radius for the given element(s), in angstroms.\n    Follows the values in [3].\n\n    [3] E. Clementi et. al, J. Chem. Phys. 47 (1967) <https://doi.org/10.1063/1.1712084>\n    \"\"\"\n    global _ELEMENT_RADII\n\n    if _ELEMENT_RADII is None:\n        with _open_binary_data('radii.npy') as f:\n            _ELEMENT_RADII = numpy.load(f, allow_pickle=False)\n\n    if isinstance(elem, polars.Series):\n        return polars.Series(values=_ELEMENT_RADII)[elem-1]\n\n    if isinstance(elem, (int, numpy.ndarray)):\n        return _ELEMENT_RADII[elem-1]  # type: ignore\n    return _ELEMENT_RADII[[e-1 for e in elem]]  # type: ignore\n
    "},{"location":"api/expr/","title":"atomlib.expr","text":""},{"location":"api/expr/#atomlib.expr.V2","title":"V2 module-attribute","text":"
    V2 = TypeVar('V2')\n
    "},{"location":"api/expr/#atomlib.expr.WSPACE_RE","title":"WSPACE_RE module-attribute","text":"
    WSPACE_RE = compile('\\\\s+')\n
    "},{"location":"api/expr/#atomlib.expr.NUMERIC_OPS","title":"NUMERIC_OPS module-attribute","text":"
    NUMERIC_OPS: Sequence[Op[SupportsNum]] = [\n    BinaryOrUnaryOp([\"-\"], sub, False, 5),\n    BinaryOp([\"+\"], add, 5),\n    BinaryOp([\"*\"], mul, 6),\n    BinaryOp([\"/\"], truediv, 6),\n    BinaryOp([\"//\"], floordiv, 6),\n    BinaryOp([\"^\", \"**\"], pow, 7),\n]\n
    "},{"location":"api/expr/#atomlib.expr.NUMERIC_PARSER","title":"NUMERIC_PARSER module-attribute","text":"
    NUMERIC_PARSER = Parser(NUMERIC_OPS, parse_numeric)\n
    "},{"location":"api/expr/#atomlib.expr.BOOLEAN_OPS","title":"BOOLEAN_OPS module-attribute","text":"
    BOOLEAN_OPS: Sequence[Op[SupportsBool]] = [\n    BinaryOp([\"=\", \"==\"], eq, 3),\n    BinaryOp([\"!=\", \"<>\"], ne, 3),\n    BinaryOp([\"|\", \"||\"], or_, 4),\n    BinaryOp([\"&\", \"&&\"], and_, 5),\n    BinaryOp([\"^\"], xor, 6),\n    UnaryOp([\"!\", \"~\"], invert),\n]\n
    "},{"location":"api/expr/#atomlib.expr.BOOLEAN_PARSER","title":"BOOLEAN_PARSER module-attribute","text":"
    BOOLEAN_PARSER = Parser(BOOLEAN_OPS, parse_boolean)\n

    Parser for boolean expressions ([1 || false && true])

    "},{"location":"api/expr/#atomlib.expr.VECTOR_OPS","title":"VECTOR_OPS module-attribute","text":"
    VECTOR_OPS: Sequence[Op[ndarray]] = [\n    *cast(Op[ndarray], op)\n    for op in NUMERIC_OPS,\n    NaryOp([\",\"], call=stack, precedence=3),\n]\n
    "},{"location":"api/expr/#atomlib.expr.VECTOR_PARSER","title":"VECTOR_PARSER module-attribute","text":"
    VECTOR_PARSER = Parser(\n    VECTOR_OPS, lambda v: array(parse_numeric(v))\n)\n
    "},{"location":"api/expr/#atomlib.expr.VariadicCallable","title":"VariadicCallable","text":"

    Bases: Protocol, Generic[V]

    Source code in atomlib/expr.py
    class VariadicCallable(t.Protocol, t.Generic[V]):\n    def __call__(self, *args: V) -> V:\n        ...\n
    "},{"location":"api/expr/#atomlib.expr.Op","title":"Op dataclass","text":"

    Bases: ABC, Generic[V]

    Source code in atomlib/expr.py
    @dataclass\nclass Op(ABC, t.Generic[V]):\n    aliases: t.List[str]\n    call: t.Callable = field(repr=False)\n\n    @property\n    @abstractmethod\n    def requires_whitespace(self) -> bool:\n        ...\n
    "},{"location":"api/expr/#atomlib.expr.Op.aliases","title":"aliases instance-attribute","text":"
    aliases: List[str]\n
    "},{"location":"api/expr/#atomlib.expr.Op.call","title":"call class-attribute instance-attribute","text":"
    call: Callable = field(repr=False)\n
    "},{"location":"api/expr/#atomlib.expr.Op.requires_whitespace","title":"requires_whitespace abstractmethod property","text":"
    requires_whitespace: bool\n
    "},{"location":"api/expr/#atomlib.expr.NaryOp","title":"NaryOp dataclass","text":"

    Bases: Op[V]

    Source code in atomlib/expr.py
    @dataclass\nclass NaryOp(Op[V]):\n    call: VariadicCallable[V] = field(repr=False)  # type: ignore\n    precedence: int = 5\n    requires_whitespace: bool = False  # type: ignore\n\n    def __call__(self, *args: V) -> V:\n        return self.call(*args)\n\n    def precedes(self, other: t.Union[BinaryOp, NaryOp, int]) -> bool:\n        if isinstance(other, (BinaryOp, NaryOp)):\n            other = other.precedence\n\n        # default to lower precedence for Nary\n        return self.precedence > other\n
    "},{"location":"api/expr/#atomlib.expr.NaryOp.aliases","title":"aliases instance-attribute","text":"
    aliases: List[str]\n
    "},{"location":"api/expr/#atomlib.expr.NaryOp.call","title":"call class-attribute instance-attribute","text":"
    call: VariadicCallable[V] = field(repr=False)\n
    "},{"location":"api/expr/#atomlib.expr.NaryOp.precedence","title":"precedence class-attribute instance-attribute","text":"
    precedence: int = 5\n
    "},{"location":"api/expr/#atomlib.expr.NaryOp.requires_whitespace","title":"requires_whitespace class-attribute instance-attribute","text":"
    requires_whitespace: bool = False\n
    "},{"location":"api/expr/#atomlib.expr.NaryOp.precedes","title":"precedes","text":"
    precedes(other: Union[BinaryOp, NaryOp, int]) -> bool\n
    Source code in atomlib/expr.py
    def precedes(self, other: t.Union[BinaryOp, NaryOp, int]) -> bool:\n    if isinstance(other, (BinaryOp, NaryOp)):\n        other = other.precedence\n\n    # default to lower precedence for Nary\n    return self.precedence > other\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOp","title":"BinaryOp dataclass","text":"

    Bases: Op[V]

    Source code in atomlib/expr.py
    @dataclass\nclass BinaryOp(Op[V]):\n    call: t.Callable[[V, V], V] = field(repr=False)  # type: ignore\n    precedence: int = 5\n    right_assoc: bool = False\n    requires_whitespace: bool = False  # type: ignore\n\n    def __call__(self, lhs: V, rhs: V) -> V:\n        return self.call(lhs, rhs)\n\n    def precedes(self, other: t.Union[BinaryOp, NaryOp, int]) -> bool:\n        \"\"\"Returns true if self is higher precedence than other.\n        Handles a tie using the associativity of self.\n        \"\"\"\n        if isinstance(other, (BinaryOp, NaryOp)):\n            other = other.precedence\n\n        if self.precedence == other:\n            return self.right_assoc\n        return self.precedence > other\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOp.aliases","title":"aliases instance-attribute","text":"
    aliases: List[str]\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOp.call","title":"call class-attribute instance-attribute","text":"
    call: Callable[[V, V], V] = field(repr=False)\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOp.precedence","title":"precedence class-attribute instance-attribute","text":"
    precedence: int = 5\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOp.right_assoc","title":"right_assoc class-attribute instance-attribute","text":"
    right_assoc: bool = False\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOp.requires_whitespace","title":"requires_whitespace class-attribute instance-attribute","text":"
    requires_whitespace: bool = False\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOp.precedes","title":"precedes","text":"
    precedes(other: Union[BinaryOp, NaryOp, int]) -> bool\n

    Returns true if self is higher precedence than other. Handles a tie using the associativity of self.

    Source code in atomlib/expr.py
    def precedes(self, other: t.Union[BinaryOp, NaryOp, int]) -> bool:\n    \"\"\"Returns true if self is higher precedence than other.\n    Handles a tie using the associativity of self.\n    \"\"\"\n    if isinstance(other, (BinaryOp, NaryOp)):\n        other = other.precedence\n\n    if self.precedence == other:\n        return self.right_assoc\n    return self.precedence > other\n
    "},{"location":"api/expr/#atomlib.expr.UnaryOp","title":"UnaryOp dataclass","text":"

    Bases: Op[V]

    Source code in atomlib/expr.py
    @dataclass\nclass UnaryOp(Op[V]):\n    call: t.Callable[[V], V] = field(repr=False)  # type: ignore\n    requires_whitespace: bool = False  # type: ignore\n\n    def __call__(self, inner: V) -> V:\n        return self.call(inner)\n
    "},{"location":"api/expr/#atomlib.expr.UnaryOp.aliases","title":"aliases instance-attribute","text":"
    aliases: List[str]\n
    "},{"location":"api/expr/#atomlib.expr.UnaryOp.call","title":"call class-attribute instance-attribute","text":"
    call: Callable[[V], V] = field(repr=False)\n
    "},{"location":"api/expr/#atomlib.expr.UnaryOp.requires_whitespace","title":"requires_whitespace class-attribute instance-attribute","text":"
    requires_whitespace: bool = False\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOrUnaryOp","title":"BinaryOrUnaryOp dataclass","text":"

    Bases: BinaryOp[V], UnaryOp[V]

    Source code in atomlib/expr.py
    @dataclass\nclass BinaryOrUnaryOp(BinaryOp[V], UnaryOp[V]):\n    call: t.Callable[[V, t.Optional[V]], V] = field(repr=False)  # type: ignore\n\n    def __call__(self, lhs: V, rhs: t.Optional[V] = None) -> V:\n        return self.call(lhs, rhs)\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOrUnaryOp.aliases","title":"aliases instance-attribute","text":"
    aliases: List[str]\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOrUnaryOp.requires_whitespace","title":"requires_whitespace class-attribute instance-attribute","text":"
    requires_whitespace: bool = False\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOrUnaryOp.precedence","title":"precedence class-attribute instance-attribute","text":"
    precedence: int = 5\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOrUnaryOp.right_assoc","title":"right_assoc class-attribute instance-attribute","text":"
    right_assoc: bool = False\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOrUnaryOp.call","title":"call class-attribute instance-attribute","text":"
    call: Callable[[V, Optional[V]], V] = field(repr=False)\n
    "},{"location":"api/expr/#atomlib.expr.BinaryOrUnaryOp.precedes","title":"precedes","text":"
    precedes(other: Union[BinaryOp, NaryOp, int]) -> bool\n

    Returns true if self is higher precedence than other. Handles a tie using the associativity of self.

    Source code in atomlib/expr.py
    def precedes(self, other: t.Union[BinaryOp, NaryOp, int]) -> bool:\n    \"\"\"Returns true if self is higher precedence than other.\n    Handles a tie using the associativity of self.\n    \"\"\"\n    if isinstance(other, (BinaryOp, NaryOp)):\n        other = other.precedence\n\n    if self.precedence == other:\n        return self.right_assoc\n    return self.precedence > other\n
    "},{"location":"api/expr/#atomlib.expr.Token","title":"Token dataclass","text":"

    Bases: ABC, Generic[T_co, V]

    Source code in atomlib/expr.py
    @dataclass\nclass Token(ABC, t.Generic[T_co, V]):\n    raw: str\n    line: int\n    span: t.Tuple[int, int]\n\n    def __str__(self):\n        return self.raw\n
    "},{"location":"api/expr/#atomlib.expr.Token.raw","title":"raw instance-attribute","text":"
    raw: str\n
    "},{"location":"api/expr/#atomlib.expr.Token.line","title":"line instance-attribute","text":"
    line: int\n
    "},{"location":"api/expr/#atomlib.expr.Token.span","title":"span instance-attribute","text":"
    span: Tuple[int, int]\n
    "},{"location":"api/expr/#atomlib.expr.OpToken","title":"OpToken dataclass","text":"

    Bases: Token[T_co, V]

    Source code in atomlib/expr.py
    @dataclass\nclass OpToken(Token[T_co, V]):\n    op: Op[V]\n
    "},{"location":"api/expr/#atomlib.expr.OpToken.raw","title":"raw instance-attribute","text":"
    raw: str\n
    "},{"location":"api/expr/#atomlib.expr.OpToken.line","title":"line instance-attribute","text":"
    line: int\n
    "},{"location":"api/expr/#atomlib.expr.OpToken.span","title":"span instance-attribute","text":"
    span: Tuple[int, int]\n
    "},{"location":"api/expr/#atomlib.expr.OpToken.op","title":"op instance-attribute","text":"
    op: Op[V]\n
    "},{"location":"api/expr/#atomlib.expr.GroupOpenToken","title":"GroupOpenToken dataclass","text":"

    Bases: Token

    Source code in atomlib/expr.py
    @dataclass\nclass GroupOpenToken(Token):\n    ...\n
    "},{"location":"api/expr/#atomlib.expr.GroupOpenToken.raw","title":"raw instance-attribute","text":"
    raw: str\n
    "},{"location":"api/expr/#atomlib.expr.GroupOpenToken.line","title":"line instance-attribute","text":"
    line: int\n
    "},{"location":"api/expr/#atomlib.expr.GroupOpenToken.span","title":"span instance-attribute","text":"
    span: Tuple[int, int]\n
    "},{"location":"api/expr/#atomlib.expr.GroupCloseToken","title":"GroupCloseToken dataclass","text":"

    Bases: Token

    Source code in atomlib/expr.py
    @dataclass\nclass GroupCloseToken(Token):\n    ...\n
    "},{"location":"api/expr/#atomlib.expr.GroupCloseToken.raw","title":"raw instance-attribute","text":"
    raw: str\n
    "},{"location":"api/expr/#atomlib.expr.GroupCloseToken.line","title":"line instance-attribute","text":"
    line: int\n
    "},{"location":"api/expr/#atomlib.expr.GroupCloseToken.span","title":"span instance-attribute","text":"
    span: Tuple[int, int]\n
    "},{"location":"api/expr/#atomlib.expr.WhitespaceToken","title":"WhitespaceToken dataclass","text":"

    Bases: Token

    Source code in atomlib/expr.py
    class WhitespaceToken(Token):\n    ...\n
    "},{"location":"api/expr/#atomlib.expr.WhitespaceToken.raw","title":"raw instance-attribute","text":"
    raw: str\n
    "},{"location":"api/expr/#atomlib.expr.WhitespaceToken.line","title":"line instance-attribute","text":"
    line: int\n
    "},{"location":"api/expr/#atomlib.expr.WhitespaceToken.span","title":"span instance-attribute","text":"
    span: Tuple[int, int]\n
    "},{"location":"api/expr/#atomlib.expr.ValueToken","title":"ValueToken dataclass","text":"

    Bases: Token[T_co, V]

    Source code in atomlib/expr.py
    @dataclass\nclass ValueToken(Token[T_co, V]):\n    val: T_co\n
    "},{"location":"api/expr/#atomlib.expr.ValueToken.raw","title":"raw instance-attribute","text":"
    raw: str\n
    "},{"location":"api/expr/#atomlib.expr.ValueToken.line","title":"line instance-attribute","text":"
    line: int\n
    "},{"location":"api/expr/#atomlib.expr.ValueToken.span","title":"span instance-attribute","text":"
    span: Tuple[int, int]\n
    "},{"location":"api/expr/#atomlib.expr.ValueToken.val","title":"val instance-attribute","text":"
    val: T_co\n
    "},{"location":"api/expr/#atomlib.expr.Expr","title":"Expr","text":"

    Bases: ABC, Generic[T_co, V]

    Source code in atomlib/expr.py
    class Expr(ABC, t.Generic[T_co, V]):\n    @abstractmethod\n    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n        ...\n\n    @abstractmethod\n    def format(self,\n               format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n               format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n        ...\n\n    def __str__(self) -> str:\n        return self.format()\n\n    def __repr__(self) -> str:\n        ...\n
    "},{"location":"api/expr/#atomlib.expr.Expr.eval","title":"eval abstractmethod","text":"
    eval(map_f: Callable[[T_co], V]) -> V\n
    Source code in atomlib/expr.py
    @abstractmethod\ndef eval(self, map_f: t.Callable[[T_co], V]) -> V:\n    ...\n
    "},{"location":"api/expr/#atomlib.expr.Expr.format","title":"format abstractmethod","text":"
    format(\n    format_scalar: Callable[\n        [ValueToken[T_co, V]], str\n    ] = str,\n    format_op: Callable[[OpToken[T_co, V]], str] = str,\n) -> str\n
    Source code in atomlib/expr.py
    @abstractmethod\ndef format(self,\n           format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n           format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n    ...\n
    "},{"location":"api/expr/#atomlib.expr.UnaryExpr","title":"UnaryExpr dataclass","text":"

    Bases: Expr[T_co, V]

    Source code in atomlib/expr.py
    @dataclass\nclass UnaryExpr(Expr[T_co, V]):\n    op_token: OpToken[T_co, V]\n    op: UnaryOp[V] = field(init=False)\n    inner: Expr[T_co, V]\n    lspace: str = \"\"\n\n    def __post_init__(self):\n        if not isinstance(self.op_token.op, UnaryOp):\n            raise TypeError()\n        self.op = self.op_token.op\n\n    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n        return self.op.call(self.inner.eval(map_f))\n\n    def format(self,\n               format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n               format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n        return f\"{self.lspace}{format_op(self.op_token)}{self.inner.format(format_scalar, format_op)}\"\n
    "},{"location":"api/expr/#atomlib.expr.UnaryExpr.op_token","title":"op_token instance-attribute","text":"
    op_token: OpToken[T_co, V]\n
    "},{"location":"api/expr/#atomlib.expr.UnaryExpr.op","title":"op class-attribute instance-attribute","text":"
    op: UnaryOp[V] = field(init=False)\n
    "},{"location":"api/expr/#atomlib.expr.UnaryExpr.inner","title":"inner instance-attribute","text":"
    inner: Expr[T_co, V]\n
    "},{"location":"api/expr/#atomlib.expr.UnaryExpr.lspace","title":"lspace class-attribute instance-attribute","text":"
    lspace: str = ''\n
    "},{"location":"api/expr/#atomlib.expr.UnaryExpr.eval","title":"eval","text":"
    eval(map_f: Callable[[T_co], V]) -> V\n
    Source code in atomlib/expr.py
    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n    return self.op.call(self.inner.eval(map_f))\n
    "},{"location":"api/expr/#atomlib.expr.UnaryExpr.format","title":"format","text":"
    format(\n    format_scalar: Callable[\n        [ValueToken[T_co, V]], str\n    ] = str,\n    format_op: Callable[[OpToken[T_co, V]], str] = str,\n) -> str\n
    Source code in atomlib/expr.py
    def format(self,\n           format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n           format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n    return f\"{self.lspace}{format_op(self.op_token)}{self.inner.format(format_scalar, format_op)}\"\n
    "},{"location":"api/expr/#atomlib.expr.BinaryExpr","title":"BinaryExpr dataclass","text":"

    Bases: Expr[T_co, V]

    Source code in atomlib/expr.py
    @dataclass\nclass BinaryExpr(Expr[T_co, V]):\n    op_token: OpToken[T_co, V]\n    op: BinaryOp[V] = field(init=False)\n    lhs: Expr[T_co, V]\n    rhs: Expr[T_co, V]\n\n    def __post_init__(self):\n        if not isinstance(self.op_token.op, BinaryOp):\n            raise TypeError()\n        self.op = self.op_token.op\n\n    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n        return self.op.call(self.lhs.eval(map_f), self.rhs.eval(map_f))\n\n    def format(self,\n               format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n               format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n        return f\"{self.lhs.format(format_scalar, format_op)}{format_op(self.op_token)}{self.rhs.format(format_scalar, format_op)}\"\n
    "},{"location":"api/expr/#atomlib.expr.BinaryExpr.op_token","title":"op_token instance-attribute","text":"
    op_token: OpToken[T_co, V]\n
    "},{"location":"api/expr/#atomlib.expr.BinaryExpr.op","title":"op class-attribute instance-attribute","text":"
    op: BinaryOp[V] = field(init=False)\n
    "},{"location":"api/expr/#atomlib.expr.BinaryExpr.lhs","title":"lhs instance-attribute","text":"
    lhs: Expr[T_co, V]\n
    "},{"location":"api/expr/#atomlib.expr.BinaryExpr.rhs","title":"rhs instance-attribute","text":"
    rhs: Expr[T_co, V]\n
    "},{"location":"api/expr/#atomlib.expr.BinaryExpr.eval","title":"eval","text":"
    eval(map_f: Callable[[T_co], V]) -> V\n
    Source code in atomlib/expr.py
    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n    return self.op.call(self.lhs.eval(map_f), self.rhs.eval(map_f))\n
    "},{"location":"api/expr/#atomlib.expr.BinaryExpr.format","title":"format","text":"
    format(\n    format_scalar: Callable[\n        [ValueToken[T_co, V]], str\n    ] = str,\n    format_op: Callable[[OpToken[T_co, V]], str] = str,\n) -> str\n
    Source code in atomlib/expr.py
    def format(self,\n           format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n           format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n    return f\"{self.lhs.format(format_scalar, format_op)}{format_op(self.op_token)}{self.rhs.format(format_scalar, format_op)}\"\n
    "},{"location":"api/expr/#atomlib.expr.NaryExpr","title":"NaryExpr dataclass","text":"

    Bases: Expr[T_co, V]

    Source code in atomlib/expr.py
    @dataclass\nclass NaryExpr(Expr[T_co, V]):\n    op_tokens: t.Sequence[OpToken[T_co, V]]\n    op: NaryOp[V] = field(init=False)\n    args: t.Sequence[Expr[T_co, V]]\n    rspace: str = \"\"\n\n    def __post_init__(self):\n        op = next(token.op for token in self.op_tokens)\n        if not all(token.op == op for token in self.op_tokens[1:]):\n            raise ValueError(\"All `op`s must be identical inside a NaryExpr\")\n        if not isinstance(op, NaryOp):\n            raise TypeError()\n        self.op = op\n\n        assert len(self.op_tokens) == len(self.args) - 1\n\n    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n        return self.op.call(*map(lambda expr: expr.eval(map_f), self.args))\n\n    def format(self,\n               format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n               format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n        return \"\".join(interleave(\n            map(lambda expr: expr.format(format_scalar, format_op), self.args),\n            map(format_op, self.op_tokens)\n        )) + self.rspace\n
    "},{"location":"api/expr/#atomlib.expr.NaryExpr.op_tokens","title":"op_tokens instance-attribute","text":"
    op_tokens: Sequence[OpToken[T_co, V]]\n
    "},{"location":"api/expr/#atomlib.expr.NaryExpr.op","title":"op class-attribute instance-attribute","text":"
    op: NaryOp[V] = field(init=False)\n
    "},{"location":"api/expr/#atomlib.expr.NaryExpr.args","title":"args instance-attribute","text":"
    args: Sequence[Expr[T_co, V]]\n
    "},{"location":"api/expr/#atomlib.expr.NaryExpr.rspace","title":"rspace class-attribute instance-attribute","text":"
    rspace: str = ''\n
    "},{"location":"api/expr/#atomlib.expr.NaryExpr.eval","title":"eval","text":"
    eval(map_f: Callable[[T_co], V]) -> V\n
    Source code in atomlib/expr.py
    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n    return self.op.call(*map(lambda expr: expr.eval(map_f), self.args))\n
    "},{"location":"api/expr/#atomlib.expr.NaryExpr.format","title":"format","text":"
    format(\n    format_scalar: Callable[\n        [ValueToken[T_co, V]], str\n    ] = str,\n    format_op: Callable[[OpToken[T_co, V]], str] = str,\n) -> str\n
    Source code in atomlib/expr.py
    def format(self,\n           format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n           format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n    return \"\".join(interleave(\n        map(lambda expr: expr.format(format_scalar, format_op), self.args),\n        map(format_op, self.op_tokens)\n    )) + self.rspace\n
    "},{"location":"api/expr/#atomlib.expr.GroupExpr","title":"GroupExpr dataclass","text":"

    Bases: Expr[T_co, V]

    Source code in atomlib/expr.py
    @dataclass\nclass GroupExpr(Expr[T_co, V]):\n    open: GroupOpenToken\n    inner: Expr[T_co, V]\n    close: GroupCloseToken\n    lspace: str = \"\"\n    rspace: str = \"\"\n\n    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n        return self.inner.eval(map_f)\n\n    def format(self,\n               format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n               format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n        return f\"{self.lspace}{self.open}{self.inner.format(format_scalar, format_op)}{self.close}{self.rspace}\"\n
    "},{"location":"api/expr/#atomlib.expr.GroupExpr.open","title":"open instance-attribute","text":"
    open: GroupOpenToken\n
    "},{"location":"api/expr/#atomlib.expr.GroupExpr.inner","title":"inner instance-attribute","text":"
    inner: Expr[T_co, V]\n
    "},{"location":"api/expr/#atomlib.expr.GroupExpr.close","title":"close instance-attribute","text":"
    close: GroupCloseToken\n
    "},{"location":"api/expr/#atomlib.expr.GroupExpr.lspace","title":"lspace class-attribute instance-attribute","text":"
    lspace: str = ''\n
    "},{"location":"api/expr/#atomlib.expr.GroupExpr.rspace","title":"rspace class-attribute instance-attribute","text":"
    rspace: str = ''\n
    "},{"location":"api/expr/#atomlib.expr.GroupExpr.eval","title":"eval","text":"
    eval(map_f: Callable[[T_co], V]) -> V\n
    Source code in atomlib/expr.py
    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n    return self.inner.eval(map_f)\n
    "},{"location":"api/expr/#atomlib.expr.GroupExpr.format","title":"format","text":"
    format(\n    format_scalar: Callable[\n        [ValueToken[T_co, V]], str\n    ] = str,\n    format_op: Callable[[OpToken[T_co, V]], str] = str,\n) -> str\n
    Source code in atomlib/expr.py
    def format(self,\n           format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n           format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n    return f\"{self.lspace}{self.open}{self.inner.format(format_scalar, format_op)}{self.close}{self.rspace}\"\n
    "},{"location":"api/expr/#atomlib.expr.ValueExpr","title":"ValueExpr dataclass","text":"

    Bases: Expr[T_co, V]

    Source code in atomlib/expr.py
    @dataclass\nclass ValueExpr(Expr[T_co, V]):\n    token: ValueToken[T_co, V]\n    lspace: str = \"\"\n    rspace: str = \"\"\n\n    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n        return map_f(self.token.val)\n\n    def format(self,\n               format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n               format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n        return f\"{self.lspace}{format_scalar(self.token)}{self.rspace}\"\n
    "},{"location":"api/expr/#atomlib.expr.ValueExpr.token","title":"token instance-attribute","text":"
    token: ValueToken[T_co, V]\n
    "},{"location":"api/expr/#atomlib.expr.ValueExpr.lspace","title":"lspace class-attribute instance-attribute","text":"
    lspace: str = ''\n
    "},{"location":"api/expr/#atomlib.expr.ValueExpr.rspace","title":"rspace class-attribute instance-attribute","text":"
    rspace: str = ''\n
    "},{"location":"api/expr/#atomlib.expr.ValueExpr.eval","title":"eval","text":"
    eval(map_f: Callable[[T_co], V]) -> V\n
    Source code in atomlib/expr.py
    def eval(self, map_f: t.Callable[[T_co], V]) -> V:\n    return map_f(self.token.val)\n
    "},{"location":"api/expr/#atomlib.expr.ValueExpr.format","title":"format","text":"
    format(\n    format_scalar: Callable[\n        [ValueToken[T_co, V]], str\n    ] = str,\n    format_op: Callable[[OpToken[T_co, V]], str] = str,\n) -> str\n
    Source code in atomlib/expr.py
    def format(self,\n           format_scalar: t.Callable[[ValueToken[T_co, V]], str] = str,\n           format_op: t.Callable[[OpToken[T_co, V]], str] = str) -> str:\n    return f\"{self.lspace}{format_scalar(self.token)}{self.rspace}\"\n
    "},{"location":"api/expr/#atomlib.expr.Parser","title":"Parser dataclass","text":"

    Bases: Generic[T_co, V]

    Source code in atomlib/expr.py
    @dataclass(init=False)\nclass Parser(t.Generic[T_co, V]):\n    parse_scalar: t.Callable[[str], T_co]\n    ops: t.Dict[str, Op[V]]\n    group_open: t.Dict[str, int]\n    group_close: t.Dict[str, int]\n\n    token_re: re.Pattern = field(init=False)\n    \"\"\"Regex matching operators, brackets, and whitespace\"\"\"\n\n    @t.overload\n    def __init__(self: Parser[str, V2], ops: t.Sequence[Op[V2]],\n                 parse_scalar: t.Optional[t.Callable[[str], str]] = None,\n                 groups: t.Optional[t.Sequence[t.Tuple[str, str]]] = None):\n        ...\n\n    @t.overload\n    def __init__(self: Parser[T, V2], ops: t.Sequence[Op[V2]],\n                 parse_scalar: t.Callable[[str], T],\n                 groups: t.Optional[t.Sequence[t.Tuple[str, str]]] = None):\n        ...\n\n    def __init__(self, ops: t.Sequence[Op[V]],\n                 parse_scalar: t.Optional[t.Callable[[str], t.Union[str, T_co]]] = None,\n                 groups: t.Optional[t.Sequence[t.Tuple[str, str]]] = None):\n        self.parse_scalar = t.cast(t.Callable[[str], T_co], parse_scalar or (lambda s: s))\n\n        if groups is None:\n            groups = [('(', ')'), ('[', ']')]\n\n        match_dict: t.Dict[str, t.Optional[Op[V]]] = {}\n\n        self.group_open = {}\n        self.group_close = {}\n        for (i, (group_open, group_close)) in enumerate(groups):\n            if group_open in match_dict:\n                raise ValueError(f\"Group open token '{group_open}' already defined\")\n            if group_close in match_dict:\n                raise ValueError(f\"Group close token '{group_close}' already defined\")\n            self.group_open[group_open] = i\n            self.group_close[group_close] = i\n            match_dict[group_open] = None\n            match_dict[group_close] = None\n\n        nary_precedences = set()\n        for op in ops:\n            if isinstance(op, NaryOp):\n                if op.precedence in nary_precedences:\n                    raise ValueError(\"N-ary operators must have distinct precedences. \"\n                                     f\"Precedence {op.precedence} conflicts with {op!r}\")\n                nary_precedences.add(op.precedence)\n            for alias in op.aliases:\n                if alias in match_dict:\n                    raise ValueError(f\"Alias '{alias}' already defined\")\n                match_dict[alias] = op\n\n        match_list = list(match_dict.items())\n        match_list.sort(key=lambda a: -len(a[0]))  #longer operators match first\n        self.ops = {k: v for (k, v) in match_list if v is not None}\n\n        def op_to_regex(tup: t.Tuple[str, t.Optional[Op[V]]]):\n            alias, op = tup\n            s = re.escape(alias)  #escape operator for use in regex\n            if op is not None and op.requires_whitespace:\n                #assert operator must be surrounded by whitespace\n                s = r\"(?<=\\s){}(?=\\s)\".format(s)\n            return s\n\n        op_alternation = \"|\".join(map(op_to_regex, match_list))\n        self.token_re = re.compile(f\"(\\\\s+|{op_alternation})\")\n\n    def parse(self, reader: TextIOBase) -> Expr[T_co, V]:\n        state = ParseState(self, reader)\n        expr = state.parse_expr()\n        if not state.empty():\n            raise ValueError(f\"At {state.line}:{state.char}, expected binary operator or end of expression, instead got token {state.peek()}\")\n        return expr\n
    "},{"location":"api/expr/#atomlib.expr.Parser.parse_scalar","title":"parse_scalar instance-attribute","text":"
    parse_scalar: Callable[[str], T_co] = cast(Callable[[str], T_co], parse_scalar or lambda s: s)\n
    "},{"location":"api/expr/#atomlib.expr.Parser.group_open","title":"group_open instance-attribute","text":"
    group_open: Dict[str, int] = {}\n
    "},{"location":"api/expr/#atomlib.expr.Parser.group_close","title":"group_close instance-attribute","text":"
    group_close: Dict[str, int] = {}\n
    "},{"location":"api/expr/#atomlib.expr.Parser.ops","title":"ops instance-attribute","text":"
    ops: Dict[str, Op[V]] = {k: _Lfor (k, v) in match_list if v is not None}\n
    "},{"location":"api/expr/#atomlib.expr.Parser.token_re","title":"token_re class-attribute instance-attribute","text":"
    token_re: Pattern = compile(f'(\\s+|{op_alternation})')\n

    Regex matching operators, brackets, and whitespace

    "},{"location":"api/expr/#atomlib.expr.Parser.parse","title":"parse","text":"
    parse(reader: TextIOBase) -> Expr[T_co, V]\n
    Source code in atomlib/expr.py
    def parse(self, reader: TextIOBase) -> Expr[T_co, V]:\n    state = ParseState(self, reader)\n    expr = state.parse_expr()\n    if not state.empty():\n        raise ValueError(f\"At {state.line}:{state.char}, expected binary operator or end of expression, instead got token {state.peek()}\")\n    return expr\n
    "},{"location":"api/expr/#atomlib.expr.ParseState","title":"ParseState","text":"

    Bases: Generic[T_co, V]

    Source code in atomlib/expr.py
    class ParseState(t.Generic[T_co, V]):\n    def __init__(self, parser: Parser[T_co, V], reader: TextIOBase):\n        self.parser: Parser[T_co, V] = parser\n        self._reader = reader\n        self._buf: t.Optional[str] = None\n        self._peek: t.List[Token[T_co, V]] = []\n        self.line = 0\n        self.char = 1\n\n    def empty(self) -> bool:\n        return self.peek() is None\n\n    def _get_buf(self) -> t.Optional[str]:\n        if self._buf is not None:\n            return self._buf\n        try:\n            self._buf = next(self._reader)\n            self.line += 1\n            self.char = 1\n        except StopIteration:\n            pass\n        return self._buf\n\n    def _refill_peek(self):\n        if len(self._peek) > 0:\n            return\n        try:\n            buf = next(self._reader)\n            self.line += 1\n            self.char = 1\n        except StopIteration:\n            return None\n        if buf is None or len(buf) == 0:  # type: ignore\n            return None\n\n        split = self.parser.token_re.split(buf)\n        for s in split:\n            if len(s) == 0:\n                continue\n            span = (self.char, self.char + len(s))\n            self.char += len(s)\n            self._peek.append(self.make_token(s, self.line, span))\n\n        self._peek.reverse()\n\n    def peek(self) -> t.Optional[Token[T_co, V]]:\n        self._refill_peek()\n        return self._peek[-1] if len(self._peek) > 0 else None\n\n    def collect_wspace(self) -> str:\n        wspace = \"\"\n        while True:\n            token = self.peek()\n            if not isinstance(token, WhitespaceToken):\n                break\n            wspace += token.raw\n            self.next()\n        return wspace\n\n    def make_token(self, s: str, line: int, span: t.Tuple[int, int]) -> Token[T_co, V]:\n        if s in self.parser.group_open:\n            return GroupOpenToken(s, line, span)\n        if s in self.parser.group_close:\n            return GroupCloseToken(s, line, span)\n        if s in self.parser.ops:\n            return OpToken(s, line, span, self.parser.ops[s])\n        if WSPACE_RE.fullmatch(s):\n            return WhitespaceToken(s, line, span)\n\n        try:\n            return ValueToken(s, line, span, self.parser.parse_scalar(s))\n        except (ValueError, TypeError):\n            raise ValueError(f\"Syntax error at {line}:{span[0]}: Unexpected token '{s}'\") from None\n\n    def next(self) -> t.Optional[Token[T_co, V]]:\n        token = self.peek()\n        if token is not None:\n            self._peek.pop()\n        return token\n\n    def parse_expr(self) -> Expr[T_co, V]:\n        \"\"\"\n            EXPR := PRIMARY, [ BINARY ]\n        \"\"\"\n        logging.debug(\"parse_expr()\")\n        lhs = self.parse_primary()\n        return self.parse_nary(lhs)\n\n    def parse_nary(self, lhs: Expr[T_co, V], level: t.Optional[int] = None) -> Expr[T_co, V]:\n        \"\"\"\n            NARY := { BINARY_OP | NARY_OP, ( PRIMARY, ? higher precedence NARY ? ) }\n        \"\"\"\n        logging.debug(f\"parse_nary({lhs}, level={level})\")\n        token = self.peek()\n        logging.debug(f\"token: '{token!r}'\")\n\n        while token is not None:\n            if not isinstance(token, OpToken) or \\\n               not isinstance(token.op, (NaryOp, BinaryOp)):\n                break\n\n            if level is not None and not token.op.precedes(level):\n                # next op has lower precedence, it needs to be parsed at a higher level\n                break\n\n            self.next()\n            rhs = self.parse_primary()\n            logging.debug(f\"rhs: '{rhs}'\")\n\n            inner = self.peek()\n            if inner is not None and isinstance(inner, OpToken):\n                inner_op = inner.op\n                if isinstance(inner_op, (NaryOp, BinaryOp)) and \\\n                    inner_op.precedes(token.op):\n                    #rhs is actually lhs of an inner expression\n                    rhs = self.parse_nary(rhs, token.op.precedence)\n\n            # append rhs to lhs and loop\n            if isinstance(token.op, NaryOp):\n                if isinstance(lhs, NaryExpr) and token.op == lhs.op:\n                    # append to existing n-ary node\n                    lhs = NaryExpr(list(lhs.op_tokens) + [token], list(lhs.args) + [rhs])\n                else:\n                    # make new n-ary expression\n                    lhs = NaryExpr([token], [lhs, rhs])\n            else:\n                # or make binary expression\n                lhs = BinaryExpr(token, lhs, rhs)\n\n            token = self.peek()\n\n        return lhs\n\n    def parse_primary(self) -> Expr[T_co, V]:\n        \"\"\"\n            PRIMARY := GROUP_OPEN, EXPR, GROUP_CLOSE | UNARY_OP, PRIMARY | SCALAR\n        \"\"\"\n\n        logging.debug(\"parse_primary()\")\n        lspace = self.collect_wspace()\n        token = self.peek()\n        logging.debug(f\"token: '{token!r}'\")\n        if token is None:\n            raise ValueError(\"Unexpected EOF while parsing expression\")\n\n        if isinstance(token, GroupOpenToken):\n            self.next()\n            inner = self.parse_expr()\n            close = self.next()\n            rspace = self.collect_wspace()\n            if close is None:\n                raise ValueError(f\"Unclosed delimeter '{token.raw}' opened at {token.line}:{token.span[0]}\")\n            if not isinstance(close, GroupCloseToken):\n                raise ValueError(f\"At {close.line}:{close.span[0]}: Expected operator or group close, instead got '{close.raw}'\")\n            if self.parser.group_open[token.raw] != self.parser.group_close[close.raw]:\n                raise ValueError(f\"At {token.line}:{token.span[0]}-{close.span[1]}: Mismatched delimeters: '{token.raw}' closed with '{close.raw}'\")\n            return GroupExpr(token, inner, close, lspace, rspace)\n\n        if isinstance(token, GroupCloseToken):\n            raise ValueError(f\"At {token.line}:{token.span[0]}: Unexpected delimeter '{token.raw.strip()}'\")\n\n        if isinstance(token, OpToken):\n            self.next()\n            if not isinstance(token.op, UnaryOp):\n                raise ValueError(f\"At {token.line}:{token.span[0]}: Unexpected operator '{token.raw}'. Expected a value or prefix operator.\")\n            inner = self.parse_primary()\n            return UnaryExpr(token, inner, lspace)\n\n        if isinstance(token, ValueToken):\n            self.next()\n            rspace = self.collect_wspace()\n            return ValueExpr(token, lspace, rspace)\n\n        raise TypeError(f\"Unknown token type '{type(token)}'\")\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.parser","title":"parser instance-attribute","text":"
    parser: Parser[T_co, V] = parser\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.line","title":"line instance-attribute","text":"
    line = 0\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.char","title":"char instance-attribute","text":"
    char = 1\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.empty","title":"empty","text":"
    empty() -> bool\n
    Source code in atomlib/expr.py
    def empty(self) -> bool:\n    return self.peek() is None\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.peek","title":"peek","text":"
    peek() -> Optional[Token[T_co, V]]\n
    Source code in atomlib/expr.py
    def peek(self) -> t.Optional[Token[T_co, V]]:\n    self._refill_peek()\n    return self._peek[-1] if len(self._peek) > 0 else None\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.collect_wspace","title":"collect_wspace","text":"
    collect_wspace() -> str\n
    Source code in atomlib/expr.py
    def collect_wspace(self) -> str:\n    wspace = \"\"\n    while True:\n        token = self.peek()\n        if not isinstance(token, WhitespaceToken):\n            break\n        wspace += token.raw\n        self.next()\n    return wspace\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.make_token","title":"make_token","text":"
    make_token(\n    s: str, line: int, span: Tuple[int, int]\n) -> Token[T_co, V]\n
    Source code in atomlib/expr.py
    def make_token(self, s: str, line: int, span: t.Tuple[int, int]) -> Token[T_co, V]:\n    if s in self.parser.group_open:\n        return GroupOpenToken(s, line, span)\n    if s in self.parser.group_close:\n        return GroupCloseToken(s, line, span)\n    if s in self.parser.ops:\n        return OpToken(s, line, span, self.parser.ops[s])\n    if WSPACE_RE.fullmatch(s):\n        return WhitespaceToken(s, line, span)\n\n    try:\n        return ValueToken(s, line, span, self.parser.parse_scalar(s))\n    except (ValueError, TypeError):\n        raise ValueError(f\"Syntax error at {line}:{span[0]}: Unexpected token '{s}'\") from None\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.next","title":"next","text":"
    next() -> Optional[Token[T_co, V]]\n
    Source code in atomlib/expr.py
    def next(self) -> t.Optional[Token[T_co, V]]:\n    token = self.peek()\n    if token is not None:\n        self._peek.pop()\n    return token\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.parse_expr","title":"parse_expr","text":"
    parse_expr() -> Expr[T_co, V]\n

    EXPR := PRIMARY, [ BINARY ]

    Source code in atomlib/expr.py
    def parse_expr(self) -> Expr[T_co, V]:\n    \"\"\"\n        EXPR := PRIMARY, [ BINARY ]\n    \"\"\"\n    logging.debug(\"parse_expr()\")\n    lhs = self.parse_primary()\n    return self.parse_nary(lhs)\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.parse_nary","title":"parse_nary","text":"
    parse_nary(\n    lhs: Expr[T_co, V], level: Optional[int] = None\n) -> Expr[T_co, V]\n

    NARY := { BINARY_OP | NARY_OP, ( PRIMARY, ? higher precedence NARY ? ) }

    Source code in atomlib/expr.py
    def parse_nary(self, lhs: Expr[T_co, V], level: t.Optional[int] = None) -> Expr[T_co, V]:\n    \"\"\"\n        NARY := { BINARY_OP | NARY_OP, ( PRIMARY, ? higher precedence NARY ? ) }\n    \"\"\"\n    logging.debug(f\"parse_nary({lhs}, level={level})\")\n    token = self.peek()\n    logging.debug(f\"token: '{token!r}'\")\n\n    while token is not None:\n        if not isinstance(token, OpToken) or \\\n           not isinstance(token.op, (NaryOp, BinaryOp)):\n            break\n\n        if level is not None and not token.op.precedes(level):\n            # next op has lower precedence, it needs to be parsed at a higher level\n            break\n\n        self.next()\n        rhs = self.parse_primary()\n        logging.debug(f\"rhs: '{rhs}'\")\n\n        inner = self.peek()\n        if inner is not None and isinstance(inner, OpToken):\n            inner_op = inner.op\n            if isinstance(inner_op, (NaryOp, BinaryOp)) and \\\n                inner_op.precedes(token.op):\n                #rhs is actually lhs of an inner expression\n                rhs = self.parse_nary(rhs, token.op.precedence)\n\n        # append rhs to lhs and loop\n        if isinstance(token.op, NaryOp):\n            if isinstance(lhs, NaryExpr) and token.op == lhs.op:\n                # append to existing n-ary node\n                lhs = NaryExpr(list(lhs.op_tokens) + [token], list(lhs.args) + [rhs])\n            else:\n                # make new n-ary expression\n                lhs = NaryExpr([token], [lhs, rhs])\n        else:\n            # or make binary expression\n            lhs = BinaryExpr(token, lhs, rhs)\n\n        token = self.peek()\n\n    return lhs\n
    "},{"location":"api/expr/#atomlib.expr.ParseState.parse_primary","title":"parse_primary","text":"
    parse_primary() -> Expr[T_co, V]\n

    PRIMARY := GROUP_OPEN, EXPR, GROUP_CLOSE | UNARY_OP, PRIMARY | SCALAR

    Source code in atomlib/expr.py
    def parse_primary(self) -> Expr[T_co, V]:\n    \"\"\"\n        PRIMARY := GROUP_OPEN, EXPR, GROUP_CLOSE | UNARY_OP, PRIMARY | SCALAR\n    \"\"\"\n\n    logging.debug(\"parse_primary()\")\n    lspace = self.collect_wspace()\n    token = self.peek()\n    logging.debug(f\"token: '{token!r}'\")\n    if token is None:\n        raise ValueError(\"Unexpected EOF while parsing expression\")\n\n    if isinstance(token, GroupOpenToken):\n        self.next()\n        inner = self.parse_expr()\n        close = self.next()\n        rspace = self.collect_wspace()\n        if close is None:\n            raise ValueError(f\"Unclosed delimeter '{token.raw}' opened at {token.line}:{token.span[0]}\")\n        if not isinstance(close, GroupCloseToken):\n            raise ValueError(f\"At {close.line}:{close.span[0]}: Expected operator or group close, instead got '{close.raw}'\")\n        if self.parser.group_open[token.raw] != self.parser.group_close[close.raw]:\n            raise ValueError(f\"At {token.line}:{token.span[0]}-{close.span[1]}: Mismatched delimeters: '{token.raw}' closed with '{close.raw}'\")\n        return GroupExpr(token, inner, close, lspace, rspace)\n\n    if isinstance(token, GroupCloseToken):\n        raise ValueError(f\"At {token.line}:{token.span[0]}: Unexpected delimeter '{token.raw.strip()}'\")\n\n    if isinstance(token, OpToken):\n        self.next()\n        if not isinstance(token.op, UnaryOp):\n            raise ValueError(f\"At {token.line}:{token.span[0]}: Unexpected operator '{token.raw}'. Expected a value or prefix operator.\")\n        inner = self.parse_primary()\n        return UnaryExpr(token, inner, lspace)\n\n    if isinstance(token, ValueToken):\n        self.next()\n        rspace = self.collect_wspace()\n        return ValueExpr(token, lspace, rspace)\n\n    raise TypeError(f\"Unknown token type '{type(token)}'\")\n
    "},{"location":"api/expr/#atomlib.expr.SupportsBool","title":"SupportsBool","text":"

    Bases: Protocol

    Source code in atomlib/expr.py
    class SupportsBool(t.Protocol):\n    def __and__(self: SupportsBoolSelf, other: SupportsBoolSelf) -> SupportsBoolSelf:\n        ...\n\n    def __or__(self: SupportsBoolSelf, other: SupportsBoolSelf) -> SupportsBoolSelf:\n        ...\n\n    def __xor__(self: SupportsBoolSelf, other: SupportsBoolSelf) -> SupportsBoolSelf:\n        ...\n\n    def __invert__(self: SupportsBoolSelf) -> SupportsBoolSelf:\n        ...\n
    "},{"location":"api/expr/#atomlib.expr.SupportsNum","title":"SupportsNum","text":"

    Bases: Protocol

    Source code in atomlib/expr.py
    class SupportsNum(t.Protocol):\n    def __add__(self: SupportsNumSelf, other: SupportsNumSelf) -> SupportsNumSelf:\n        ...\n\n    def __sub__(self: SupportsNumSelf, other: SupportsNumSelf) -> SupportsNumSelf:\n        ...\n\n    def __mul__(self: SupportsNumSelf, other: SupportsNumSelf) -> SupportsNumSelf:\n        ...\n\n    def __truediv__(self: SupportsNumSelf, other: SupportsNumSelf) -> SupportsNumSelf:\n        ...\n\n    def __floordiv__(self: SupportsNumSelf, other: SupportsNumSelf) -> SupportsNumSelf:\n        ...\n\n    def __mod__(self: SupportsNumSelf, other: SupportsNumSelf) -> SupportsNumSelf:\n        ...\n\n    def __pow__(self: SupportsNumSelf, other: SupportsNumSelf) -> SupportsNumSelf:\n        ...\n\n    def __neg__(self: SupportsNumSelf) -> SupportsNumSelf:\n        ...\n\n    def __pos__(self: SupportsNumSelf) -> SupportsNumSelf:\n        ...\n
    "},{"location":"api/expr/#atomlib.expr.interleave","title":"interleave","text":"
    interleave(\n    l1: Iterable[T_co], l2: Iterable[T_co]\n) -> Iterator[T_co]\n
    Source code in atomlib/expr.py
    def interleave(l1: t.Iterable[T_co], l2: t.Iterable[T_co]) -> t.Iterator[T_co]:\n    for (v1, v2) in zip_longest(l1, l2):\n        yield v1\n        if v2 is not None:\n            yield v2\n
    "},{"location":"api/expr/#atomlib.expr.parse_numeric","title":"parse_numeric","text":"
    parse_numeric(s: str) -> Union[int, float]\n
    Source code in atomlib/expr.py
    def parse_numeric(s: str) -> t.Union[int, float]:\n    try:\n        return int(s)\n    except ValueError:\n        pass\n    return float(s)\n
    "},{"location":"api/expr/#atomlib.expr.sub","title":"sub","text":"
    sub(lhs: SupportsNum, rhs: Optional[SupportsNum] = None)\n
    Source code in atomlib/expr.py
    def sub(lhs: SupportsNum, rhs: t.Optional[SupportsNum] = None):\n    if rhs is None:\n        return -lhs\n    return lhs-rhs\n
    "},{"location":"api/expr/#atomlib.expr.add","title":"add","text":"
    add(lhs: SupportsNum, rhs: Optional[SupportsNum] = None)\n
    Source code in atomlib/expr.py
    def add(lhs: SupportsNum, rhs: t.Optional[SupportsNum] = None):\n    if rhs is None:\n        return +lhs\n    return lhs+rhs\n
    "},{"location":"api/expr/#atomlib.expr.parse_boolean","title":"parse_boolean","text":"
    parse_boolean(s: str) -> bool\n
    Source code in atomlib/expr.py
    def parse_boolean(s: str) -> bool:\n    if s.lower() in (\"0\", \"false\", \"f\"):\n        return False\n    elif s.lower() in (\"1\", \"true\", \"t\"):\n        return True\n    raise ValueError(f\"Can't parse '{s}' as boolean\")\n
    "},{"location":"api/expr/#atomlib.expr.stack","title":"stack","text":"
    stack(*vs: ndarray) -> ndarray\n
    Source code in atomlib/expr.py
    def stack(*vs: numpy.ndarray) -> numpy.ndarray:\n    return numpy.stack(vs, axis=0)\n
    "},{"location":"api/make/","title":"atomlib.make","text":"

    Functions to create structures and cells.

    "},{"location":"api/make/#atomlib.make.CellType","title":"CellType module-attribute","text":"
    CellType: TypeAlias = Literal['conv', 'prim', 'ortho']\n
    "},{"location":"api/make/#atomlib.make.fcc","title":"fcc","text":"
    fcc(\n    elem: ElemLike,\n    a: Num,\n    *,\n    cell: CellType = \"conv\",\n    additional: Optional[IntoAtoms] = None\n) -> AtomCell\n

    Make a FCC lattice of the specified element, with the given cell. If cell='conv' (the default), return the conventional cell, four atoms with cubic cell symmetry. If cell='prim', return the primitive cell, a single atom with rhombohedral cell symmetry. If cell='ortho', return an orthogonal cell, two atoms in a cell of size [a/sqrt(2), a/sqrt(2), a].

    If additional is specified, those atoms will be added to the lattice (in fractional coordinates).

    PARAMETER DESCRIPTION elem

    Element to add (e.g. 'Al' or 13)

    TYPE: ElemLike

    a

    Lattice parameter (Angstrom)

    TYPE: Num

    cell

    Cell type to return ('conv', 'prim', or 'ortho')

    TYPE: CellType DEFAULT: 'conv'

    additional

    Additional atoms to add to the structure.

    TYPE: Optional[IntoAtoms] DEFAULT: None

    RETURNS DESCRIPTION AtomCell

    Periodic FCC unit cell

    Source code in atomlib/make/__init__.py
    def fcc(elem: ElemLike, a: Num, *, cell: CellType = 'conv', additional: t.Optional[IntoAtoms] = None) -> AtomCell:\n    \"\"\"\n    Make a FCC lattice of the specified element, with the given cell.\n    If `cell='conv'` (the default), return the conventional cell, four atoms with cubic cell symmetry.\n    If `cell='prim'`, return the primitive cell, a single atom with rhombohedral cell symmetry.\n    If `cell='ortho'`, return an orthogonal cell, two atoms in a cell of size `[a/sqrt(2), a/sqrt(2), a]`.\n\n    If `additional` is specified, those atoms will be added to the lattice (in fractional coordinates).\n\n    Args:\n      elem: Element to add (e.g. `'Al'` or `13`)\n      a: Lattice parameter (Angstrom)\n      cell: Cell type to return ('conv', 'prim', or 'ortho')\n      additional: Additional atoms to add to the structure.\n\n    Returns:\n      Periodic FCC unit cell\n    \"\"\"\n\n    elems = [get_elem(elem)]\n    cell = t.cast(CellType, str(cell).lower())\n\n    if cell == 'prim':\n        xs = ys = zs = [0.]\n        ortho = LinearTransform3D(a / 2. * numpy.array([\n            [0., 1., 1.],\n            [1., 0., 1.],\n            [1., 1., 0.],\n        ]))\n    elif cell == 'ortho':\n        elems *= 2\n        xs = ys = zs = [0., 0.5]\n        b = a / numpy.sqrt(2)\n        ortho = LinearTransform3D.scale(b, b, a)\n    elif cell == 'conv':\n        elems *= 4\n        xs = [0., 0., 0.5, 0.5]\n        ys = [0., 0.5, 0., 0.5]\n        zs = [0., 0.5, 0.5, 0.]\n        ortho = LinearTransform3D.scale(all=a)\n    else:\n        raise ValueError(f\"Unknown cell type '{cell}'. Expected 'conv', 'prim', or 'ortho'.\")\n\n    frame = Atoms(dict(x=xs, y=ys, z=zs, elem=elems))\n    if additional is not None:\n        frame = Atoms.concat((frame, additional), how='vertical')\n\n    return AtomCell.from_ortho(frame, ortho, frame='cell_frac')\n
    "},{"location":"api/make/#atomlib.make.wurtzite","title":"wurtzite","text":"
    wurtzite(\n    elems: ElemsLike,\n    a: Num,\n    c: Optional[Num] = None,\n    d: Optional[Num] = None,\n    *,\n    cell: CellType = \"conv\"\n) -> AtomCell\n

    Create a wurzite lattice of the specified two elements, with the given cell. a and c are the hexagonal cell parameters. d is the fractional distance between the two sublattices.

    If cell='prim' or cell='conv' (the default), return a hexagonal unit cell. If cell='ortho', return an orthogonal unit cell constructed from the hexagonal unit cell as \\(\\hat{\\mathbf{a}} = \\mathbf{a}\\), \\(\\hat{\\mathbf{b}} = \\mathbf{a} + 2 \\mathbf{b}\\), \\(\\hat{\\mathbf{c}} = \\mathbf{c}\\).

    PARAMETER DESCRIPTION elems

    Elements to add (e.g. 'AlN' or ('Al', 'N'))

    TYPE: ElemsLike

    a

    Lattice parameter (Angstrom)

    TYPE: Num

    c

    Vertical lattice parameter (Angstrom)

    TYPE: Optional[Num] DEFAULT: None

    d

    Vertical distance between the two sublattices (fractional)

    TYPE: Optional[Num] DEFAULT: None

    cell

    Cell type to return ('conv', 'prim', or 'ortho')

    TYPE: CellType DEFAULT: 'conv'

    RETURNS DESCRIPTION AtomCell

    Periodic wurtzite unit cell

    Source code in atomlib/make/__init__.py
    def wurtzite(elems: ElemsLike, a: Num, c: t.Optional[Num] = None,\n             d: t.Optional[Num] = None, *, cell: CellType = 'conv') -> AtomCell:\n    r\"\"\"\n    Create a wurzite lattice of the specified two elements, with the given cell.\n    `a` and `c` are the hexagonal cell parameters. `d` is the fractional distance\n    between the two sublattices.\n\n    If `cell='prim'` or `cell='conv'` (the default), return a hexagonal unit cell.\n    If `cell='ortho'`, return an orthogonal unit cell constructed from the hexagonal unit cell as\n    $\\hat{\\mathbf{a}} = \\mathbf{a}$, $\\hat{\\mathbf{b}} = \\mathbf{a} + 2 \\mathbf{b}$, $\\hat{\\mathbf{c}} = \\mathbf{c}$.\n\n    Args:\n      elems: Elements to add (e.g. `'AlN'` or `('Al', 'N')`)\n      a: Lattice parameter (Angstrom)\n      c: Vertical lattice parameter (Angstrom)\n      d: Vertical distance between the two sublattices (fractional)\n      cell: Cell type to return ('conv', 'prim', or 'ortho')\n\n    Returns:\n      Periodic wurtzite unit cell\n    \"\"\"\n    elems = [t[0] for t in get_elems(elems)]\n\n    if len(elems) != 2:\n        raise ValueError(\"Expected two elements.\")\n\n    # default to ideal c/a\n    c_a = float(numpy.sqrt(8. / 3.) if c is None else c / a)\n\n    d = 0.25 + 1 / (3 * c_a**2) if d is None else d\n    if not 0 < d < 0.5:\n        raise ValueError(f\"Invalid 'd' parameter: {d}\")\n\n    cell = t.cast(CellType, str(cell).lower())\n    if cell not in ('prim', 'conv', 'ortho'):\n        raise ValueError(f\"Unknown cell type '{cell}'. Expected 'conv', 'prim', or 'ortho'.\")\n\n    ortho = cell_to_ortho(\n        a * numpy.array([1., 1., c_a]), \n        numpy.pi * numpy.array([1/2., 1/2., 2/3.])\n    )\n    xs = [2/3, 2/3, 1/3, 1/3]\n    ys = [1/3, 1/3, 2/3, 2/3]\n    #zs = [1. - d, 0., 0.5 - d, 0.5]\n    zs = [0.5, 0.5 + d, 0., d]\n    elems *= 2\n\n    frame = Atoms(dict(x=xs, y=ys, z=zs, elem=elems))\n    atoms = AtomCell.from_ortho(frame, ortho, frame='cell_frac')\n    if cell == 'ortho':\n        return _ortho_hexagonal(atoms)\n    return atoms\n
    "},{"location":"api/make/#atomlib.make.graphite","title":"graphite","text":"
    graphite(\n    elem: Union[str, ElemLike, None] = None,\n    a: Optional[Num] = None,\n    c: Optional[Num] = None,\n    *,\n    cell: CellType = \"conv\"\n)\n
    Source code in atomlib/make/__init__.py
    def graphite(elem: t.Union[str, ElemLike, None] = None, a: t.Optional[Num] = None,\n             c: t.Optional[Num] = None, *, cell: CellType = 'conv'):\n    if elem is None:\n        elem = 6\n    else:\n        elem = get_elem(elem)\n        if elem != 6 and a is None or c is None:\n            raise ValueError(\"'a' and 'c' must be specified for non-graphite elements.\")\n\n    if a is None:\n        a = 2.47\n    if c is None:\n        c = 8.69\n\n    cell = t.cast(CellType, str(cell).lower())\n    if cell not in ('prim', 'conv', 'ortho'):\n        raise ValueError(f\"Unknown cell type '{cell}'. Expected 'conv', 'prim', or 'ortho'.\")\n\n    ortho = cell_to_ortho(\n        numpy.array([a, a, c]), \n        numpy.pi * numpy.array([1/2., 1/2., 2/3.])\n    )\n    xs = [0., 2/3, 0., 1/3]\n    ys = [0., 1/3, 0., 2/3]\n    zs = [0., 0., 1/2, 1/2]\n    elems = [elem] * 4\n\n    frame = Atoms(dict(x=xs, y=ys, z=zs, elem=elems))\n    atoms = AtomCell.from_ortho(frame, ortho, frame='cell_frac')\n\n    if cell == 'ortho':\n        return _ortho_hexagonal(atoms)\n    return atoms\n
    "},{"location":"api/make/#atomlib.make.rocksalt","title":"rocksalt","text":"
    rocksalt(\n    elems: ElemsLike, a: Num, *, cell: CellType = \"conv\"\n) -> AtomCell\n

    Create a rock salt FCC structure AB. Returns the same cell types as fcc.

    PARAMETER DESCRIPTION elems

    Elements to add (e.g. 'NaCl' or ('Na', 'Cl'))

    TYPE: ElemsLike

    a

    Lattice parameter (Angstrom)

    TYPE: Num

    cell

    Cell type to return ('conv', 'prim', or 'ortho'). Returns the same cell types as fcc.

    TYPE: CellType DEFAULT: 'conv'

    RETURNS DESCRIPTION AtomCell

    Periodic rocksalt unit cell

    Source code in atomlib/make/__init__.py
    def rocksalt(elems: ElemsLike, a: Num, *,\n             cell: CellType = 'conv') -> AtomCell:\n    \"\"\"\n    Create a rock salt FCC structure AB. Returns the same cell types as `fcc`.\n\n    Args:\n      elems: Elements to add (e.g. `'NaCl'` or `('Na', 'Cl')`)\n      a: Lattice parameter (Angstrom)\n      cell: Cell type to return ('conv', 'prim', or 'ortho'). Returns\n            the same cell types as `fcc`.\n\n    Returns:\n      Periodic rocksalt unit cell\n    \"\"\"\n    elems = [t[0] for t in get_elems(elems)]\n\n    if len(elems) != 2:\n        raise ValueError(\"Expected two elements.\")\n\n    if cell == 'prim':\n        additional: t.Dict[str, t.Any] = {\n            'x': [-0.5],\n            'y': [0.5],\n            'z': [0.5],\n            'elem': [elems[1]],\n        }\n    elif cell == 'ortho':\n        additional: t.Dict[str, t.Any] = {\n            'x': [0.5, 0.0],\n            'y': [0.5, 0.0],\n            'z': [0.0, 0.5],\n            'elem': [elems[1]] * 2,\n        }\n    elif cell == 'conv':\n        additional: t.Dict[str, t.Any] = {\n            'x': [0.5, 0.0, 0.0, 0.5],\n            'y': [0.0, 0.5, 0.0, 0.5],\n            'z': [0.0, 0.0, 0.5, 0.5],\n            'elem': [elems[1]] * 4,\n        }\n    else:\n        raise ValueError(f\"Unknown cell type '{cell}'. Expected 'conv', 'prim', or 'ortho'.\")\n\n    return fcc(elems[0], a, cell=cell, additional=additional)\n
    "},{"location":"api/make/#atomlib.make.zincblende","title":"zincblende","text":"
    zincblende(\n    elems: ElemsLike, a: Num, *, cell: CellType = \"conv\"\n) -> AtomCell\n

    Create a zinc-blende FCC structure AB.

    PARAMETER DESCRIPTION elems

    Elements to add (e.g. 'ZnS' or ('Zn', 'S'))

    TYPE: ElemsLike

    a

    Lattice parameter (Angstrom)

    TYPE: Num

    cell

    Cell type to return ('conv', 'prim', or 'ortho'). Returns the same cell types as fcc.

    TYPE: CellType DEFAULT: 'conv'

    RETURNS DESCRIPTION AtomCell

    Periodic zinc-blende unit cell

    Source code in atomlib/make/__init__.py
    def zincblende(elems: ElemsLike, a: Num, *,\n               cell: CellType = 'conv') -> AtomCell:\n    \"\"\"\n    Create a zinc-blende FCC structure AB.\n\n    Args:\n      elems: Elements to add (e.g. `'ZnS'` or `('Zn', 'S')`)\n      a: Lattice parameter (Angstrom)\n      cell: Cell type to return ('conv', 'prim', or 'ortho'). Returns\n            the same cell types as `fcc`.\n\n    Returns:\n      Periodic zinc-blende unit cell\n    \"\"\"\n    elems = [t[0] for t in get_elems(elems)]\n\n    if len(elems) != 2:\n        raise ValueError(\"Expected two elements.\")\n\n    if cell == 'prim':\n        d = [0.25]\n        additional: t.Dict[str, t.Any] = {\n            'x': d,\n            'y': d,\n            'z': d,\n            'elem': [elems[1]],\n        }\n    elif cell == 'ortho':\n        additional: t.Dict[str, t.Any] = {\n            'x': [0.5, 0.0],\n            'y': [0.0, 0.5],\n            'z': [0.25, 0.75],\n            'elem': [elems[1]] * 2,\n        }\n    elif cell == 'conv':\n        additional: t.Dict[str, t.Any] = {\n            'x': [0.25, 0.25, 0.75, 0.75],\n            'y': [0.25, 0.75, 0.25, 0.75],\n            'z': [0.25, 0.75, 0.75, 0.25],\n            'elem': [elems[1]] * 4,\n        }\n    else:\n        raise ValueError(f\"Unknown cell type '{cell}'. Expected 'conv', 'prim', or 'ortho'.\")\n\n    return fcc(elems[0], a, cell=cell, additional=additional)\n
    "},{"location":"api/make/#atomlib.make.diamond","title":"diamond","text":"
    diamond(\n    elem: None = None,\n    a: Optional[Num] = None,\n    *,\n    cell: CellType = \"conv\"\n) -> AtomCell\n
    diamond(\n    elem: Optional[ElemLike],\n    a: Num,\n    *,\n    cell: CellType = \"conv\"\n) -> AtomCell\n
    diamond(\n    elem: Optional[ElemLike] = None,\n    a: Optional[Num] = None,\n    *,\n    cell: CellType = \"conv\"\n) -> AtomCell\n

    Create a diamond cubic FCC structure. elem and a can be left unspecified to return a diamond structure. Otherwise, both must be specified.

    PARAMETER DESCRIPTION elem

    Element to add (e.g. 'C')

    TYPE: Optional[ElemLike] DEFAULT: None

    a

    Lattice parameter (Angstrom)

    TYPE: Optional[Num] DEFAULT: None

    cell

    Cell type to return ('conv', 'prim', or 'ortho'). Returns the same cell types as fcc.

    TYPE: CellType DEFAULT: 'conv'

    RETURNS DESCRIPTION AtomCell

    Periodic diamond cubic unit cell

    Source code in atomlib/make/__init__.py
    def diamond(elem: t.Optional[ElemLike] = None, a: t.Optional[Num] = None, *,\n            cell: CellType = 'conv') -> AtomCell:\n    \"\"\"\n    Create a diamond cubic FCC structure. `elem` and `a` can be left\n    unspecified to return a diamond structure. Otherwise, both\n    must be specified.\n\n    Args:\n      elem: Element to add (e.g. `'C'`)\n      a: Lattice parameter (Angstrom)\n      cell: Cell type to return ('conv', 'prim', or 'ortho'). Returns\n            the same cell types as `fcc`.\n\n    Returns:\n      Periodic diamond cubic unit cell\n    \"\"\"\n    if elem is None:\n        elems = (6, 6)\n    else:\n        elem = get_elem(elem)\n        elems = (elem, elem)\n\n    if a is None:\n        if elems == (6, 6):\n            # diamond lattice parameter\n            a = 3.567\n        else:\n            raise ValueError(\"Must specify lattice parameter 'a'.\")\n\n    return zincblende(elems, a, cell=cell)\n
    "},{"location":"api/make/#atomlib.make.fluorite","title":"fluorite","text":"
    fluorite(\n    elems: ElemsLike, a: Num, *, cell: CellType = \"conv\"\n) -> AtomCell\n

    Create a fluorite FCC structure \\(\\mathrm{AB_2}\\). Returns the same cell types as fcc.

    PARAMETER DESCRIPTION elems

    Elements to add (e.g. 'CaF' or ('Ca', 'F'))

    TYPE: ElemsLike

    a

    Lattice parameter (Angstrom)

    TYPE: Num

    cell

    Cell type to return ('conv', 'prim', or 'ortho'). Returns the same cell types as fcc.

    TYPE: CellType DEFAULT: 'conv'

    RETURNS DESCRIPTION AtomCell

    Periodic fluorite unit cell

    Source code in atomlib/make/__init__.py
    def fluorite(elems: ElemsLike, a: Num, *,\n             cell: CellType = 'conv') -> AtomCell:\n    \"\"\"\n    Create a fluorite FCC structure $\\\\mathrm{AB_2}$. Returns the same cell types as `fcc`.\n\n    Args:\n      elems: Elements to add (e.g. `'CaF'` or `('Ca', 'F')`)\n      a: Lattice parameter (Angstrom)\n      cell: Cell type to return ('conv', 'prim', or 'ortho'). Returns\n            the same cell types as `fcc`.\n\n    Returns:\n      Periodic fluorite unit cell\n    \"\"\"\n    elems = [t[0] for t in get_elems(elems)]\n\n    if len(elems) != 2:\n        raise ValueError(\"Expected two elements.\")\n\n    if cell == 'prim':\n        d = [0.25, 0.75]\n        additional: t.Dict[str, t.Any] = {\n            'x': d, 'y': d, 'z': d,\n            'elem': [elems[1]] * 2,\n        }\n    elif cell == 'ortho':\n        additional: t.Dict[str, t.Any] = {\n            'x': [0.5, 0.5, 0.0, 0.0],\n            'y': [0.0, 0.0, 0.5, 0.5],\n            'z': [0.25, 0.75, 0.25, 0.75],\n            'elem': [elems[1]] * 4,\n        }\n    elif cell == 'conv':\n        additional: t.Dict[str, t.Any] = {\n            'x': [0.25] * 4 + [0.75] * 4,\n            'y': [0.25, 0.25, 0.75, 0.75, 0.25, 0.25, 0.75, 0.75],\n            'z': [0.25, 0.75, 0.25, 0.75, 0.25, 0.75, 0.25, 0.75],\n            'elem': [elems[1]] * 8,\n        }\n    else:\n        raise ValueError(f\"Unknown cell type '{cell}'. Expected 'conv', 'prim', or 'ortho'.\")\n\n    return fcc(elems[0], a, cell=cell, additional=additional)\n
    "},{"location":"api/make/#atomlib.make.cesium_chloride","title":"cesium_chloride","text":"
    cesium_chloride(\n    elems: Literal[\"CsCl\"] = \"CsCl\",\n    a: None = None,\n    *,\n    d: None = None,\n    cell: CellType = \"conv\"\n) -> AtomCell\n
    cesium_chloride(\n    elems: ElemsLike,\n    a: Num,\n    *,\n    d: None = None,\n    cell: CellType = \"conv\"\n) -> AtomCell\n
    cesium_chloride(\n    elems: ElemsLike = \"CsCl\",\n    a: None = None,\n    *,\n    d: Num,\n    cell: CellType = \"conv\"\n) -> AtomCell\n
    cesium_chloride(\n    elems: ElemsLike = \"CsCl\",\n    a: Optional[Num] = None,\n    *,\n    d: Optional[Num] = None,\n    cell: CellType = \"conv\"\n) -> AtomCell\n

    Create a cesium chloride structure \\(\\mathrm{AB}\\). CsCl is simple cubic, so all cell types are the same.

    Only one of a (lattice parameter) or d (bond distance) needs to be specified.

    PARAMETER DESCRIPTION elems

    Elements to add (e.g. 'CsCl' or ('Cs', 'Cl'))

    TYPE: ElemsLike DEFAULT: 'CsCl'

    a

    Lattice parameter (Angstrom)

    TYPE: Optional[Num] DEFAULT: None

    d

    Nearest-neighbor bond distance (Angstrom)

    TYPE: Optional[Num] DEFAULT: None

    cell

    Cell type to return ('conv', 'prim', or 'ortho'). All are identical for this structure

    TYPE: CellType DEFAULT: 'conv'

    RETURNS DESCRIPTION AtomCell

    Periodic cesium chloride unit cell

    Source code in atomlib/make/__init__.py
    def cesium_chloride(elems: ElemsLike = 'CsCl', a: t.Optional[Num] = None, *,\n                    d: t.Optional[Num] = None, cell: CellType = 'conv') -> AtomCell:\n    \"\"\"\n    Create a cesium chloride structure $\\\\mathrm{AB}$.\n    CsCl is simple cubic, so all cell types are the same.\n\n    Only one of `a` (lattice parameter) or `d` (bond distance) needs to be specified.\n\n    Args:\n      elems: Elements to add (e.g. `'CsCl'` or `('Cs', 'Cl')`)\n      a: Lattice parameter (Angstrom)\n      d: Nearest-neighbor bond distance (Angstrom)\n      cell: Cell type to return ('conv', 'prim', or 'ortho').\n            All are identical for this structure\n\n    Returns:\n      Periodic cesium chloride unit cell\n    \"\"\"\n    elems = [t[0] for t in get_elems(elems)]\n\n    if len(elems) != 2:\n        raise ValueError(\"Expected two elements.\")\n\n    if a is not None and d is not None:\n        raise ValueError(\"Both 'a' and 'd' cannot be specified.\")\n\n    if a is None:\n        if d is not None:\n            a_ = d * 2/numpy.sqrt(3)\n        elif elems == [55, 17]:\n            # CsCl lattice parameter\n            a_ = 4.123\n        else:\n            raise ValueError(\"Must specify either 'a' or 'd' lattice parameter\")\n    else:\n        a_ = a\n\n    ortho = cell_to_ortho([a_] * 3)\n\n    frame = Atoms(dict(x=[0., 0.5], y=[0., 0.5], z=[0., 0.5], elem=elems))\n    return AtomCell.from_ortho(frame, ortho, frame='cell_frac')\n
    "},{"location":"api/make/#atomlib.make.perovskite","title":"perovskite","text":"
    perovskite(\n    elems: ElemsLike,\n    cell_size: VecLike,\n    *,\n    cell: CellType = \"conv\"\n) -> AtomCell\n

    Create a perovskite structure \\(\\mathrm{ABX_3}\\).

    A is placed at the origin and B is placed at the cell center. cell_size determines whether a cubic, tetragonal, or orthorhombic structure is created. For instance, cell_size=3. returns a cubic structure, while cell_size=[3., 5.] returns a tetragonal structure a=3, c=5.

    All cell types are the same for perovskite, so the cell parameter has no effect.

    PARAMETER DESCRIPTION elems

    Elements to add (e.g. 'CaTiO' or ('Ca', 'Ti', 'O'))

    TYPE: ElemsLike

    cell_size

    Lattice parameters (e.g. 3.0 (cubic), [3.0, 5.0] (tetragonal), or [3.0, 4.0, 5.0] (orthorhombic)).

    TYPE: VecLike

    cell

    Cell type to return ('conv', 'prim', or 'ortho'). All are identical for this structure

    TYPE: CellType DEFAULT: 'conv'

    RETURNS DESCRIPTION AtomCell

    Periodic perovskite unit cell.

    Source code in atomlib/make/__init__.py
    def perovskite(elems: ElemsLike, cell_size: VecLike, *,\n               cell: CellType = 'conv') -> AtomCell:\n    \"\"\"\n    Create a perovskite structure $\\\\mathrm{ABX_3}$.\n\n    `A` is placed at the origin and `B` is placed at the cell center.\n    `cell_size` determines whether a cubic, tetragonal, or orthorhombic\n    structure is created. For instance, `cell_size=3.` returns a cubic\n    structure, while `cell_size=[3., 5.]` returns a tetragonal structure\n    `a=3`, `c=5`.\n\n    All cell types are the same for perovskite, so the `cell` parameter\n    has no effect.\n\n    Args:\n      elems: Elements to add (e.g. `'CaTiO'` or `('Ca', 'Ti', 'O')`)\n      cell_size: Lattice parameters (e.g. `3.0` (cubic), `[3.0, 5.0]`\n                 (tetragonal), or `[3.0, 4.0, 5.0]` (orthorhombic)).\n      cell: Cell type to return ('conv', 'prim', or 'ortho').\n            All are identical for this structure\n\n    Returns:\n      Periodic perovskite unit cell.\n    \"\"\"\n    elems = [t[0] for t in get_elems(elems)]\n\n    if len(elems) != 3:\n        raise ValueError(\"Expected three elements.\")\n\n    cell_size = numpy.atleast_1d(cell_size)\n    if cell_size.squeeze().ndim > 1:\n        raise ValueError(\"Expected a 1D vector\")\n    if len(cell_size) == 2:\n        # tetragonal shortcut\n        cell_size = numpy.array([cell_size[0], cell_size[0], cell_size[1]])\n\n    if cell not in ('prim', 'ortho', 'conv'):\n        raise ValueError(f\"Unknown cell type '{cell}'. Expected 'conv', 'prim', or 'ortho'.\")\n\n    xs = [0., 0.5, 0., 0.5, 0.5]\n    ys = [0., 0.5, 0.5, 0., 0.5]\n    zs = [0., 0.5, 0.5, 0.5, 0.]\n    elems = [elems[0], elems[1], *([elems[2]] * 3)]\n\n    atoms = Atoms(dict(x=xs, y=ys, z=zs, elem=elems))\n    return AtomCell(atoms, Cell.from_unit_cell(cell_size), frame='cell_frac')\n
    "},{"location":"api/make/#atomlib.make.random","title":"random","text":"
    random(\n    cell: Union[Cell, VecLike],\n    elems: ElemsLike,\n    density: float,\n    seed: Optional[object] = None,\n    **extra_cols: Any\n) -> AtomCell\n

    Make a random arrangement of atoms inside cell (Cell or cell_size vector).

    PARAMETER DESCRIPTION elems

    Elements to add (e.g. 'C', 6, or SiO2)

    TYPE: ElemsLike

    density

    Mean mass density to target (g/cm^3)

    TYPE: float

    seed

    Deterministic random seed to add (any object)

    TYPE: Optional[object] DEFAULT: None

    extra_cols

    Extra parameters to add to each atom

    TYPE: Any DEFAULT: {}

    RETURNS DESCRIPTION AtomCell

    A random arrangement of atoms

    Source code in atomlib/make/__init__.py
    def random(cell: t.Union[Cell, VecLike], elems: ElemsLike, density: float,\n           seed: t.Optional[object] = None, **extra_cols: t.Any) -> AtomCell:\n    \"\"\"\n    Make a random arrangement of atoms inside `cell`\n    ([`Cell`][atomlib.cell.Cell] or cell_size vector).\n\n    Args:\n      elems: Elements to add (e.g. `'C'`, `6`, or `SiO2`)\n      density: Mean mass density to target (g/cm^3)\n      seed: Deterministic random seed to add (any object)\n      extra_cols: Extra parameters to add to each atom\n\n    Returns:\n      A random arrangement of atoms\n    \"\"\"\n    if not isinstance(cell, Cell):\n        cell = Cell.from_unit_cell(cell, pbc=[True, True, True])\n\n    elems = get_elems(elems)\n    # normalize formula unit\n    total_num = sum(elem[1] for elem in elems)\n    elems = [(elem, num / total_num) for (elem, num) in elems]\n\n    total_mass = sum(get_mass(elem) * num for (elem, num) in elems)\n    # g/cm^3 / g/mol * 6.022e23/mol * 1e-24 cm^3/angstrom^3\n    total_number_density = density / total_mass * 0.60221408\n\n    rng = numpy.random.RandomState(proc_seed(seed, 'make.random'))\n    atoms = []\n\n    for (elem, frac) in elems:\n        n = int(numpy.round(numpy.prod(cell.box_size) * total_number_density * frac).astype(int))\n        pos = rng.uniform(0., 1., size=(3, n))\n\n        atoms.append(Atoms({\n            'x': pos[0], 'y': pos[1], 'z': pos[2],\n            'elem': [elem] * n,\n            **{k: [v] * n for (k, v) in extra_cols.items()}\n        }))\n\n    return AtomCell(Atoms.concat(atoms), cell=cell, frame='cell_box')\n
    "},{"location":"api/make/#atomlib.make.slab","title":"slab","text":"
    slab(\n    atoms: HasAtomCellT,\n    zone: VecLike = (0.0, 0.0, 1.0),\n    horz: VecLike = (1.0, 0.0, 0.0),\n    *,\n    max_n: int = 50,\n    tol: float = 0.001\n) -> HasAtomCellT\n

    Create an periodic orthogonal slab of the periodic cell atoms.

    zone in the original crystal will point along the +z-axis, and horz (minus the zone component) wil point along the +x-axis.

    Finds a periodic orthogonal slab with less than tol amount of strain, and no more than max_n cells on one side.

    PARAMETER DESCRIPTION atoms

    Input structure

    TYPE: HasAtomCellT

    zone

    Zone to align with the +z-axis

    TYPE: VecLike DEFAULT: (0.0, 0.0, 1.0)

    horz

    Zone to align with the +x-axis

    TYPE: VecLike DEFAULT: (1.0, 0.0, 0.0)

    max_n

    Maximum number of unit cells to search

    TYPE: int DEFAULT: 50

    tol

    Maximum strain tolerance

    TYPE: float DEFAULT: 0.001

    RETURNS DESCRIPTION HasAtomCellT

    A periodic, orthogonal cell

    Source code in atomlib/make/__init__.py
    def slab(atoms: HasAtomCellT, zone: VecLike = (0., 0., 1.), horz: VecLike = (1., 0., 0.), *,\n         max_n: int = 50, tol: float = 0.001) -> HasAtomCellT:\n    \"\"\"\n    Create an periodic orthogonal slab of the periodic cell `atoms`.\n\n    `zone` in the original crystal will point along the +z-axis,\n    and `horz` (minus the `zone` component) wil point along the +x-axis.\n\n    Finds a periodic orthogonal slab with less than `tol` amount of strain,\n    and no more than `max_n` cells on one side.\n\n    Args:\n      atoms: Input structure\n      zone: Zone to align with the +z-axis\n      horz: Zone to align with the +x-axis\n      max_n: Maximum number of unit cells to search\n      tol: Maximum strain tolerance\n\n    Returns:\n      A periodic, orthogonal cell\n    \"\"\"\n\n    # align `zone` with the z-axis, and `horz` with the x-axis\n    zone = reduce_vec(to_vec3(zone))  # ensure `zone` is a lattice vector\n    # TODO should this go from 'local' or 'ortho'?\n    cell_transform = atoms.get_transform('local', 'cell_frac').to_linear()\n    align_transform = LinearTransform3D.align(cell_transform @ zone, cell_transform @ horz)\n    transform = (align_transform @ cell_transform)\n    z = transform @ zone\n    numpy.testing.assert_allclose(z / numpy.linalg.norm(z), [0., 0., 1.], atol=1e-6)\n\n    # generate lattice points\n    lattice_coords = numpy.stack(numpy.meshgrid(*[numpy.arange(-max_n, max_n)]*3), axis=-1).reshape(-1, 3)\n    realspace_coords = transform @ lattice_coords\n    realspace_norm = numpy.linalg.norm(realspace_coords, axis=-1)\n\n    # sort coordinates from smallest to largest (TODO this method is slow)\n    sorting = realspace_norm.argsort()[1:]\n    realspace_norm = realspace_norm[sorting]\n    lattice_coords = lattice_coords[sorting]\n    realspace_coords = realspace_coords[sorting]\n    tols = realspace_norm * tol\n\n    # find lattice points which are acceptablly close to orthogonal\n    (x_close, y_close, z_close) = split_arr(numpy.abs(realspace_coords) < tols[:, None], axis=-1)\n\n    try:\n        x_i = numpy.argwhere(z_close & ~x_close & y_close)[0, 0],\n        y_i = numpy.argwhere(z_close & ~y_close & x_close)[0, 0],\n\n        logging.info(f\"x: {lattice_coords[x_i]} transforms to {realspace_coords[x_i]}\")\n        logging.info(f\"y: {lattice_coords[y_i]} transforms to {realspace_coords[y_i]}\")\n        logging.info(f\"z: {zone} transforms to {z}\")\n    except IndexError:\n        raise ValueError(\"Couldn't find a viable surface along zone {zone}\") from None\n\n    # orient vectors correctly\n    x = realspace_coords[x_i] * numpy.sign(realspace_coords[x_i][0])\n    y = realspace_coords[y_i] * numpy.sign(realspace_coords[y_i][1])\n\n    # repeat original lattice to cover orthogonal lattice\n    pts = transform.inverse() @ BBox3D.from_pts([numpy.zeros(3), x, y, z]).corners()\n    raw_atoms = atoms._repeat_to_contain(pts).get_atoms('local').transform(align_transform)\n    cell = Cell.from_ortho(LinearTransform3D(numpy.stack([x, y, z], axis=0)))\n\n    # strain cell to orthogonal (with atoms in the ``cell`` frame)\n    raw_atoms = raw_atoms.transform(cell.get_transform('cell', 'local'))\n    cell = cell.strain_orthogonal()\n    return atoms.with_cell(cell).with_atoms(raw_atoms, 'cell').crop_to_box()\n
    "},{"location":"api/make/#atomlib.make.stacking_sequence","title":"stacking_sequence","text":"
    stacking_sequence(\n    layer: AtomCell,\n    sequence: str,\n    shift_vector: VecLike = (1, 0, 0),\n    *,\n    n_layers: int = 3\n) -> AtomCell\n

    Create an arbitrary stacking sequence from a single layer layer.

    PARAMETER DESCRIPTION layer

    Layer to stack into a stacking sequence. Will be stacked along the c axis.

    TYPE: AtomCell

    sequence

    Stacking sequence. Each layer should be \"A\", \"B\", or \"C\" (in the common case where there are three layers). Example: \"ABCABC\".

    TYPE: str

    shift_vector

    Shift to apply, in fractional coordinates. The shift between each layer will be shift_vector/n_layers. Typically an integer value, to preserve periodicity. Defaults to [100].

    TYPE: VecLike DEFAULT: (1, 0, 0)

    n_layers

    Number of layers which corresponds to a shift of a complete lattice vector. Defaults to 3, the case for FCC and HCP.

    TYPE: int DEFAULT: 3

    RETURNS DESCRIPTION AtomCell

    An AtomCell containing the stacked structure.

    Source code in atomlib/make/__init__.py
    def stacking_sequence(layer: AtomCell, sequence: str, shift_vector: VecLike = (1, 0, 0), *,\n                      n_layers: int = 3) -> AtomCell:\n    \"\"\"\n    Create an arbitrary stacking sequence from a single layer `layer`.\n\n    Args:\n      layer: Layer to stack into a stacking sequence. Will be stacked along the c axis.\n      sequence: Stacking sequence. Each layer should be \"A\", \"B\", or \"C\" (in the common case\n                where there are three layers). Example: `\"ABCABC\"`.\n      shift_vector: Shift to apply, in fractional coordinates. The shift between each layer\n                    will be `shift_vector/n_layers`. Typically an integer value, to\n                    preserve periodicity. Defaults to `[100]`.\n      n_layers: Number of layers which corresponds to a shift of a complete lattice vector.\n                Defaults to `3`, the case for FCC and HCP.\n\n    Returns:\n     An [`AtomCell`][atomlib.atomcell.AtomCell] containing the stacked structure.\n    \"\"\"\n\n    layers = string.ascii_uppercase[:n_layers]\n\n    # TODO generalize this to arbitrary number of layers\n    sequence = sequence.upper()\n    if any(s not in layers for s in sequence):\n        raise ValueError(f\"Invalid sequence '{sequence}'. Expected values in '{layers}'\")\n\n    # c vector to shift along\n    c_vec = layer.to_ortho().transform_vec([0, 0, 1])\n    # new cell is original cell tiled by the number of layers\n    cell = layer.get_cell().repeat([1, 1, len(sequence)])\n\n    # convert shift_vector to local coordinates\n    shift_vector = layer.get_cell().get_transform('local', 'cell_frac').transform_vec(shift_vector)\n\n    atoms = layer.get_atoms('local')\n    return AtomCell(Atoms.concat(\n        # translate by the shift vector and translate to the correct layer\n        atoms.transform(AffineTransform3D.translate(shift_vector * (layers.find(c)) / n_layers + i*c_vec))\n        for (i, c) in enumerate(sequence)\n    ), cell).wrap()\n
    "},{"location":"api/mixins/","title":"atomlib.mixins","text":""},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin","title":"AtomsIOMixin","text":"

    Bases: _HasAtoms, ABC

    Mix-in to add IO methods to HasAtoms.

    All concrete subclasses of HasAtoms should also subclass this.

    Source code in atomlib/mixins.py
    class AtomsIOMixin(_HasAtoms, abc.ABC):\n    \"\"\"\n    Mix-in to add IO methods to [`HasAtoms`][atomlib.atoms.HasAtoms].\n\n    All concrete subclasses of [`HasAtoms`][atomlib.atoms.HasAtoms] should also subclass this.\n    \"\"\"\n\n    @t.overload\n    @classmethod\n    def read(cls: t.Type[HasAtomsT], path: FileOrPath, ty: FileType) -> HasAtomsT:\n        ...\n\n    @t.overload\n    @classmethod\n    def read(cls: t.Type[HasAtomsT], path: t.Union[str, Path, t.TextIO], ty: t.Literal[None] = None) -> HasAtomsT:\n        ...\n\n    @classmethod\n    def read(cls: t.Type[HasAtomsT], path: FileOrPath, ty: t.Optional[FileType] = None) -> HasAtomsT:\n        \"\"\"\n        Read a structure from a file.\n\n        Supported types can be found in the [io][atomlib.io] module.\n        If no `ty` is specified, it is inferred from the file's extension.\n        \"\"\"\n        from .io import read\n        return _cast_atoms(read(path, ty), cls)  # type: ignore\n\n    @classmethod\n    def read_cif(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CIF, CIFDataBlock], block: t.Union[int, str, None] = None) -> HasAtomsT:\n        \"\"\"\n        Read a structure from a CIF file.\n\n        If `block` is specified, read data from the given block of the CIF file (index or name).\n        \"\"\"\n        from .io import read_cif\n        return _cast_atoms(read_cif(f, block), cls)\n\n    @classmethod\n    def read_xyz(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XYZ]) -> HasAtomsT:\n        \"\"\"Read a structure from an XYZ file.\"\"\"\n        from .io import read_xyz\n        return _cast_atoms(read_xyz(f), cls)\n\n    @classmethod\n    def read_xsf(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XSF]) -> HasAtomsT:\n        \"\"\"Read a structure from an XSF file.\"\"\"\n        from .io import read_xsf\n        return _cast_atoms(read_xsf(f), cls)\n\n    @classmethod\n    def read_cfg(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CFG]) -> HasAtomsT:\n        \"\"\"Read a structure from a CFG file.\"\"\"\n        from .io import read_cfg\n        return _cast_atoms(read_cfg(f), cls)\n\n    @classmethod\n    def read_lmp(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, LMP], type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> HasAtomsT:\n        \"\"\"Read a structure from a LAAMPS data file.\"\"\"\n        from .io import read_lmp\n        return _cast_atoms(read_lmp(f, type_map=type_map), cls)\n\n    def write_cif(self, f: FileOrPath):\n        from .io import write_cif\n        write_cif(self, f)\n\n    def write_xyz(self, f: FileOrPath, fmt: XYZFormat = 'exyz'):\n        from .io import write_xyz\n        write_xyz(self, f, fmt)\n\n    def write_xsf(self, f: FileOrPath):\n        from .io import write_xsf\n        write_xsf(self, f)\n\n    def write_cfg(self, f: FileOrPath):\n        from .io import write_cfg\n        write_cfg(self, f)\n\n    def write_lmp(self, f: FileOrPath):\n        from .io import write_lmp\n        write_lmp(self, f)\n\n    @t.overload\n    def write(self, path: FileOrPath, ty: FileType):\n        ...\n\n    @t.overload\n    def write(self, path: t.Union[str, Path, t.TextIO], ty: t.Literal[None] = None):\n        ...\n\n    def write(self, path: FileOrPath, ty: t.Optional[FileType] = None):\n        \"\"\"\n        Write this structure to a file.\n\n        A file type may be specified using `ty`.\n        If no `ty` is specified, it is inferred from the path's extension.\n        \"\"\"\n        from .io import write\n        write(self, path, ty)  # type: ignore\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.read","title":"read classmethod","text":"
    read(path: FileOrPath, ty: FileType) -> HasAtomsT\n
    read(\n    path: Union[str, Path, TextIO], ty: Literal[None] = None\n) -> HasAtomsT\n
    read(\n    path: FileOrPath, ty: Optional[FileType] = None\n) -> HasAtomsT\n

    Read a structure from a file.

    Supported types can be found in the io module. If no ty is specified, it is inferred from the file's extension.

    Source code in atomlib/mixins.py
    @classmethod\ndef read(cls: t.Type[HasAtomsT], path: FileOrPath, ty: t.Optional[FileType] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a file.\n\n    Supported types can be found in the [io][atomlib.io] module.\n    If no `ty` is specified, it is inferred from the file's extension.\n    \"\"\"\n    from .io import read\n    return _cast_atoms(read(path, ty), cls)  # type: ignore\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.read_cif","title":"read_cif classmethod","text":"
    read_cif(\n    f: Union[FileOrPath, CIF, CIFDataBlock],\n    block: Union[int, str, None] = None,\n) -> HasAtomsT\n

    Read a structure from a CIF file.

    If block is specified, read data from the given block of the CIF file (index or name).

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cif(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CIF, CIFDataBlock], block: t.Union[int, str, None] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a CIF file.\n\n    If `block` is specified, read data from the given block of the CIF file (index or name).\n    \"\"\"\n    from .io import read_cif\n    return _cast_atoms(read_cif(f, block), cls)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.read_xyz","title":"read_xyz classmethod","text":"
    read_xyz(f: Union[FileOrPath, XYZ]) -> HasAtomsT\n

    Read a structure from an XYZ file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xyz(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XYZ]) -> HasAtomsT:\n    \"\"\"Read a structure from an XYZ file.\"\"\"\n    from .io import read_xyz\n    return _cast_atoms(read_xyz(f), cls)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.read_xsf","title":"read_xsf classmethod","text":"
    read_xsf(f: Union[FileOrPath, XSF]) -> HasAtomsT\n

    Read a structure from an XSF file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xsf(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XSF]) -> HasAtomsT:\n    \"\"\"Read a structure from an XSF file.\"\"\"\n    from .io import read_xsf\n    return _cast_atoms(read_xsf(f), cls)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.read_cfg","title":"read_cfg classmethod","text":"
    read_cfg(f: Union[FileOrPath, CFG]) -> HasAtomsT\n

    Read a structure from a CFG file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cfg(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CFG]) -> HasAtomsT:\n    \"\"\"Read a structure from a CFG file.\"\"\"\n    from .io import read_cfg\n    return _cast_atoms(read_cfg(f), cls)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.read_lmp","title":"read_lmp classmethod","text":"
    read_lmp(\n    f: Union[FileOrPath, LMP],\n    type_map: Optional[Dict[int, Union[str, int]]] = None,\n) -> HasAtomsT\n

    Read a structure from a LAAMPS data file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_lmp(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, LMP], type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> HasAtomsT:\n    \"\"\"Read a structure from a LAAMPS data file.\"\"\"\n    from .io import read_lmp\n    return _cast_atoms(read_lmp(f, type_map=type_map), cls)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.write_cif","title":"write_cif","text":"
    write_cif(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cif(self, f: FileOrPath):\n    from .io import write_cif\n    write_cif(self, f)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.write_xyz","title":"write_xyz","text":"
    write_xyz(f: FileOrPath, fmt: XYZFormat = 'exyz')\n
    Source code in atomlib/mixins.py
    def write_xyz(self, f: FileOrPath, fmt: XYZFormat = 'exyz'):\n    from .io import write_xyz\n    write_xyz(self, f, fmt)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.write_xsf","title":"write_xsf","text":"
    write_xsf(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_xsf(self, f: FileOrPath):\n    from .io import write_xsf\n    write_xsf(self, f)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.write_cfg","title":"write_cfg","text":"
    write_cfg(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cfg(self, f: FileOrPath):\n    from .io import write_cfg\n    write_cfg(self, f)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.write_lmp","title":"write_lmp","text":"
    write_lmp(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_lmp(self, f: FileOrPath):\n    from .io import write_lmp\n    write_lmp(self, f)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomsIOMixin.write","title":"write","text":"
    write(path: FileOrPath, ty: FileType)\n
    write(\n    path: Union[str, Path, TextIO], ty: Literal[None] = None\n)\n
    write(path: FileOrPath, ty: Optional[FileType] = None)\n

    Write this structure to a file.

    A file type may be specified using ty. If no ty is specified, it is inferred from the path's extension.

    Source code in atomlib/mixins.py
    def write(self, path: FileOrPath, ty: t.Optional[FileType] = None):\n    \"\"\"\n    Write this structure to a file.\n\n    A file type may be specified using `ty`.\n    If no `ty` is specified, it is inferred from the path's extension.\n    \"\"\"\n    from .io import write\n    write(self, path, ty)  # type: ignore\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin","title":"AtomCellIOMixin","text":"

    Bases: _HasAtomCell, AtomsIOMixin

    Mix-in to add IO methods to HasAtomCell.

    All concrete subclasses of HasAtomCell should also subclass this.

    Source code in atomlib/mixins.py
    class AtomCellIOMixin(_HasAtomCell, AtomsIOMixin):\n    \"\"\"\n    Mix-in to add IO methods to [`HasAtomCell`][atomlib.atomcell.HasAtomCell].\n\n    All concrete subclasses of [`HasAtomCell`][atomlib.atomcell.HasAtomCell] should also subclass this.\n    \"\"\"\n\n    def write_mslice(self, f: BinaryFileOrPath, template: t.Optional[MSliceFile] = None, *,\n                 slice_thickness: t.Optional[float] = None,  # angstrom\n                 scan_points: t.Optional[ArrayLike] = None,\n                 scan_extent: t.Optional[ArrayLike] = None,\n                 noise_sigma: t.Optional[float] = None,  # angstrom\n                 conv_angle: t.Optional[float] = None,  # mrad\n                 energy: t.Optional[float] = None,  # keV\n                 defocus: t.Optional[float] = None,  # angstrom\n                 tilt: t.Optional[t.Tuple[float, float]] = None,  # (mrad, mrad)\n                 tds: t.Optional[bool] = None,\n                 n_cells: t.Optional[ArrayLike] = None):\n        \"\"\"\n        Write a structure to an mslice file.\n\n        `template` may be a file, path, or `ElementTree` containing an existing mslice file.\n        Its structure will be modified to make the final output. If not specified, a default\n        template will be used.\n\n        Additional options modify simulation properties. If an option is not specified, the\n        template's properties are used.\n        \"\"\"\n        from .io import write_mslice\n        return write_mslice(self, f, template, slice_thickness=slice_thickness,\n                            scan_points=scan_points, scan_extent=scan_extent,\n                            conv_angle=conv_angle, energy=energy, defocus=defocus,\n                            noise_sigma=noise_sigma, tilt=tilt, tds=tds, n_cells=n_cells)\n\n    def write_qe(self, f: FileOrPath, pseudo: t.Optional[t.Mapping[str, str]] = None):\n        \"\"\"\n        Write a structure to a Quantum Espresso pw.x file.\n\n        Args:\n          f: File or path to write to\n          pseudo: Mapping from atom symbol\n        \"\"\"\n        from .io import write_qe\n        write_qe(self, f, pseudo)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.read","title":"read classmethod","text":"
    read(path: FileOrPath, ty: FileType) -> HasAtomsT\n
    read(\n    path: Union[str, Path, TextIO], ty: Literal[None] = None\n) -> HasAtomsT\n
    read(\n    path: FileOrPath, ty: Optional[FileType] = None\n) -> HasAtomsT\n

    Read a structure from a file.

    Supported types can be found in the io module. If no ty is specified, it is inferred from the file's extension.

    Source code in atomlib/mixins.py
    @classmethod\ndef read(cls: t.Type[HasAtomsT], path: FileOrPath, ty: t.Optional[FileType] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a file.\n\n    Supported types can be found in the [io][atomlib.io] module.\n    If no `ty` is specified, it is inferred from the file's extension.\n    \"\"\"\n    from .io import read\n    return _cast_atoms(read(path, ty), cls)  # type: ignore\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.read_cif","title":"read_cif classmethod","text":"
    read_cif(\n    f: Union[FileOrPath, CIF, CIFDataBlock],\n    block: Union[int, str, None] = None,\n) -> HasAtomsT\n

    Read a structure from a CIF file.

    If block is specified, read data from the given block of the CIF file (index or name).

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cif(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CIF, CIFDataBlock], block: t.Union[int, str, None] = None) -> HasAtomsT:\n    \"\"\"\n    Read a structure from a CIF file.\n\n    If `block` is specified, read data from the given block of the CIF file (index or name).\n    \"\"\"\n    from .io import read_cif\n    return _cast_atoms(read_cif(f, block), cls)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.read_xyz","title":"read_xyz classmethod","text":"
    read_xyz(f: Union[FileOrPath, XYZ]) -> HasAtomsT\n

    Read a structure from an XYZ file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xyz(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XYZ]) -> HasAtomsT:\n    \"\"\"Read a structure from an XYZ file.\"\"\"\n    from .io import read_xyz\n    return _cast_atoms(read_xyz(f), cls)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.read_xsf","title":"read_xsf classmethod","text":"
    read_xsf(f: Union[FileOrPath, XSF]) -> HasAtomsT\n

    Read a structure from an XSF file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_xsf(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, XSF]) -> HasAtomsT:\n    \"\"\"Read a structure from an XSF file.\"\"\"\n    from .io import read_xsf\n    return _cast_atoms(read_xsf(f), cls)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.read_cfg","title":"read_cfg classmethod","text":"
    read_cfg(f: Union[FileOrPath, CFG]) -> HasAtomsT\n

    Read a structure from a CFG file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_cfg(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, CFG]) -> HasAtomsT:\n    \"\"\"Read a structure from a CFG file.\"\"\"\n    from .io import read_cfg\n    return _cast_atoms(read_cfg(f), cls)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.read_lmp","title":"read_lmp classmethod","text":"
    read_lmp(\n    f: Union[FileOrPath, LMP],\n    type_map: Optional[Dict[int, Union[str, int]]] = None,\n) -> HasAtomsT\n

    Read a structure from a LAAMPS data file.

    Source code in atomlib/mixins.py
    @classmethod\ndef read_lmp(cls: t.Type[HasAtomsT], f: t.Union[FileOrPath, LMP], type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> HasAtomsT:\n    \"\"\"Read a structure from a LAAMPS data file.\"\"\"\n    from .io import read_lmp\n    return _cast_atoms(read_lmp(f, type_map=type_map), cls)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.write_cif","title":"write_cif","text":"
    write_cif(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cif(self, f: FileOrPath):\n    from .io import write_cif\n    write_cif(self, f)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.write_xyz","title":"write_xyz","text":"
    write_xyz(f: FileOrPath, fmt: XYZFormat = 'exyz')\n
    Source code in atomlib/mixins.py
    def write_xyz(self, f: FileOrPath, fmt: XYZFormat = 'exyz'):\n    from .io import write_xyz\n    write_xyz(self, f, fmt)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.write_xsf","title":"write_xsf","text":"
    write_xsf(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_xsf(self, f: FileOrPath):\n    from .io import write_xsf\n    write_xsf(self, f)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.write_cfg","title":"write_cfg","text":"
    write_cfg(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_cfg(self, f: FileOrPath):\n    from .io import write_cfg\n    write_cfg(self, f)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.write_lmp","title":"write_lmp","text":"
    write_lmp(f: FileOrPath)\n
    Source code in atomlib/mixins.py
    def write_lmp(self, f: FileOrPath):\n    from .io import write_lmp\n    write_lmp(self, f)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.write","title":"write","text":"
    write(path: FileOrPath, ty: FileType)\n
    write(\n    path: Union[str, Path, TextIO], ty: Literal[None] = None\n)\n
    write(path: FileOrPath, ty: Optional[FileType] = None)\n

    Write this structure to a file.

    A file type may be specified using ty. If no ty is specified, it is inferred from the path's extension.

    Source code in atomlib/mixins.py
    def write(self, path: FileOrPath, ty: t.Optional[FileType] = None):\n    \"\"\"\n    Write this structure to a file.\n\n    A file type may be specified using `ty`.\n    If no `ty` is specified, it is inferred from the path's extension.\n    \"\"\"\n    from .io import write\n    write(self, path, ty)  # type: ignore\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.write_mslice","title":"write_mslice","text":"
    write_mslice(\n    f: BinaryFileOrPath,\n    template: Optional[MSliceFile] = None,\n    *,\n    slice_thickness: Optional[float] = None,\n    scan_points: Optional[ArrayLike] = None,\n    scan_extent: Optional[ArrayLike] = None,\n    noise_sigma: Optional[float] = None,\n    conv_angle: Optional[float] = None,\n    energy: Optional[float] = None,\n    defocus: Optional[float] = None,\n    tilt: Optional[Tuple[float, float]] = None,\n    tds: Optional[bool] = None,\n    n_cells: Optional[ArrayLike] = None\n)\n

    Write a structure to an mslice file.

    template may be a file, path, or ElementTree containing an existing mslice file. Its structure will be modified to make the final output. If not specified, a default template will be used.

    Additional options modify simulation properties. If an option is not specified, the template's properties are used.

    Source code in atomlib/mixins.py
    def write_mslice(self, f: BinaryFileOrPath, template: t.Optional[MSliceFile] = None, *,\n             slice_thickness: t.Optional[float] = None,  # angstrom\n             scan_points: t.Optional[ArrayLike] = None,\n             scan_extent: t.Optional[ArrayLike] = None,\n             noise_sigma: t.Optional[float] = None,  # angstrom\n             conv_angle: t.Optional[float] = None,  # mrad\n             energy: t.Optional[float] = None,  # keV\n             defocus: t.Optional[float] = None,  # angstrom\n             tilt: t.Optional[t.Tuple[float, float]] = None,  # (mrad, mrad)\n             tds: t.Optional[bool] = None,\n             n_cells: t.Optional[ArrayLike] = None):\n    \"\"\"\n    Write a structure to an mslice file.\n\n    `template` may be a file, path, or `ElementTree` containing an existing mslice file.\n    Its structure will be modified to make the final output. If not specified, a default\n    template will be used.\n\n    Additional options modify simulation properties. If an option is not specified, the\n    template's properties are used.\n    \"\"\"\n    from .io import write_mslice\n    return write_mslice(self, f, template, slice_thickness=slice_thickness,\n                        scan_points=scan_points, scan_extent=scan_extent,\n                        conv_angle=conv_angle, energy=energy, defocus=defocus,\n                        noise_sigma=noise_sigma, tilt=tilt, tds=tds, n_cells=n_cells)\n
    "},{"location":"api/mixins/#atomlib.mixins.AtomCellIOMixin.write_qe","title":"write_qe","text":"
    write_qe(\n    f: FileOrPath,\n    pseudo: Optional[Mapping[str, str]] = None,\n)\n

    Write a structure to a Quantum Espresso pw.x file.

    PARAMETER DESCRIPTION f

    File or path to write to

    TYPE: FileOrPath

    pseudo

    Mapping from atom symbol

    TYPE: Optional[Mapping[str, str]] DEFAULT: None

    Source code in atomlib/mixins.py
    def write_qe(self, f: FileOrPath, pseudo: t.Optional[t.Mapping[str, str]] = None):\n    \"\"\"\n    Write a structure to a Quantum Espresso pw.x file.\n\n    Args:\n      f: File or path to write to\n      pseudo: Mapping from atom symbol\n    \"\"\"\n    from .io import write_qe\n    write_qe(self, f, pseudo)\n
    "},{"location":"api/testing/","title":"atomlib.testing","text":""},{"location":"api/testing/#atomlib.testing.CallableT","title":"CallableT module-attribute","text":"
    CallableT = TypeVar('CallableT', bound=Callable)\n
    "},{"location":"api/testing/#atomlib.testing.OUTPUT_PATH","title":"OUTPUT_PATH module-attribute","text":"
    OUTPUT_PATH = parents[2] / 'tests/baseline_files'\n
    "},{"location":"api/testing/#atomlib.testing.INPUT_PATH","title":"INPUT_PATH module-attribute","text":"
    INPUT_PATH = parents[2] / 'tests/input_files'\n
    "},{"location":"api/testing/#atomlib.testing.assert_files_equal","title":"assert_files_equal","text":"
    assert_files_equal(\n    expected_path: Union[str, Path],\n    actual_path: Union[str, Path],\n)\n
    Source code in atomlib/testing/__init__.py
    def assert_files_equal(expected_path: t.Union[str, Path], actual_path: t.Union[str, Path]):\n    with open(OUTPUT_PATH / expected_path, 'r') as f:\n        expected = re.sub('\\r\\n', '\\n', f.read())\n    with open(actual_path, 'r') as f:\n        actual = re.sub('\\r\\n', '\\n', f.read())\n\n    assert expected == actual\n
    "},{"location":"api/testing/#atomlib.testing.check_equals_file","title":"check_equals_file","text":"
    check_equals_file(\n    name: Union[str, Path], *, skip_lines: int = 0\n) -> Callable[[Callable[..., Any]], Callable[..., None]]\n
    Source code in atomlib/testing/__init__.py
    def check_equals_file(name: t.Union[str, Path], *, skip_lines: int = 0) -> t.Callable[[t.Callable[..., t.Any]], t.Callable[..., None]]:\n    def decorator(f: t.Callable[..., str]):\n        @pytest.mark.expected_filename(name)\n        def wrapper(expected_contents_text: str, *args, **kwargs):  # type: ignore\n            buf = StringIO()\n            f(buf, *args, **kwargs)\n            if skip_lines > 0:\n                lhs = \"\".join(buf.getvalue().splitlines(True)[skip_lines:])\n                rhs = \"\".join(expected_contents_text.splitlines(True)[skip_lines:])\n                assert lhs == rhs\n            else:\n                assert buf.getvalue() == expected_contents_text\n\n        return _wrap_pytest(wrapper, f, lambda params: [inspect.Parameter('expected_contents_text', inspect.Parameter.POSITIONAL_OR_KEYWORD), *params[1:]])\n\n    return decorator\n
    "},{"location":"api/testing/#atomlib.testing.check_equals_binary_file","title":"check_equals_binary_file","text":"
    check_equals_binary_file(\n    name: Union[str, Path]\n) -> Callable[[Callable[..., Any]], Callable[..., None]]\n
    Source code in atomlib/testing/__init__.py
    def check_equals_binary_file(name: t.Union[str, Path]) -> t.Callable[[t.Callable[..., t.Any]], t.Callable[..., None]]:\n    def decorator(f: t.Callable[..., str]):\n        @pytest.mark.expected_filename(name)\n        def wrapper(expected_contents_binary: bytes, *args, **kwargs):  # type: ignore\n            buf = BytesIO()\n            f(buf, *args, **kwargs)\n            assert buf.getvalue() == expected_contents_binary\n\n        return _wrap_pytest(wrapper, f, lambda params: [inspect.Parameter('expected_contents_binary', inspect.Parameter.POSITIONAL_OR_KEYWORD), *params[1:]])\n\n    return decorator\n
    "},{"location":"api/testing/#atomlib.testing.assert_structure_equal","title":"assert_structure_equal","text":"
    assert_structure_equal(\n    expected_path: Union[str, Path],\n    actual: Union[str, Path, AtomsIOMixin],\n)\n
    Source code in atomlib/testing/__init__.py
    def assert_structure_equal(expected_path: t.Union[str, Path], actual: t.Union[str, Path, AtomsIOMixin]):\n    from atomlib.io import read\n\n    expected = read(OUTPUT_PATH / expected_path)\n\n    try:\n        if isinstance(actual, (str, Path)):\n            actual = t.cast('AtomsIOMixin', read(actual))\n    except Exception:\n        print(\"Failed to load structure under test.\")\n        raise\n\n    try:\n        if hasattr(actual, 'assert_equal'):\n            actual.assert_equal(expected)  # type: ignore\n        else:\n            assert actual == expected\n    except AssertionError:\n        try:\n            actual_path = Path(expected_path).with_stem(Path(expected_path).stem + '_actual').name\n            print(f\"Saving result structure to '{actual_path}'\")\n            actual.write(OUTPUT_PATH / actual_path)\n        except Exception:\n            print(\"Failed to save result structure.\")\n        raise\n
    "},{"location":"api/testing/#atomlib.testing.check_equals_structure","title":"check_equals_structure","text":"
    check_equals_structure(\n    name: Union[str, Path]\n) -> Callable[\n    [Callable[..., AtomsIOMixin]], Callable[..., None]\n]\n

    Test that the wrapped function returns the same structure as contained in name.

    Source code in atomlib/testing/__init__.py
    def check_equals_structure(name: t.Union[str, Path]) -> t.Callable[[t.Callable[..., AtomsIOMixin]], t.Callable[..., None]]:\n    \"\"\"Test that the wrapped function returns the same structure as contained in `name`.\"\"\"\n    def decorator(f: t.Callable[..., 'AtomsIOMixin']):\n        @pytest.mark.expected_filename(name)\n        def wrapper(expected_structure: 'HasAtoms', *args, **kwargs):  # type: ignore\n            result = f(*args, **kwargs)\n            try:\n                if hasattr(result, 'assert_equal'):\n                    result.assert_equal(expected_structure)  # type: ignore\n                else:\n                    assert result == expected_structure\n            except AssertionError:\n                try:\n                    actual_path = Path(name).with_stem(Path(name).stem + '_actual').name\n                    print(f\"Saving result structure to '{actual_path}'\")\n                    result.write(OUTPUT_PATH / actual_path)\n                except Exception:\n                    print(\"Failed to save result structure.\")\n                raise\n\n        return _wrap_pytest(wrapper, f, lambda params: [inspect.Parameter('expected_structure', inspect.Parameter.POSITIONAL_OR_KEYWORD), *params])\n\n    return decorator\n
    "},{"location":"api/testing/#atomlib.testing.check_parse_structure","title":"check_parse_structure","text":"
    check_parse_structure(\n    name: Union[str, Path]\n) -> Callable[\n    [Callable[..., HasAtoms]], Callable[..., None]\n]\n

    Test that name parses to the same structure as given in the function body.

    Source code in atomlib/testing/__init__.py
    def check_parse_structure(name: t.Union[str, Path]) -> t.Callable[[t.Callable[..., HasAtoms]], t.Callable[..., None]]:\n    \"\"\"Test that `name` parses to the same structure as given in the function body.\"\"\"\n    def decorator(f: t.Callable[..., 'HasAtoms']):\n        def wrapper(*args, **kwargs):  # type: ignore\n            expected = f(*args, **kwargs)\n\n            from atomlib.io import read\n            result = read(INPUT_PATH / name)\n\n            if hasattr(result, 'assert_equal'):\n                result.assert_equal(expected)  # type: ignore\n            else:\n                assert result == expected\n\n        return _wrap_pytest(wrapper, f)\n    return decorator\n
    "},{"location":"api/testing/#atomlib.testing.check_figure_draw","title":"check_figure_draw","text":"
    check_figure_draw(\n    name: Union[str, Path, Sequence[Union[str, Path]]],\n    savefig_kwarg: Optional[Dict[str, Any]] = None,\n) -> Callable[[Callable[..., None]], Callable[..., None]]\n

    Test that the wrapped function draws an identical figure to name in baseline_images.

    Source code in atomlib/testing/__init__.py
    def check_figure_draw(name: t.Union[str, Path, t.Sequence[t.Union[str, Path]]],\n                      savefig_kwarg: t.Optional[t.Dict[str, t.Any]] = None) -> t.Callable[[t.Callable[..., None]], t.Callable[..., None]]:\n    \"\"\"Test that the wrapped function draws an identical figure to `name` in `baseline_images`.\"\"\"\n\n    if isinstance(name, (str, Path)):\n        names = (str(name),)\n    else:\n        names = tuple(map(str, name))\n\n    def decorator(f: t.Callable[..., None]):\n        from matplotlib.testing.decorators import image_comparison\n        return image_comparison(list(names), savefig_kwarg=savefig_kwarg)(f)\n\n    return decorator\n
    "},{"location":"api/transform/","title":"atomlib.transform","text":""},{"location":"api/transform/#atomlib.transform.IntoTransform3D","title":"IntoTransform3D module-attribute","text":"
    IntoTransform3D: TypeAlias = Union[\n    \"Transform3D\",\n    Callable[[NDArray[floating]], ndarray],\n    ndarray,\n]\n

    Type which is coercable into a Transform3D.

    Includes transformations, numpy arrays (3x3 or 4x4), and functions (which should take a Nx3 ndarray and return an ndarray of the same shape).

    "},{"location":"api/transform/#atomlib.transform.Transform3D","title":"Transform3D","text":"

    Bases: ABC

    Arbitrary 3D transformation. Superclass of all 3D transformation types.

    Transformations can be composed: t3 = t1 @ t2, or applied to points: transformed = transform @ points using the @ operator.

    Alternatively, points can be transformed using functional notation: transformed = transform(points).

    Source code in atomlib/transform.py
    class Transform3D(ABC):\n    \"\"\"\n    Arbitrary 3D transformation. Superclass of all 3D transformation types.\n\n    Transformations can be composed: `t3 = t1 @ t2`, or applied\n    to points: `transformed = transform @ points` using the `@`\n    operator.\n\n    Alternatively, points can be transformed using functional notation:\n    `transformed = transform(points)`.\n    \"\"\"\n\n    @staticmethod\n    @abstractmethod\n    def identity() -> Transform3D:\n        \"\"\"Return an identity transformation.\"\"\"\n        ...\n\n    @staticmethod\n    def make(data: IntoTransform3D) -> Transform3D:\n        \"\"\"Make a transformation from a function or numpy array (3x3 or 4x4).\"\"\"\n        if isinstance(data, Transform3D):\n            return data\n        if not isinstance(data, numpy.ndarray) and hasattr(data, '__call__'):\n            return FuncTransform3D(data)\n        data = numpy.array(data)\n        if data.shape == (3, 3):\n            return LinearTransform3D(data)\n        if data.shape == (4, 4):\n            return AffineTransform3D(data)\n        raise ValueError(f\"Transform3D of invalid shape {data.shape}\")\n\n    @abstractmethod\n    def compose(self, other: Transform3D) -> Transform3D:\n        \"\"\"Compose this transformation with another.\"\"\"\n        ...\n\n    @t.overload\n    @abstractmethod\n    def transform(self, points: BBox3D) -> BBox3D:\n        ...\n\n    @t.overload\n    @abstractmethod\n    def transform(self, points: ArrayLike) -> NDArray[numpy.floating]:\n        ...\n\n    @abstractmethod\n    def transform(self, points: Pts3DLike) -> t.Union[BBox3D, NDArray[numpy.floating]]:\n        \"\"\"Transform points according to the given transformation.\"\"\"\n        ...\n\n    __call__ = transform\n\n    def transform_vec(self, vecs: ArrayLike) -> NDArray[numpy.floating]:\n        \"\"\"Transform vector quantities. This excludes translation, as would be expected when transforming vectors.\"\"\"\n        a = numpy.atleast_1d(vecs)\n        return self.transform(a) - self.transform(numpy.zeros_like(a))\n\n    @t.overload\n    def __matmul__(self, other: Transform3D) -> Transform3D:\n        ...\n\n    @t.overload\n    def __matmul__(self, other: BBox3D) -> BBox3D:\n        ...\n\n    @t.overload\n    def __matmul__(self, other: ArrayLike) -> NDArray[numpy.floating]:\n        ...\n\n    def __matmul__(self, other: t.Union[Transform3D, Pts3DLike]) -> t.Union[Transform3D, BBox3D, NDArray[numpy.floating]]:\n        \"\"\"Compose this transformation, or apply it to a given set of points.\"\"\"\n        if isinstance(other, Transform3D):\n            return other.compose(self)\n        return self.transform(other)\n\n    def __rmatmul__(self, other: t.Any):\n        raise ValueError(\"Transform must be applied to points, not the other way around.\")\n
    "},{"location":"api/transform/#atomlib.transform.Transform3D.identity","title":"identity abstractmethod staticmethod","text":"
    identity() -> Transform3D\n

    Return an identity transformation.

    Source code in atomlib/transform.py
    @staticmethod\n@abstractmethod\ndef identity() -> Transform3D:\n    \"\"\"Return an identity transformation.\"\"\"\n    ...\n
    "},{"location":"api/transform/#atomlib.transform.Transform3D.make","title":"make staticmethod","text":"
    make(data: IntoTransform3D) -> Transform3D\n

    Make a transformation from a function or numpy array (3x3 or 4x4).

    Source code in atomlib/transform.py
    @staticmethod\ndef make(data: IntoTransform3D) -> Transform3D:\n    \"\"\"Make a transformation from a function or numpy array (3x3 or 4x4).\"\"\"\n    if isinstance(data, Transform3D):\n        return data\n    if not isinstance(data, numpy.ndarray) and hasattr(data, '__call__'):\n        return FuncTransform3D(data)\n    data = numpy.array(data)\n    if data.shape == (3, 3):\n        return LinearTransform3D(data)\n    if data.shape == (4, 4):\n        return AffineTransform3D(data)\n    raise ValueError(f\"Transform3D of invalid shape {data.shape}\")\n
    "},{"location":"api/transform/#atomlib.transform.Transform3D.compose","title":"compose abstractmethod","text":"
    compose(other: Transform3D) -> Transform3D\n

    Compose this transformation with another.

    Source code in atomlib/transform.py
    @abstractmethod\ndef compose(self, other: Transform3D) -> Transform3D:\n    \"\"\"Compose this transformation with another.\"\"\"\n    ...\n
    "},{"location":"api/transform/#atomlib.transform.Transform3D.transform","title":"transform abstractmethod","text":"
    transform(points: BBox3D) -> BBox3D\n
    transform(points: ArrayLike) -> NDArray[floating]\n
    transform(\n    points: Pts3DLike,\n) -> Union[BBox3D, NDArray[floating]]\n

    Transform points according to the given transformation.

    Source code in atomlib/transform.py
    @abstractmethod\ndef transform(self, points: Pts3DLike) -> t.Union[BBox3D, NDArray[numpy.floating]]:\n    \"\"\"Transform points according to the given transformation.\"\"\"\n    ...\n
    "},{"location":"api/transform/#atomlib.transform.Transform3D.transform_vec","title":"transform_vec","text":"
    transform_vec(vecs: ArrayLike) -> NDArray[floating]\n

    Transform vector quantities. This excludes translation, as would be expected when transforming vectors.

    Source code in atomlib/transform.py
    def transform_vec(self, vecs: ArrayLike) -> NDArray[numpy.floating]:\n    \"\"\"Transform vector quantities. This excludes translation, as would be expected when transforming vectors.\"\"\"\n    a = numpy.atleast_1d(vecs)\n    return self.transform(a) - self.transform(numpy.zeros_like(a))\n
    "},{"location":"api/transform/#atomlib.transform.FuncTransform3D","title":"FuncTransform3D","text":"

    Bases: Transform3D

    Transformation which applies a function to the given points.

    Source code in atomlib/transform.py
    class FuncTransform3D(Transform3D):\n    \"\"\"Transformation which applies a function to the given points.\"\"\"\n\n    def __init__(self, f: t.Callable[[numpy.ndarray], numpy.ndarray]):\n        self.f: t.Callable[[numpy.ndarray], numpy.ndarray] = f\n        \"\"\"Wrapped function\"\"\"\n\n    @staticmethod\n    def identity() -> FuncTransform3D:\n        \"\"\"Return an identity transformation.\"\"\"\n        return FuncTransform3D(lambda pts: pts)\n\n    @t.overload\n    def transform(self, points: BBox3D) -> BBox3D:\n        ...\n\n    @t.overload\n    def transform(self, points: ArrayLike) -> NDArray[numpy.floating]:\n        ...\n\n    def transform(self, points: Pts3DLike) -> t.Union[BBox3D, NDArray[numpy.floating]]:\n        \"\"\"Transform points according to the given transformation.\"\"\"\n        if isinstance(points, BBox3D):\n            return points.from_pts(self.transform(points.corners()))\n\n        return self.f(numpy.atleast_1d(points))\n\n    def compose(self, other: Transform3D) -> FuncTransform3D:\n        \"\"\"Compose this transformation with another.\"\"\"\n        return FuncTransform3D(lambda pts: other.transform(self.f(pts)))\n\n    def _rcompose(self, after: Transform3D) -> FuncTransform3D:\n        return FuncTransform3D(lambda pts: self.f(after.transform(pts)))\n\n    __call__ = transform\n
    "},{"location":"api/transform/#atomlib.transform.FuncTransform3D.f","title":"f instance-attribute","text":"
    f: Callable[[ndarray], ndarray] = f\n

    Wrapped function

    "},{"location":"api/transform/#atomlib.transform.FuncTransform3D.make","title":"make staticmethod","text":"
    make(data: IntoTransform3D) -> Transform3D\n

    Make a transformation from a function or numpy array (3x3 or 4x4).

    Source code in atomlib/transform.py
    @staticmethod\ndef make(data: IntoTransform3D) -> Transform3D:\n    \"\"\"Make a transformation from a function or numpy array (3x3 or 4x4).\"\"\"\n    if isinstance(data, Transform3D):\n        return data\n    if not isinstance(data, numpy.ndarray) and hasattr(data, '__call__'):\n        return FuncTransform3D(data)\n    data = numpy.array(data)\n    if data.shape == (3, 3):\n        return LinearTransform3D(data)\n    if data.shape == (4, 4):\n        return AffineTransform3D(data)\n    raise ValueError(f\"Transform3D of invalid shape {data.shape}\")\n
    "},{"location":"api/transform/#atomlib.transform.FuncTransform3D.transform_vec","title":"transform_vec","text":"
    transform_vec(vecs: ArrayLike) -> NDArray[floating]\n

    Transform vector quantities. This excludes translation, as would be expected when transforming vectors.

    Source code in atomlib/transform.py
    def transform_vec(self, vecs: ArrayLike) -> NDArray[numpy.floating]:\n    \"\"\"Transform vector quantities. This excludes translation, as would be expected when transforming vectors.\"\"\"\n    a = numpy.atleast_1d(vecs)\n    return self.transform(a) - self.transform(numpy.zeros_like(a))\n
    "},{"location":"api/transform/#atomlib.transform.FuncTransform3D.identity","title":"identity staticmethod","text":"
    identity() -> FuncTransform3D\n

    Return an identity transformation.

    Source code in atomlib/transform.py
    @staticmethod\ndef identity() -> FuncTransform3D:\n    \"\"\"Return an identity transformation.\"\"\"\n    return FuncTransform3D(lambda pts: pts)\n
    "},{"location":"api/transform/#atomlib.transform.FuncTransform3D.transform","title":"transform","text":"
    transform(points: BBox3D) -> BBox3D\n
    transform(points: ArrayLike) -> NDArray[floating]\n
    transform(\n    points: Pts3DLike,\n) -> Union[BBox3D, NDArray[floating]]\n

    Transform points according to the given transformation.

    Source code in atomlib/transform.py
    def transform(self, points: Pts3DLike) -> t.Union[BBox3D, NDArray[numpy.floating]]:\n    \"\"\"Transform points according to the given transformation.\"\"\"\n    if isinstance(points, BBox3D):\n        return points.from_pts(self.transform(points.corners()))\n\n    return self.f(numpy.atleast_1d(points))\n
    "},{"location":"api/transform/#atomlib.transform.FuncTransform3D.compose","title":"compose","text":"
    compose(other: Transform3D) -> FuncTransform3D\n

    Compose this transformation with another.

    Source code in atomlib/transform.py
    def compose(self, other: Transform3D) -> FuncTransform3D:\n    \"\"\"Compose this transformation with another.\"\"\"\n    return FuncTransform3D(lambda pts: other.transform(self.f(pts)))\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D","title":"AffineTransform3D","text":"

    Bases: Transform3D

    Source code in atomlib/transform.py
    class AffineTransform3D(Transform3D):\n    __array_ufunc__ = None\n\n    def __init__(self, array: t.Optional[ArrayLike] = None):\n        if array is None:\n            array = numpy.eye(4)\n        self.inner = numpy.broadcast_to(array, (4, 4))\n\n    @property\n    def __array_interface__(self):\n        return self.inner.__array_interface__\n\n    def __repr__(self) -> str:\n        return f\"AffineTransform3D(\\n{self.inner!r}\\n)\"\n\n    @staticmethod\n    def identity() -> AffineTransform3D:\n        \"\"\"Return an identity transformation.\"\"\"\n        return AffineTransform3D()\n\n    def round_near_zero(self: Affine3DSelf) -> Affine3DSelf:\n        \"\"\"Round near-zero matrix elements in self.\"\"\"\n        return type(self)(\n            numpy.where(numpy.abs(self.inner) < 1e-15, 0., self.inner)\n        )\n\n    @staticmethod\n    def from_linear(linear: LinearTransform3D) -> AffineTransform3D:\n        \"\"\"Make an affine transformation from a linear transformation.\"\"\"\n        dtype = linear.inner.dtype\n        return AffineTransform3D(numpy.block([\n            [linear.inner, numpy.zeros((3, 1), dtype=dtype)],\n            [numpy.zeros((1, 3), dtype=dtype), numpy.ones((), dtype=dtype)]\n        ]))  # type: ignore\n\n    def to_linear(self) -> LinearTransform3D:\n        \"\"\"Return the linear part of an affine transformation.\"\"\"\n        return LinearTransform3D(self.inner[:3, :3])\n\n    def to_translation(self) -> AffineTransform3D:\n        \"\"\"Extract the translation component of `self`, and return it.\"\"\"\n        return AffineTransform3D.translate(self.translation())\n\n    def det(self) -> float:\n        \"\"\"Return the determinant of an affine transformation.\"\"\"\n        return numpy.linalg.det(self.inner[:3, :3])\n\n    def translation(self) -> numpy.ndarray:\n        \"\"\"Extract the translation component of `self`, and return it as a vector.\"\"\"\n        return self.inner[:3, -1]\n\n    def inverse(self) -> AffineTransform3D:\n        \"\"\"Return the inverse of an affine transformation.\"\"\"\n        linear_inv = LinearTransform3D(self.inner[:3, :3]).inverse()\n        # first undo translation, then undo linear transformation\n        return linear_inv @ AffineTransform3D.translate(*-self.translation())\n\n    @t.overload\n    @classmethod\n    def translate(cls, x: VecLike, /) -> AffineTransform3D:\n        ...\n\n    @t.overload\n    @classmethod\n    def translate(cls, x: Num = 0., y: Num = 0., z: Num = 0.) -> AffineTransform3D:\n        ...\n\n    @opt_classmethod\n    def translate(self, x: t.Union[Num, VecLike] = 0., y: Num = 0., z: Num = 0.) -> AffineTransform3D:\n        \"\"\"\n        Create or append an affine translation.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        if isinstance(x, t.Sized) and len(x) > 1:\n            try:\n                (x, y, z) = to_vec3(x)\n            except ValueError:\n                raise ValueError(\"translate() must be called with a sequence or three numbers.\")\n\n        if isinstance(self, LinearTransform3D):\n            self = AffineTransform3D.from_linear(self)\n\n        a = self.inner.copy()\n        a[:3, -1] += [x, y, z]\n        return AffineTransform3D(a)\n\n    @t.overload\n    @classmethod\n    def scale(cls, x: VecLike, /) -> AffineTransform3D:\n        ...\n\n    @t.overload\n    @classmethod\n    def scale(cls, x: Num = 1., y: Num = 1., z: Num = 1., *,\n              all: Num = 1.) -> AffineTransform3D:\n        ...\n\n    @opt_classmethod\n    def scale(self, x: t.Union[Num, VecLike] = 1., y: Num = 1., z: Num = 1., *,\n              all: Num = 1.) -> AffineTransform3D:\n        \"\"\"\n        Create or append a scaling transformation.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        return self.compose(LinearTransform3D.scale(x, y, z, all=all))  # type: ignore\n\n    @opt_classmethod\n    def rotate(self, v: VecLike, theta: Num) -> AffineTransform3D:\n        \"\"\"\n        Create or append a rotation transformation of `theta`\n        radians CCW around the given vector `v`.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        return self.compose(LinearTransform3D.rotate(v, theta))\n\n    @opt_classmethod\n    def rotate_euler(self, x: Num = 0., y: Num = 0., z: Num = 0.) -> AffineTransform3D:\n        \"\"\"\n        Create or append a Euler rotation transformation.\n        Rotation is performed on the x axis first, then y axis and z axis.\n        Values are specified in radians.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        return self.compose(LinearTransform3D.rotate_euler(x, y, z))\n\n    @t.overload\n    @classmethod\n    def mirror(cls, a: VecLike, /) -> AffineTransform3D:\n        ...\n\n    @t.overload\n    @classmethod\n    def mirror(cls, a: Num, b: Num, c: Num) -> AffineTransform3D:\n        ...\n\n    @opt_classmethod\n    def mirror(self, a: t.Union[Num, VecLike],\n               b: t.Optional[Num] = None,\n               c: t.Optional[Num] = None) -> AffineTransform3D:\n        \"\"\"\n        Create or append a mirror transformation across the given plane.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        return self.compose(LinearTransform3D.mirror(a, b, c))  # type: ignore\n\n    @opt_classmethod\n    def strain(self, strain: float, v: VecLike = (0, 0, 1), poisson: float = 0.) -> AffineTransform3D:\n        \"\"\"\n        Apply a strain of ``strain`` in direction ``v``, assuming an elastically isotropic material.\n\n        Strain is applied relative to the origin.\n\n        With ``poisson=0`` (default), a uniaxial strain is applied.\n        With ``poisson=-1``, hydrostatic strain is applied.\n        Otherwise, a uniaxial stress is applied for a material with Poisson ratio `poisson`,\n        which results in shrinkage perpendicular to the direction strain is applied.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        return self.compose(LinearTransform3D.strain(strain, v, poisson))\n\n    def align_standard(self) -> AffineTransform3D:\n        \"\"\"\n        Align `self` so `v1` is in the x-axis and `v2` is in the xy-plane.\n\n        This is equivalent to a $QR$ decomposition which keeps only the\n        right-triangular matrix $R$.\n\n        For an affine transformation, this rotates the transformation\n        around the global origin (including any transformation).\n        \"\"\"\n        if self.det() <= 0:\n            raise ValueError(\"align_standard() requires a right-handed transformation\")\n\n        translation = self.translation()\n\n        import scipy.linalg\n        q, r = t.cast(t.Tuple[numpy.ndarray, numpy.ndarray], scipy.linalg.qr(self.to_linear().inner))\n        # qr unique up to the sign of the digonal\n        # we choose the case where r is positive definite\n        q = q * numpy.sign(r.diagonal())\n        r = r * numpy.sign(r.diagonal())[:, None]\n        assert numpy.linalg.det(r) > 0\n        # we need to remove the rotation from the translation component as well\n        # q is orthogonal, so q^-1 = q.T\n        return LinearTransform3D(r).translate(q.T @ translation).round_near_zero()\n\n    @t.overload\n    def transform(self, points: BBox3D) -> BBox3D:\n        ...\n\n    @t.overload\n    def transform(self, points: ArrayLike) -> NDArray[numpy.floating]:\n        ...\n\n    def transform(self, points: Pts3DLike) -> t.Union[BBox3D, NDArray[numpy.floating]]:\n        \"\"\"Transform points according to the given transformation.\"\"\"\n        if isinstance(points, BBox3D):\n            return points.from_pts(self.transform(points.corners()))\n\n        pts: NDArray[numpy.floating] = numpy.atleast_1d(points).astype(self.inner.dtype)\n        pts = numpy.concatenate((points, numpy.broadcast_to(1., (*pts.shape[:-1], 1))), axis=-1)\n\n        # carefully handle inf and nan\n        isnan = numpy.bitwise_or.reduce(numpy.isnan(pts), axis=-1)\n\n        with numpy.errstate(invalid='ignore'):\n            out = pts @ self.inner.T\n            isinf = numpy.isnan(out[..., 0]) & ~isnan\n\n            prod = numpy.multiply(self.inner, pts[isinf, None, :])\n\n        # inf * 0 = 0\n        prod[numpy.isnan(prod)] = 0.\n        out[isinf] = numpy.sum(prod, axis=-1)\n\n        return out[..., :3]\n\n    __call__ = transform\n\n    def transform_vec(self, vecs: ArrayLike) -> NDArray[numpy.floating]:\n        return self.to_linear().transform(vecs)\n\n    @t.overload\n    def compose(self, other: AffineTransform3D) -> AffineTransform3D:\n        ...\n\n    @t.overload\n    def compose(self, other: Transform3DT) -> Transform3DT:\n        ...\n\n    def compose(self, other: Transform3D) -> Transform3D:\n        \"\"\"Compose this transformation with another.\"\"\"\n        if not isinstance(other, Transform3D):\n            raise TypeError(f\"Expected a Transform3D, got {type(other)}\")\n        if isinstance(other, LinearTransform3D):\n            return self.compose(AffineTransform3D.from_linear(other))\n        if isinstance(other, AffineTransform3D):\n            return AffineTransform3D(other.inner @ self.inner)\n        elif hasattr(other, '_rcompose'):\n            return other._rcompose(self)  # type: ignore\n        else:\n            raise NotImplementedError()\n\n    @t.overload\n    def conjugate(self, transform: AffineTransform3D) -> AffineTransform3D:\n        ...\n\n    @t.overload\n    def conjugate(self, transform: Transform3DT) -> Transform3DT:\n        ...\n\n    def conjugate(self, transform: Transform3D) -> Transform3D:\n        \"\"\"\n        Apply ``transform`` in the coordinate frame of ``self``.\n\n        Equivalent to an (inverse) conjugation in group theory, or :math:`T^-1 A T`\n        \"\"\"\n        return self.inverse() @ transform @ self\n\n    @t.overload\n    def __matmul__(self, other: AffineTransform3D) -> AffineTransform3D:\n        ...\n\n    @t.overload\n    def __matmul__(self, other: Transform3D) -> Transform3D:\n        ...\n\n    @t.overload\n    def __matmul__(self, other: BBox3D) -> BBox3D:\n        ...\n\n    @t.overload\n    def __matmul__(self, other: ArrayLike) -> NDArray[numpy.floating]:\n        ...\n\n    def __matmul__(self, other: t.Union[Transform3D, ArrayLike, BBox3D]):  # type: ignore (spurious)\n        \"\"\"Compose this transformation, or apply it to a given set of points.\"\"\"\n        if isinstance(other, Transform3D):\n            return other.compose(self)\n        return self.transform(other)\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.inner","title":"inner instance-attribute","text":"
    inner = broadcast_to(array, (4, 4))\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.make","title":"make staticmethod","text":"
    make(data: IntoTransform3D) -> Transform3D\n

    Make a transformation from a function or numpy array (3x3 or 4x4).

    Source code in atomlib/transform.py
    @staticmethod\ndef make(data: IntoTransform3D) -> Transform3D:\n    \"\"\"Make a transformation from a function or numpy array (3x3 or 4x4).\"\"\"\n    if isinstance(data, Transform3D):\n        return data\n    if not isinstance(data, numpy.ndarray) and hasattr(data, '__call__'):\n        return FuncTransform3D(data)\n    data = numpy.array(data)\n    if data.shape == (3, 3):\n        return LinearTransform3D(data)\n    if data.shape == (4, 4):\n        return AffineTransform3D(data)\n    raise ValueError(f\"Transform3D of invalid shape {data.shape}\")\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.identity","title":"identity staticmethod","text":"
    identity() -> AffineTransform3D\n

    Return an identity transformation.

    Source code in atomlib/transform.py
    @staticmethod\ndef identity() -> AffineTransform3D:\n    \"\"\"Return an identity transformation.\"\"\"\n    return AffineTransform3D()\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.round_near_zero","title":"round_near_zero","text":"
    round_near_zero() -> Affine3DSelf\n

    Round near-zero matrix elements in self.

    Source code in atomlib/transform.py
    def round_near_zero(self: Affine3DSelf) -> Affine3DSelf:\n    \"\"\"Round near-zero matrix elements in self.\"\"\"\n    return type(self)(\n        numpy.where(numpy.abs(self.inner) < 1e-15, 0., self.inner)\n    )\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.from_linear","title":"from_linear staticmethod","text":"
    from_linear(linear: LinearTransform3D) -> AffineTransform3D\n

    Make an affine transformation from a linear transformation.

    Source code in atomlib/transform.py
    @staticmethod\ndef from_linear(linear: LinearTransform3D) -> AffineTransform3D:\n    \"\"\"Make an affine transformation from a linear transformation.\"\"\"\n    dtype = linear.inner.dtype\n    return AffineTransform3D(numpy.block([\n        [linear.inner, numpy.zeros((3, 1), dtype=dtype)],\n        [numpy.zeros((1, 3), dtype=dtype), numpy.ones((), dtype=dtype)]\n    ]))  # type: ignore\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.to_linear","title":"to_linear","text":"
    to_linear() -> LinearTransform3D\n

    Return the linear part of an affine transformation.

    Source code in atomlib/transform.py
    def to_linear(self) -> LinearTransform3D:\n    \"\"\"Return the linear part of an affine transformation.\"\"\"\n    return LinearTransform3D(self.inner[:3, :3])\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.to_translation","title":"to_translation","text":"
    to_translation() -> AffineTransform3D\n

    Extract the translation component of self, and return it.

    Source code in atomlib/transform.py
    def to_translation(self) -> AffineTransform3D:\n    \"\"\"Extract the translation component of `self`, and return it.\"\"\"\n    return AffineTransform3D.translate(self.translation())\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.det","title":"det","text":"
    det() -> float\n

    Return the determinant of an affine transformation.

    Source code in atomlib/transform.py
    def det(self) -> float:\n    \"\"\"Return the determinant of an affine transformation.\"\"\"\n    return numpy.linalg.det(self.inner[:3, :3])\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.translation","title":"translation","text":"
    translation() -> ndarray\n

    Extract the translation component of self, and return it as a vector.

    Source code in atomlib/transform.py
    def translation(self) -> numpy.ndarray:\n    \"\"\"Extract the translation component of `self`, and return it as a vector.\"\"\"\n    return self.inner[:3, -1]\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.inverse","title":"inverse","text":"
    inverse() -> AffineTransform3D\n

    Return the inverse of an affine transformation.

    Source code in atomlib/transform.py
    def inverse(self) -> AffineTransform3D:\n    \"\"\"Return the inverse of an affine transformation.\"\"\"\n    linear_inv = LinearTransform3D(self.inner[:3, :3]).inverse()\n    # first undo translation, then undo linear transformation\n    return linear_inv @ AffineTransform3D.translate(*-self.translation())\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.translate","title":"translate","text":"
    translate(x: VecLike) -> AffineTransform3D\n
    translate(\n    x: Num = 0.0, y: Num = 0.0, z: Num = 0.0\n) -> AffineTransform3D\n
    translate(\n    x: Union[Num, VecLike] = 0.0, y: Num = 0.0, z: Num = 0.0\n) -> AffineTransform3D\n

    Create or append an affine translation.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef translate(self, x: t.Union[Num, VecLike] = 0., y: Num = 0., z: Num = 0.) -> AffineTransform3D:\n    \"\"\"\n    Create or append an affine translation.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    if isinstance(x, t.Sized) and len(x) > 1:\n        try:\n            (x, y, z) = to_vec3(x)\n        except ValueError:\n            raise ValueError(\"translate() must be called with a sequence or three numbers.\")\n\n    if isinstance(self, LinearTransform3D):\n        self = AffineTransform3D.from_linear(self)\n\n    a = self.inner.copy()\n    a[:3, -1] += [x, y, z]\n    return AffineTransform3D(a)\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.scale","title":"scale","text":"
    scale(x: VecLike) -> AffineTransform3D\n
    scale(\n    x: Num = 1.0,\n    y: Num = 1.0,\n    z: Num = 1.0,\n    *,\n    all: Num = 1.0\n) -> AffineTransform3D\n
    scale(\n    x: Union[Num, VecLike] = 1.0,\n    y: Num = 1.0,\n    z: Num = 1.0,\n    *,\n    all: Num = 1.0\n) -> AffineTransform3D\n

    Create or append a scaling transformation.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef scale(self, x: t.Union[Num, VecLike] = 1., y: Num = 1., z: Num = 1., *,\n          all: Num = 1.) -> AffineTransform3D:\n    \"\"\"\n    Create or append a scaling transformation.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    return self.compose(LinearTransform3D.scale(x, y, z, all=all))  # type: ignore\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.rotate","title":"rotate","text":"
    rotate(v: VecLike, theta: Num) -> AffineTransform3D\n

    Create or append a rotation transformation of theta radians CCW around the given vector v.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef rotate(self, v: VecLike, theta: Num) -> AffineTransform3D:\n    \"\"\"\n    Create or append a rotation transformation of `theta`\n    radians CCW around the given vector `v`.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    return self.compose(LinearTransform3D.rotate(v, theta))\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.rotate_euler","title":"rotate_euler","text":"
    rotate_euler(\n    x: Num = 0.0, y: Num = 0.0, z: Num = 0.0\n) -> AffineTransform3D\n

    Create or append a Euler rotation transformation. Rotation is performed on the x axis first, then y axis and z axis. Values are specified in radians.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef rotate_euler(self, x: Num = 0., y: Num = 0., z: Num = 0.) -> AffineTransform3D:\n    \"\"\"\n    Create or append a Euler rotation transformation.\n    Rotation is performed on the x axis first, then y axis and z axis.\n    Values are specified in radians.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    return self.compose(LinearTransform3D.rotate_euler(x, y, z))\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.mirror","title":"mirror","text":"
    mirror(a: VecLike) -> AffineTransform3D\n
    mirror(a: Num, b: Num, c: Num) -> AffineTransform3D\n
    mirror(\n    a: Union[Num, VecLike],\n    b: Optional[Num] = None,\n    c: Optional[Num] = None,\n) -> AffineTransform3D\n

    Create or append a mirror transformation across the given plane.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef mirror(self, a: t.Union[Num, VecLike],\n           b: t.Optional[Num] = None,\n           c: t.Optional[Num] = None) -> AffineTransform3D:\n    \"\"\"\n    Create or append a mirror transformation across the given plane.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    return self.compose(LinearTransform3D.mirror(a, b, c))  # type: ignore\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.strain","title":"strain","text":"
    strain(\n    strain: float,\n    v: VecLike = (0, 0, 1),\n    poisson: float = 0.0,\n) -> AffineTransform3D\n

    Apply a strain of strain in direction v, assuming an elastically isotropic material.

    Strain is applied relative to the origin.

    With poisson=0 (default), a uniaxial strain is applied. With poisson=-1, hydrostatic strain is applied. Otherwise, a uniaxial stress is applied for a material with Poisson ratio poisson, which results in shrinkage perpendicular to the direction strain is applied.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef strain(self, strain: float, v: VecLike = (0, 0, 1), poisson: float = 0.) -> AffineTransform3D:\n    \"\"\"\n    Apply a strain of ``strain`` in direction ``v``, assuming an elastically isotropic material.\n\n    Strain is applied relative to the origin.\n\n    With ``poisson=0`` (default), a uniaxial strain is applied.\n    With ``poisson=-1``, hydrostatic strain is applied.\n    Otherwise, a uniaxial stress is applied for a material with Poisson ratio `poisson`,\n    which results in shrinkage perpendicular to the direction strain is applied.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    return self.compose(LinearTransform3D.strain(strain, v, poisson))\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.align_standard","title":"align_standard","text":"
    align_standard() -> AffineTransform3D\n

    Align self so v1 is in the x-axis and v2 is in the xy-plane.

    This is equivalent to a \\(QR\\) decomposition which keeps only the right-triangular matrix \\(R\\).

    For an affine transformation, this rotates the transformation around the global origin (including any transformation).

    Source code in atomlib/transform.py
    def align_standard(self) -> AffineTransform3D:\n    \"\"\"\n    Align `self` so `v1` is in the x-axis and `v2` is in the xy-plane.\n\n    This is equivalent to a $QR$ decomposition which keeps only the\n    right-triangular matrix $R$.\n\n    For an affine transformation, this rotates the transformation\n    around the global origin (including any transformation).\n    \"\"\"\n    if self.det() <= 0:\n        raise ValueError(\"align_standard() requires a right-handed transformation\")\n\n    translation = self.translation()\n\n    import scipy.linalg\n    q, r = t.cast(t.Tuple[numpy.ndarray, numpy.ndarray], scipy.linalg.qr(self.to_linear().inner))\n    # qr unique up to the sign of the digonal\n    # we choose the case where r is positive definite\n    q = q * numpy.sign(r.diagonal())\n    r = r * numpy.sign(r.diagonal())[:, None]\n    assert numpy.linalg.det(r) > 0\n    # we need to remove the rotation from the translation component as well\n    # q is orthogonal, so q^-1 = q.T\n    return LinearTransform3D(r).translate(q.T @ translation).round_near_zero()\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.transform","title":"transform","text":"
    transform(points: BBox3D) -> BBox3D\n
    transform(points: ArrayLike) -> NDArray[floating]\n
    transform(\n    points: Pts3DLike,\n) -> Union[BBox3D, NDArray[floating]]\n

    Transform points according to the given transformation.

    Source code in atomlib/transform.py
    def transform(self, points: Pts3DLike) -> t.Union[BBox3D, NDArray[numpy.floating]]:\n    \"\"\"Transform points according to the given transformation.\"\"\"\n    if isinstance(points, BBox3D):\n        return points.from_pts(self.transform(points.corners()))\n\n    pts: NDArray[numpy.floating] = numpy.atleast_1d(points).astype(self.inner.dtype)\n    pts = numpy.concatenate((points, numpy.broadcast_to(1., (*pts.shape[:-1], 1))), axis=-1)\n\n    # carefully handle inf and nan\n    isnan = numpy.bitwise_or.reduce(numpy.isnan(pts), axis=-1)\n\n    with numpy.errstate(invalid='ignore'):\n        out = pts @ self.inner.T\n        isinf = numpy.isnan(out[..., 0]) & ~isnan\n\n        prod = numpy.multiply(self.inner, pts[isinf, None, :])\n\n    # inf * 0 = 0\n    prod[numpy.isnan(prod)] = 0.\n    out[isinf] = numpy.sum(prod, axis=-1)\n\n    return out[..., :3]\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.transform_vec","title":"transform_vec","text":"
    transform_vec(vecs: ArrayLike) -> NDArray[floating]\n
    Source code in atomlib/transform.py
    def transform_vec(self, vecs: ArrayLike) -> NDArray[numpy.floating]:\n    return self.to_linear().transform(vecs)\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.compose","title":"compose","text":"
    compose(other: AffineTransform3D) -> AffineTransform3D\n
    compose(other: Transform3DT) -> Transform3DT\n
    compose(other: Transform3D) -> Transform3D\n

    Compose this transformation with another.

    Source code in atomlib/transform.py
    def compose(self, other: Transform3D) -> Transform3D:\n    \"\"\"Compose this transformation with another.\"\"\"\n    if not isinstance(other, Transform3D):\n        raise TypeError(f\"Expected a Transform3D, got {type(other)}\")\n    if isinstance(other, LinearTransform3D):\n        return self.compose(AffineTransform3D.from_linear(other))\n    if isinstance(other, AffineTransform3D):\n        return AffineTransform3D(other.inner @ self.inner)\n    elif hasattr(other, '_rcompose'):\n        return other._rcompose(self)  # type: ignore\n    else:\n        raise NotImplementedError()\n
    "},{"location":"api/transform/#atomlib.transform.AffineTransform3D.conjugate","title":"conjugate","text":"
    conjugate(\n    transform: AffineTransform3D,\n) -> AffineTransform3D\n
    conjugate(transform: Transform3DT) -> Transform3DT\n
    conjugate(transform: Transform3D) -> Transform3D\n

    Apply transform in the coordinate frame of self.

    Equivalent to an (inverse) conjugation in group theory, or :math:T^-1 A T

    Source code in atomlib/transform.py
    def conjugate(self, transform: Transform3D) -> Transform3D:\n    \"\"\"\n    Apply ``transform`` in the coordinate frame of ``self``.\n\n    Equivalent to an (inverse) conjugation in group theory, or :math:`T^-1 A T`\n    \"\"\"\n    return self.inverse() @ transform @ self\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D","title":"LinearTransform3D","text":"

    Bases: AffineTransform3D

    Source code in atomlib/transform.py
    class LinearTransform3D(AffineTransform3D):\n    def __init__(self, array: t.Optional[ArrayLike] = None):\n        if array is None:\n            array = numpy.eye(3, dtype=numpy.float64)\n        self.inner = numpy.broadcast_to(array, (3, 3))\n\n    @property\n    def T(self):\n        return LinearTransform3D(self.inner.T)\n\n    def __repr__(self) -> str:\n        return f\"LinearTransform3D(\\n{self.inner!r}\\n)\"\n\n    def translation(self):\n        \"\"\"Extract the translation component of `self`, and return it as a vector.\"\"\"\n        return numpy.zeros(3, dtype=self.inner.dtype)\n\n    @staticmethod\n    def identity() -> LinearTransform3D:\n        \"\"\"Return an identity transformation.\"\"\"\n        return LinearTransform3D()\n\n    def det(self) -> float:\n        \"\"\"Return the determinant of an affine transformation.\"\"\"\n        return numpy.linalg.det(self.inner)\n\n    def inverse(self) -> LinearTransform3D:\n        \"\"\"Return the inverse of an affine transformation.\"\"\"\n        return LinearTransform3D(numpy.linalg.inv(self.inner))\n\n    def to_linear(self) -> LinearTransform3D:\n        \"\"\"Return the linear part of an affine transformation.\"\"\"\n        return self\n\n    def is_diagonal(self, tol: float = 1e-10) -> bool:\n        \"\"\"\n        Return whether this transformation is diagonal (i.e. axis-aligned scaling only).\n        \"\"\"\n        d = self.inner.shape[0]\n        p, q = self.inner.strides\n        offdiag = numpy.lib.stride_tricks.as_strided(self.inner[:, 1:], (d-1, d), (p+q, q))\n        return bool((numpy.abs(offdiag) < tol).all())\n\n    def is_normal(self, rtol: float = 1e-5, atol: float = 1e-8) -> bool:\n        \"\"\"Returns `True` if `self` is a normal matrix.\"\"\"\n        return bool(numpy.allclose(\n            self.inner.T @ self.inner, self.inner @ self.inner.T,\n            rtol=rtol, atol=atol\n        ))\n\n    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n        \"\"\"\n        Returns `True` if `self` is an orthogonal matrix (i.e. a pure rotation or roto-reflection).\n        \"\"\"\n        return numpy.allclose(self.inner @ self.inner.T, numpy.eye(3), atol=tol)\n\n    def is_scaled_orthogonal(self, tol: float = 1e-8) -> bool:\n        \"\"\"\n        Returns `True` if `self` is a scaled orthogonal matrix (composed of orthogonal\n        basis vectors, i.e. a scaling + a rotation or roto-reflection)\n        \"\"\"\n        return is_diagonal(self.inner @ self.inner.T, tol=tol)\n\n    @t.overload\n    @classmethod\n    def mirror(cls, a: VecLike, /) -> LinearTransform3D:\n        ...\n\n    @t.overload\n    @classmethod\n    def mirror(cls, a: Num, b: Num, c: Num) -> LinearTransform3D:\n        ...\n\n    @opt_classmethod\n    def mirror(self, a: t.Union[Num, VecLike],\n               b: t.Optional[Num] = None,\n               c: t.Optional[Num] = None) -> LinearTransform3D:\n        \"\"\"\n        Create or append a mirror transformation across the given plane.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        if isinstance(a, t.Sized):\n            v = numpy.array(numpy.broadcast_to(a, 3), dtype=numpy.float64)\n            if b is not None or c is not None:\n                raise ValueError(\"mirror() must be passed a sequence or three numbers.\")\n        else:\n            v = numpy.array([a, b, c], dtype=numpy.float64)\n        v /= numpy.linalg.norm(v)\n        mirror = numpy.eye(3) - 2 * numpy.outer(v, v)\n        return LinearTransform3D(mirror @ self.inner)\n\n    @opt_classmethod\n    def strain(self, strain: float, v: VecLike = (0, 0, 1), poisson: float = 0.) -> LinearTransform3D:\n        \"\"\"\n        Apply a strain of ``strain`` in direction ``v``, assuming an elastically isotropic material.\n\n        Strain is applied relative to the origin.\n\n        With ``poisson=0`` (default), a uniaxial strain is applied.\n        With ``poisson=-1``, hydrostatic strain is applied.\n        Otherwise, a uniaxial stress is applied for a material with Poisson ratio `poisson`,\n        which results in shrinkage perpendicular to the direction strain is applied.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        shrink = (1 + strain) ** -poisson\n        return self.compose(LinearTransform3D.align(v).conjugate(\n                            LinearTransform3D.scale([shrink, shrink, 1. + strain])))\n\n    @opt_classmethod\n    def rotate(self, v: VecLike, theta: Num) -> LinearTransform3D:\n        \"\"\"\n        Create or append a rotation transformation of `theta`\n        radians CCW around the given vector `v`.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        theta = float(theta)\n        v = numpy.array(numpy.broadcast_to(v, (3,)), dtype=numpy.float64)\n        l = numpy.linalg.norm(v)\n        if numpy.isclose(l, 0.):\n            if numpy.isclose(theta, 0.):\n                # null rotation\n                return self\n            raise ValueError(\"rotate() about the zero vector is undefined.\")\n        v /= l\n\n        # Rodrigues rotation formula\n        w = numpy.array([[  0., -v[2],  v[1]],\n                         [ v[2],   0., -v[0]],\n                         [-v[1], v[0],   0.]], dtype=numpy.float64)\n        # I + sin(t) W + (1 - cos(t)) W^2 = I + sin(t) W + 2*sin^2(t/2) W^2\n        a = numpy.eye(3) + numpy.sin(theta) * w + 2 * (numpy.sin(theta / 2)**2) * w @ w\n        return LinearTransform3D(a @ self.inner)\n\n    @opt_classmethod\n    def rotate_euler(self, x: Num = 0., y: Num = 0., z: Num = 0.) -> LinearTransform3D:\n        \"\"\"\n        Create or append a Euler rotation transformation.\n        Rotation is performed on the x axis first, then y axis and z axis.\n        Values are specified in radians.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        angles = numpy.array([x, y, z], dtype=numpy.float64)\n        c, s = numpy.cos(angles), numpy.sin(angles)\n        a = numpy.array([\n            [c[1]*c[2], s[0]*s[1]*c[2] - c[0]*s[2], c[0]*s[1]*c[2] + s[0]*s[2]],\n            [c[1]*s[2], s[0]*s[1]*s[2] + c[0]*c[2], c[0]*s[1]*s[2] - s[0]*c[2]],\n            [-s[1],     s[0]*c[1],                  c[0]*c[1]],\n        ], dtype=numpy.float64)\n        return LinearTransform3D(a @ self.inner)\n\n    @opt_classmethod\n    def align(self, v1: VecLike, horz: t.Optional[VecLike] = None) -> LinearTransform3D:\n        \"\"\"\n        Create a transformation which transforms `v1` to align with [0, 0, 1].\n        If `horz` is specified, it will be aligned in the direction of [1, 0, 0].\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        v1 = numpy.broadcast_to(v1, 3)\n        v1 = v1 / numpy.linalg.norm(v1)\n        if horz is None:\n            if numpy.isclose(v1[0], 1.):\n                # zone is [1., 0., 0.], choose a different direction\n                horz = numpy.array([0., 1., 0.])\n            else:\n                horz = numpy.array([1., 0., 0.])\n        else:\n            horz = numpy.broadcast_to(horz, 3)\n\n        return self.align_to(v1, [0., 0., 1.], horz, [1., 0., 0.])\n\n    @t.overload\n    @classmethod\n    def align_to(cls, v1: VecLike, v2: VecLike, p1: t.Literal[None] = None, p2: t.Literal[None] = None) -> LinearTransform3D:\n        ...\n\n    @t.overload\n    @classmethod\n    def align_to(cls, v1: VecLike, v2: VecLike, p1: VecLike, p2: VecLike) -> LinearTransform3D:\n        ...\n\n    @opt_classmethod\n    def align_to(self, v1: VecLike, v2: VecLike,\n                 p1: t.Optional[VecLike] = None, p2: t.Optional[VecLike] = None) -> LinearTransform3D:\n        \"\"\"\n        Create a transformation which transforms `v1` to align with `v2`.\n        If specified, additionally ensure that `p1` aligns with `p2` in the plane of `v2`.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        v1 = numpy.broadcast_to(v1, 3)\n        v1 = v1 / numpy.linalg.norm(v1)\n        v2 = numpy.broadcast_to(v2, 3)\n        v2 = v2 / numpy.linalg.norm(v2)\n\n        v3 = numpy.cross(v1, v2)\n        # rotate along v1 x v2 (geodesic rotation)\n        theta = numpy.arctan2(numpy.linalg.norm(v3), numpy.dot(v1, v2))\n        if numpy.isclose(numpy.linalg.norm(v3), 0.):\n            # any non-v1/v2 vector works. We choose the unit vector with largest cross product\n            v3 = numpy.zeros_like(v3)\n            v3[numpy.argmin(numpy.abs(v1))] = 1.\n\n        aligned = self.rotate(v3, theta)\n\n        if p1 is None and p2 is None:\n            return aligned.round_near_zero()\n        if p1 is None:\n            raise ValueError(\"If `p2` is specified, `p1` must also be specified.\")\n        if p2 is None:\n            raise ValueError(\"If `p1` is specified, `p2` must also be specified.\")\n\n        p1_align = aligned.transform(numpy.broadcast_to(p1, 3))\n        p2 = numpy.broadcast_to(p2, 3)\n        # components perpendicular to v2\n        p2_perp = perp(p2, v2)\n        p1_perp = perp(p1_align, v2)\n        # now rotate along v2\n        theta = numpy.arctan2(numpy.dot(v2, numpy.cross(p1_perp, p2_perp)), numpy.dot(p1_perp, p2_perp))\n        #theta = numpy.arctan2(numpy.linalg.norm(numpy.cross(p1_perp, p2_perp)), numpy.dot(p1_perp, p2_perp))\n        return aligned.rotate(v2, theta).round_near_zero()\n\n    def align_standard(self) -> LinearTransform3D:\n        \"\"\"\n        Align `self` so `v1` is in the x-axis and `v2` is in the xy-plane.\n\n        This is equivalent to a $QR$ decomposition which keeps only the\n        right-triangular matrix $R$.\n        \"\"\"\n        if self.det() <= 0:\n            raise ValueError(\"align_standard() requires a right-handed transformation\")\n\n        import scipy.linalg\n        _q, r = t.cast(t.Tuple[numpy.ndarray, numpy.ndarray], scipy.linalg.qr(self.inner))\n        # qr unique up to the sign of the digonal\n        # we choose the case where r is positive definite\n        r = r * numpy.sign(r.diagonal())[:, None]\n        assert numpy.linalg.det(r) > 0  # check our work\n        return LinearTransform3D(r).round_near_zero()\n\n    def _orthogonal_axes(self, max_denom: int = 1000) -> NDArray[numpy.int_]:\n        \"\"\"\n        Given a linear transformation A, compute an optimal linear\n        combination of basis vectors to form an orthogonal basis.\n\n        More formally, returns a small integer matrix M such that A@M is normal.\n        \"\"\"\n        import scipy.linalg\n\n        inv = self.inverse().inner\n        r, _q = scipy.linalg.rq(inv)\n        # rq unique up to the sign of the digonal\n        r = r * numpy.sign(r.diagonal())\n\n        int_r = numpy.array([reduce_vec(v, max_denom) for v in r.T]).T\n        return int_r\n\n    @t.overload\n    @classmethod\n    def scale(cls, x: VecLike, /) -> LinearTransform3D:\n        ...\n\n    @t.overload\n    @classmethod\n    def scale(cls, x: Num = 1., y: Num = 1., z: Num = 1., *,\n              all: Num = 1.) -> LinearTransform3D:\n        ...\n\n    @opt_classmethod\n    def scale(self, x: t.Union[Num, VecLike] = 1., y: Num = 1., z: Num = 1., *,\n              all: Num = 1.) -> LinearTransform3D:\n        \"\"\"\n        Create or append a scaling transformation.\n\n        Can be called as a classmethod or instance method.\n        \"\"\"\n        if isinstance(x, t.Sized):\n            v = numpy.broadcast_to(x, 3)\n            if y != 1. or z != 1.:\n                raise ValueError(\"scale() must be passed a sequence or three numbers.\")\n        else:\n            v = numpy.array([x, y, z])\n\n        a = numpy.zeros((3, 3), dtype=self.inner.dtype)\n        a[numpy.diag_indices(3)] = all * v\n        return LinearTransform3D(a @ self.inner)\n\n    def conjugate(self, transform: Transform3DT) -> Transform3DT:  # type: ignore (spurious)\n        \"\"\"\n        Apply `transform` in the coordinate frame of `self`.\n\n        Equivalent to an (inverse) conjugation in group theory, or $T^{-1} A T$\n        \"\"\"\n        return self.inverse() @ self.compose(transform)\n\n    def compose(self, other: Transform3DT) -> Transform3DT:  # type: ignore (spurious)\n        \"\"\"Compose this transformation with another.\"\"\"\n        if isinstance(other, LinearTransform3D):\n            return other.__class__(other.inner @ self.inner)\n        if isinstance(other, AffineTransform3D):\n            return AffineTransform3D.from_linear(self).compose(other)\n        if not isinstance(other, Transform3D):\n            raise TypeError(f\"Expected a Transform3D, got {type(other)}\")\n        elif hasattr(other, '_rcompose'):\n            return other._rcompose(self)  # type: ignore\n        raise NotImplementedError()\n\n    @t.overload\n    def transform(self, points: BBox3D) -> BBox3D:\n        ...\n\n    @t.overload\n    def transform(self, points: ArrayLike) -> NDArray[numpy.floating]:\n        ...\n\n    def transform(self, points: Pts3DLike) -> t.Union[BBox3D, NDArray[numpy.floating]]:\n        \"\"\"Transform points according to the given transformation.\"\"\"\n        if isinstance(points, BBox3D):\n            return points.from_pts(self.transform(points.corners()))\n\n        pts: NDArray[numpy.floating] = numpy.atleast_1d(points).astype(self.inner.dtype)\n        if pts.shape[-1] != 3:\n            raise ValueError(f\"{self.__class__} works on 3d points only.\")\n\n        # carefully handle inf and nan\n        isnan = numpy.bitwise_or.reduce(numpy.isnan(pts), axis=-1)\n\n        with numpy.errstate(invalid='ignore'):\n            out = pts @ self.inner.T\n            isinf = numpy.isnan(out[..., 0]) & ~isnan\n\n            prod = numpy.multiply(self.inner, pts[isinf, None, :])\n\n        # inf * 0 = 0\n        prod[numpy.isnan(prod)] = 0.\n        out[isinf] = numpy.sum(prod, axis=-1)\n\n        return out\n\n    @t.overload\n    def __matmul__(self, other: Transform3DT) -> Transform3DT:\n        ...\n\n    @t.overload\n    def __matmul__(self, other: BBox3D) -> BBox3D:\n        ...\n\n    @t.overload\n    def __matmul__(self, other: ArrayLike) -> NDArray[numpy.floating]:\n        ...\n\n    def __matmul__(self, other: t.Union[Transform3DT, ArrayLike, BBox3D]) -> t.Union[Transform3DT, NDArray[numpy.floating], BBox3D]:\n        \"\"\"Compose this transformation, or apply it to a given set of points.\"\"\"\n        if isinstance(other, Transform3D):\n            return other.compose(self)\n        return self.transform(other)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.inner","title":"inner instance-attribute","text":"
    inner = broadcast_to(array, (3, 3))\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.make","title":"make staticmethod","text":"
    make(data: IntoTransform3D) -> Transform3D\n

    Make a transformation from a function or numpy array (3x3 or 4x4).

    Source code in atomlib/transform.py
    @staticmethod\ndef make(data: IntoTransform3D) -> Transform3D:\n    \"\"\"Make a transformation from a function or numpy array (3x3 or 4x4).\"\"\"\n    if isinstance(data, Transform3D):\n        return data\n    if not isinstance(data, numpy.ndarray) and hasattr(data, '__call__'):\n        return FuncTransform3D(data)\n    data = numpy.array(data)\n    if data.shape == (3, 3):\n        return LinearTransform3D(data)\n    if data.shape == (4, 4):\n        return AffineTransform3D(data)\n    raise ValueError(f\"Transform3D of invalid shape {data.shape}\")\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.transform_vec","title":"transform_vec","text":"
    transform_vec(vecs: ArrayLike) -> NDArray[floating]\n
    Source code in atomlib/transform.py
    def transform_vec(self, vecs: ArrayLike) -> NDArray[numpy.floating]:\n    return self.to_linear().transform(vecs)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.round_near_zero","title":"round_near_zero","text":"
    round_near_zero() -> Affine3DSelf\n

    Round near-zero matrix elements in self.

    Source code in atomlib/transform.py
    def round_near_zero(self: Affine3DSelf) -> Affine3DSelf:\n    \"\"\"Round near-zero matrix elements in self.\"\"\"\n    return type(self)(\n        numpy.where(numpy.abs(self.inner) < 1e-15, 0., self.inner)\n    )\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.from_linear","title":"from_linear staticmethod","text":"
    from_linear(linear: LinearTransform3D) -> AffineTransform3D\n

    Make an affine transformation from a linear transformation.

    Source code in atomlib/transform.py
    @staticmethod\ndef from_linear(linear: LinearTransform3D) -> AffineTransform3D:\n    \"\"\"Make an affine transformation from a linear transformation.\"\"\"\n    dtype = linear.inner.dtype\n    return AffineTransform3D(numpy.block([\n        [linear.inner, numpy.zeros((3, 1), dtype=dtype)],\n        [numpy.zeros((1, 3), dtype=dtype), numpy.ones((), dtype=dtype)]\n    ]))  # type: ignore\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.to_translation","title":"to_translation","text":"
    to_translation() -> AffineTransform3D\n

    Extract the translation component of self, and return it.

    Source code in atomlib/transform.py
    def to_translation(self) -> AffineTransform3D:\n    \"\"\"Extract the translation component of `self`, and return it.\"\"\"\n    return AffineTransform3D.translate(self.translation())\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.translate","title":"translate","text":"
    translate(x: VecLike) -> AffineTransform3D\n
    translate(\n    x: Num = 0.0, y: Num = 0.0, z: Num = 0.0\n) -> AffineTransform3D\n
    translate(\n    x: Union[Num, VecLike] = 0.0, y: Num = 0.0, z: Num = 0.0\n) -> AffineTransform3D\n

    Create or append an affine translation.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef translate(self, x: t.Union[Num, VecLike] = 0., y: Num = 0., z: Num = 0.) -> AffineTransform3D:\n    \"\"\"\n    Create or append an affine translation.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    if isinstance(x, t.Sized) and len(x) > 1:\n        try:\n            (x, y, z) = to_vec3(x)\n        except ValueError:\n            raise ValueError(\"translate() must be called with a sequence or three numbers.\")\n\n    if isinstance(self, LinearTransform3D):\n        self = AffineTransform3D.from_linear(self)\n\n    a = self.inner.copy()\n    a[:3, -1] += [x, y, z]\n    return AffineTransform3D(a)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.translation","title":"translation","text":"
    translation()\n

    Extract the translation component of self, and return it as a vector.

    Source code in atomlib/transform.py
    def translation(self):\n    \"\"\"Extract the translation component of `self`, and return it as a vector.\"\"\"\n    return numpy.zeros(3, dtype=self.inner.dtype)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.identity","title":"identity staticmethod","text":"
    identity() -> LinearTransform3D\n

    Return an identity transformation.

    Source code in atomlib/transform.py
    @staticmethod\ndef identity() -> LinearTransform3D:\n    \"\"\"Return an identity transformation.\"\"\"\n    return LinearTransform3D()\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.det","title":"det","text":"
    det() -> float\n

    Return the determinant of an affine transformation.

    Source code in atomlib/transform.py
    def det(self) -> float:\n    \"\"\"Return the determinant of an affine transformation.\"\"\"\n    return numpy.linalg.det(self.inner)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.inverse","title":"inverse","text":"
    inverse() -> LinearTransform3D\n

    Return the inverse of an affine transformation.

    Source code in atomlib/transform.py
    def inverse(self) -> LinearTransform3D:\n    \"\"\"Return the inverse of an affine transformation.\"\"\"\n    return LinearTransform3D(numpy.linalg.inv(self.inner))\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.to_linear","title":"to_linear","text":"
    to_linear() -> LinearTransform3D\n

    Return the linear part of an affine transformation.

    Source code in atomlib/transform.py
    def to_linear(self) -> LinearTransform3D:\n    \"\"\"Return the linear part of an affine transformation.\"\"\"\n    return self\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.is_diagonal","title":"is_diagonal","text":"
    is_diagonal(tol: float = 1e-10) -> bool\n

    Return whether this transformation is diagonal (i.e. axis-aligned scaling only).

    Source code in atomlib/transform.py
    def is_diagonal(self, tol: float = 1e-10) -> bool:\n    \"\"\"\n    Return whether this transformation is diagonal (i.e. axis-aligned scaling only).\n    \"\"\"\n    d = self.inner.shape[0]\n    p, q = self.inner.strides\n    offdiag = numpy.lib.stride_tricks.as_strided(self.inner[:, 1:], (d-1, d), (p+q, q))\n    return bool((numpy.abs(offdiag) < tol).all())\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.is_normal","title":"is_normal","text":"
    is_normal(rtol: float = 1e-05, atol: float = 1e-08) -> bool\n

    Returns True if self is a normal matrix.

    Source code in atomlib/transform.py
    def is_normal(self, rtol: float = 1e-5, atol: float = 1e-8) -> bool:\n    \"\"\"Returns `True` if `self` is a normal matrix.\"\"\"\n    return bool(numpy.allclose(\n        self.inner.T @ self.inner, self.inner @ self.inner.T,\n        rtol=rtol, atol=atol\n    ))\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.is_orthogonal","title":"is_orthogonal","text":"
    is_orthogonal(tol: float = 1e-08) -> bool\n

    Returns True if self is an orthogonal matrix (i.e. a pure rotation or roto-reflection).

    Source code in atomlib/transform.py
    def is_orthogonal(self, tol: float = 1e-8) -> bool:\n    \"\"\"\n    Returns `True` if `self` is an orthogonal matrix (i.e. a pure rotation or roto-reflection).\n    \"\"\"\n    return numpy.allclose(self.inner @ self.inner.T, numpy.eye(3), atol=tol)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.is_scaled_orthogonal","title":"is_scaled_orthogonal","text":"
    is_scaled_orthogonal(tol: float = 1e-08) -> bool\n

    Returns True if self is a scaled orthogonal matrix (composed of orthogonal basis vectors, i.e. a scaling + a rotation or roto-reflection)

    Source code in atomlib/transform.py
    def is_scaled_orthogonal(self, tol: float = 1e-8) -> bool:\n    \"\"\"\n    Returns `True` if `self` is a scaled orthogonal matrix (composed of orthogonal\n    basis vectors, i.e. a scaling + a rotation or roto-reflection)\n    \"\"\"\n    return is_diagonal(self.inner @ self.inner.T, tol=tol)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.mirror","title":"mirror","text":"
    mirror(a: VecLike) -> LinearTransform3D\n
    mirror(a: Num, b: Num, c: Num) -> LinearTransform3D\n
    mirror(\n    a: Union[Num, VecLike],\n    b: Optional[Num] = None,\n    c: Optional[Num] = None,\n) -> LinearTransform3D\n

    Create or append a mirror transformation across the given plane.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef mirror(self, a: t.Union[Num, VecLike],\n           b: t.Optional[Num] = None,\n           c: t.Optional[Num] = None) -> LinearTransform3D:\n    \"\"\"\n    Create or append a mirror transformation across the given plane.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    if isinstance(a, t.Sized):\n        v = numpy.array(numpy.broadcast_to(a, 3), dtype=numpy.float64)\n        if b is not None or c is not None:\n            raise ValueError(\"mirror() must be passed a sequence or three numbers.\")\n    else:\n        v = numpy.array([a, b, c], dtype=numpy.float64)\n    v /= numpy.linalg.norm(v)\n    mirror = numpy.eye(3) - 2 * numpy.outer(v, v)\n    return LinearTransform3D(mirror @ self.inner)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.strain","title":"strain","text":"
    strain(\n    strain: float,\n    v: VecLike = (0, 0, 1),\n    poisson: float = 0.0,\n) -> LinearTransform3D\n

    Apply a strain of strain in direction v, assuming an elastically isotropic material.

    Strain is applied relative to the origin.

    With poisson=0 (default), a uniaxial strain is applied. With poisson=-1, hydrostatic strain is applied. Otherwise, a uniaxial stress is applied for a material with Poisson ratio poisson, which results in shrinkage perpendicular to the direction strain is applied.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef strain(self, strain: float, v: VecLike = (0, 0, 1), poisson: float = 0.) -> LinearTransform3D:\n    \"\"\"\n    Apply a strain of ``strain`` in direction ``v``, assuming an elastically isotropic material.\n\n    Strain is applied relative to the origin.\n\n    With ``poisson=0`` (default), a uniaxial strain is applied.\n    With ``poisson=-1``, hydrostatic strain is applied.\n    Otherwise, a uniaxial stress is applied for a material with Poisson ratio `poisson`,\n    which results in shrinkage perpendicular to the direction strain is applied.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    shrink = (1 + strain) ** -poisson\n    return self.compose(LinearTransform3D.align(v).conjugate(\n                        LinearTransform3D.scale([shrink, shrink, 1. + strain])))\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.rotate","title":"rotate","text":"
    rotate(v: VecLike, theta: Num) -> LinearTransform3D\n

    Create or append a rotation transformation of theta radians CCW around the given vector v.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef rotate(self, v: VecLike, theta: Num) -> LinearTransform3D:\n    \"\"\"\n    Create or append a rotation transformation of `theta`\n    radians CCW around the given vector `v`.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    theta = float(theta)\n    v = numpy.array(numpy.broadcast_to(v, (3,)), dtype=numpy.float64)\n    l = numpy.linalg.norm(v)\n    if numpy.isclose(l, 0.):\n        if numpy.isclose(theta, 0.):\n            # null rotation\n            return self\n        raise ValueError(\"rotate() about the zero vector is undefined.\")\n    v /= l\n\n    # Rodrigues rotation formula\n    w = numpy.array([[  0., -v[2],  v[1]],\n                     [ v[2],   0., -v[0]],\n                     [-v[1], v[0],   0.]], dtype=numpy.float64)\n    # I + sin(t) W + (1 - cos(t)) W^2 = I + sin(t) W + 2*sin^2(t/2) W^2\n    a = numpy.eye(3) + numpy.sin(theta) * w + 2 * (numpy.sin(theta / 2)**2) * w @ w\n    return LinearTransform3D(a @ self.inner)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.rotate_euler","title":"rotate_euler","text":"
    rotate_euler(\n    x: Num = 0.0, y: Num = 0.0, z: Num = 0.0\n) -> LinearTransform3D\n

    Create or append a Euler rotation transformation. Rotation is performed on the x axis first, then y axis and z axis. Values are specified in radians.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef rotate_euler(self, x: Num = 0., y: Num = 0., z: Num = 0.) -> LinearTransform3D:\n    \"\"\"\n    Create or append a Euler rotation transformation.\n    Rotation is performed on the x axis first, then y axis and z axis.\n    Values are specified in radians.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    angles = numpy.array([x, y, z], dtype=numpy.float64)\n    c, s = numpy.cos(angles), numpy.sin(angles)\n    a = numpy.array([\n        [c[1]*c[2], s[0]*s[1]*c[2] - c[0]*s[2], c[0]*s[1]*c[2] + s[0]*s[2]],\n        [c[1]*s[2], s[0]*s[1]*s[2] + c[0]*c[2], c[0]*s[1]*s[2] - s[0]*c[2]],\n        [-s[1],     s[0]*c[1],                  c[0]*c[1]],\n    ], dtype=numpy.float64)\n    return LinearTransform3D(a @ self.inner)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.align","title":"align","text":"
    align(\n    v1: VecLike, horz: Optional[VecLike] = None\n) -> LinearTransform3D\n

    Create a transformation which transforms v1 to align with [0, 0, 1]. If horz is specified, it will be aligned in the direction of [1, 0, 0].

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef align(self, v1: VecLike, horz: t.Optional[VecLike] = None) -> LinearTransform3D:\n    \"\"\"\n    Create a transformation which transforms `v1` to align with [0, 0, 1].\n    If `horz` is specified, it will be aligned in the direction of [1, 0, 0].\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    v1 = numpy.broadcast_to(v1, 3)\n    v1 = v1 / numpy.linalg.norm(v1)\n    if horz is None:\n        if numpy.isclose(v1[0], 1.):\n            # zone is [1., 0., 0.], choose a different direction\n            horz = numpy.array([0., 1., 0.])\n        else:\n            horz = numpy.array([1., 0., 0.])\n    else:\n        horz = numpy.broadcast_to(horz, 3)\n\n    return self.align_to(v1, [0., 0., 1.], horz, [1., 0., 0.])\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.align_to","title":"align_to","text":"
    align_to(\n    v1: VecLike,\n    v2: VecLike,\n    p1: Literal[None] = None,\n    p2: Literal[None] = None,\n) -> LinearTransform3D\n
    align_to(\n    v1: VecLike, v2: VecLike, p1: VecLike, p2: VecLike\n) -> LinearTransform3D\n
    align_to(\n    v1: VecLike,\n    v2: VecLike,\n    p1: Optional[VecLike] = None,\n    p2: Optional[VecLike] = None,\n) -> LinearTransform3D\n

    Create a transformation which transforms v1 to align with v2. If specified, additionally ensure that p1 aligns with p2 in the plane of v2.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef align_to(self, v1: VecLike, v2: VecLike,\n             p1: t.Optional[VecLike] = None, p2: t.Optional[VecLike] = None) -> LinearTransform3D:\n    \"\"\"\n    Create a transformation which transforms `v1` to align with `v2`.\n    If specified, additionally ensure that `p1` aligns with `p2` in the plane of `v2`.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    v1 = numpy.broadcast_to(v1, 3)\n    v1 = v1 / numpy.linalg.norm(v1)\n    v2 = numpy.broadcast_to(v2, 3)\n    v2 = v2 / numpy.linalg.norm(v2)\n\n    v3 = numpy.cross(v1, v2)\n    # rotate along v1 x v2 (geodesic rotation)\n    theta = numpy.arctan2(numpy.linalg.norm(v3), numpy.dot(v1, v2))\n    if numpy.isclose(numpy.linalg.norm(v3), 0.):\n        # any non-v1/v2 vector works. We choose the unit vector with largest cross product\n        v3 = numpy.zeros_like(v3)\n        v3[numpy.argmin(numpy.abs(v1))] = 1.\n\n    aligned = self.rotate(v3, theta)\n\n    if p1 is None and p2 is None:\n        return aligned.round_near_zero()\n    if p1 is None:\n        raise ValueError(\"If `p2` is specified, `p1` must also be specified.\")\n    if p2 is None:\n        raise ValueError(\"If `p1` is specified, `p2` must also be specified.\")\n\n    p1_align = aligned.transform(numpy.broadcast_to(p1, 3))\n    p2 = numpy.broadcast_to(p2, 3)\n    # components perpendicular to v2\n    p2_perp = perp(p2, v2)\n    p1_perp = perp(p1_align, v2)\n    # now rotate along v2\n    theta = numpy.arctan2(numpy.dot(v2, numpy.cross(p1_perp, p2_perp)), numpy.dot(p1_perp, p2_perp))\n    #theta = numpy.arctan2(numpy.linalg.norm(numpy.cross(p1_perp, p2_perp)), numpy.dot(p1_perp, p2_perp))\n    return aligned.rotate(v2, theta).round_near_zero()\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.align_standard","title":"align_standard","text":"
    align_standard() -> LinearTransform3D\n

    Align self so v1 is in the x-axis and v2 is in the xy-plane.

    This is equivalent to a \\(QR\\) decomposition which keeps only the right-triangular matrix \\(R\\).

    Source code in atomlib/transform.py
    def align_standard(self) -> LinearTransform3D:\n    \"\"\"\n    Align `self` so `v1` is in the x-axis and `v2` is in the xy-plane.\n\n    This is equivalent to a $QR$ decomposition which keeps only the\n    right-triangular matrix $R$.\n    \"\"\"\n    if self.det() <= 0:\n        raise ValueError(\"align_standard() requires a right-handed transformation\")\n\n    import scipy.linalg\n    _q, r = t.cast(t.Tuple[numpy.ndarray, numpy.ndarray], scipy.linalg.qr(self.inner))\n    # qr unique up to the sign of the digonal\n    # we choose the case where r is positive definite\n    r = r * numpy.sign(r.diagonal())[:, None]\n    assert numpy.linalg.det(r) > 0  # check our work\n    return LinearTransform3D(r).round_near_zero()\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.scale","title":"scale","text":"
    scale(x: VecLike) -> LinearTransform3D\n
    scale(\n    x: Num = 1.0,\n    y: Num = 1.0,\n    z: Num = 1.0,\n    *,\n    all: Num = 1.0\n) -> LinearTransform3D\n
    scale(\n    x: Union[Num, VecLike] = 1.0,\n    y: Num = 1.0,\n    z: Num = 1.0,\n    *,\n    all: Num = 1.0\n) -> LinearTransform3D\n

    Create or append a scaling transformation.

    Can be called as a classmethod or instance method.

    Source code in atomlib/transform.py
    @opt_classmethod\ndef scale(self, x: t.Union[Num, VecLike] = 1., y: Num = 1., z: Num = 1., *,\n          all: Num = 1.) -> LinearTransform3D:\n    \"\"\"\n    Create or append a scaling transformation.\n\n    Can be called as a classmethod or instance method.\n    \"\"\"\n    if isinstance(x, t.Sized):\n        v = numpy.broadcast_to(x, 3)\n        if y != 1. or z != 1.:\n            raise ValueError(\"scale() must be passed a sequence or three numbers.\")\n    else:\n        v = numpy.array([x, y, z])\n\n    a = numpy.zeros((3, 3), dtype=self.inner.dtype)\n    a[numpy.diag_indices(3)] = all * v\n    return LinearTransform3D(a @ self.inner)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.conjugate","title":"conjugate","text":"
    conjugate(transform: Transform3DT) -> Transform3DT\n

    Apply transform in the coordinate frame of self.

    Equivalent to an (inverse) conjugation in group theory, or \\(T^{-1} A T\\)

    Source code in atomlib/transform.py
    def conjugate(self, transform: Transform3DT) -> Transform3DT:  # type: ignore (spurious)\n    \"\"\"\n    Apply `transform` in the coordinate frame of `self`.\n\n    Equivalent to an (inverse) conjugation in group theory, or $T^{-1} A T$\n    \"\"\"\n    return self.inverse() @ self.compose(transform)\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.compose","title":"compose","text":"
    compose(other: Transform3DT) -> Transform3DT\n

    Compose this transformation with another.

    Source code in atomlib/transform.py
    def compose(self, other: Transform3DT) -> Transform3DT:  # type: ignore (spurious)\n    \"\"\"Compose this transformation with another.\"\"\"\n    if isinstance(other, LinearTransform3D):\n        return other.__class__(other.inner @ self.inner)\n    if isinstance(other, AffineTransform3D):\n        return AffineTransform3D.from_linear(self).compose(other)\n    if not isinstance(other, Transform3D):\n        raise TypeError(f\"Expected a Transform3D, got {type(other)}\")\n    elif hasattr(other, '_rcompose'):\n        return other._rcompose(self)  # type: ignore\n    raise NotImplementedError()\n
    "},{"location":"api/transform/#atomlib.transform.LinearTransform3D.transform","title":"transform","text":"
    transform(points: BBox3D) -> BBox3D\n
    transform(points: ArrayLike) -> NDArray[floating]\n
    transform(\n    points: Pts3DLike,\n) -> Union[BBox3D, NDArray[floating]]\n

    Transform points according to the given transformation.

    Source code in atomlib/transform.py
    def transform(self, points: Pts3DLike) -> t.Union[BBox3D, NDArray[numpy.floating]]:\n    \"\"\"Transform points according to the given transformation.\"\"\"\n    if isinstance(points, BBox3D):\n        return points.from_pts(self.transform(points.corners()))\n\n    pts: NDArray[numpy.floating] = numpy.atleast_1d(points).astype(self.inner.dtype)\n    if pts.shape[-1] != 3:\n        raise ValueError(f\"{self.__class__} works on 3d points only.\")\n\n    # carefully handle inf and nan\n    isnan = numpy.bitwise_or.reduce(numpy.isnan(pts), axis=-1)\n\n    with numpy.errstate(invalid='ignore'):\n        out = pts @ self.inner.T\n        isinf = numpy.isnan(out[..., 0]) & ~isnan\n\n        prod = numpy.multiply(self.inner, pts[isinf, None, :])\n\n    # inf * 0 = 0\n    prod[numpy.isnan(prod)] = 0.\n    out[isinf] = numpy.sum(prod, axis=-1)\n\n    return out\n
    "},{"location":"api/types/","title":"atomlib.types","text":""},{"location":"api/types/#atomlib.types.Vec3","title":"Vec3 module-attribute","text":"
    Vec3: TypeAlias = NDArray[floating[Any]]\n

    3D float vector, of shape (3,).

    "},{"location":"api/types/#atomlib.types.VecLike","title":"VecLike module-attribute","text":"
    VecLike: TypeAlias = ArrayLike\n

    3d vector-like

    "},{"location":"api/types/#atomlib.types.Pts3DLike","title":"Pts3DLike module-attribute","text":"
    Pts3DLike: TypeAlias = Union['BBox3D', ArrayLike]\n

    Sequence of 3d points-like

    "},{"location":"api/types/#atomlib.types.Num","title":"Num module-attribute","text":"
    Num: TypeAlias = Union[float, int]\n

    Scalar numeric type

    "},{"location":"api/types/#atomlib.types.ElemLike","title":"ElemLike module-attribute","text":"
    ElemLike: TypeAlias = Union[str, int]\n

    Element-like

    "},{"location":"api/types/#atomlib.types.ElemsLike","title":"ElemsLike module-attribute","text":"
    ElemsLike: TypeAlias = Union[\n    str,\n    int,\n    Sequence[Union[ElemLike, Tuple[ElemLike, float]]],\n]\n
    "},{"location":"api/types/#atomlib.types.ScalarT","title":"ScalarT module-attribute","text":"
    ScalarT = TypeVar('ScalarT', bound=generic)\n

    numpy.generic-bound type variable

    "},{"location":"api/types/#atomlib.types.to_vec3","title":"to_vec3","text":"
    to_vec3(v: VecLike, dtype: None = None) -> NDArray[float64]\n
    to_vec3(\n    v: VecLike, dtype: Type[ScalarT]\n) -> NDArray[ScalarT]\n
    to_vec3(\n    v: VecLike, dtype: Optional[Type[generic]] = None\n) -> NDArray[generic]\n

    Broadcast and coerce v to a Vec3 of type dtype.

    Source code in atomlib/types.py
    def to_vec3(v: VecLike, dtype: t.Optional[t.Type[numpy.generic]] = None) -> NDArray[numpy.generic]:\n    \"\"\"\n    Broadcast and coerce `v` to a [`Vec3`][atomlib.types.Vec3] of type `dtype`.\n    \"\"\"\n\n    try:\n        v = numpy.broadcast_to(v, (3,)).astype(dtype or numpy.float64)\n    except (ValueError, TypeError):\n        raise TypeError(\"Expected a vector of 3 elements.\") from None\n    return v\n
    "},{"location":"api/util/","title":"atomlib.util","text":""},{"location":"api/util/#atomlib.util.FileOrPath","title":"FileOrPath module-attribute","text":"
    FileOrPath: TypeAlias = Union[str, Path, TextIOBase, TextIO]\n

    Open text file or path to a file. Use with open_file.

    "},{"location":"api/util/#atomlib.util.BinaryFileOrPath","title":"BinaryFileOrPath module-attribute","text":"
    BinaryFileOrPath: TypeAlias = Union[\n    str, Path, TextIO, BinaryIO, IOBase\n]\n

    Open binary file or path to a file. Use with open_file_binary.

    "},{"location":"api/util/#atomlib.util.opt_classmethod","title":"opt_classmethod","text":"

    Bases: classmethod, Generic[T, P, U_co]

    Decorates a method that may be called either on an instance or the class. If called on the class, a default instance will be constructed before calling the wrapped function.

    Source code in atomlib/util.py
    class opt_classmethod(classmethod, t.Generic[T, P, U_co]):\n    \"\"\"\n    Decorates a method that may be called either on an instance or the class.\n    If called on the class, a default instance will be constructed before\n    calling the wrapped function.\n    \"\"\"\n\n    __func__: t.Callable[Concatenate[T, P], U_co]  # type: ignore\n    def __init__(self, f: t.Callable[Concatenate[T, P], U_co]):\n        super().__init__(f)  # type: ignore\n\n    def __get__(self, obj: t.Optional[T], ty: t.Optional[t.Type[T]] = None) -> t.Callable[P, U_co]:  # type: ignore\n        if obj is None:\n            if ty is None:\n                raise RuntimeError()  # pragma: no cover\n            obj = ty()\n        return t.cast(\n            t.Callable[P, U_co],\n            super().__get__(obj, obj)  # type: ignore\n        )\n
    "},{"location":"api/util/#atomlib.util.CheckedJoinError","title":"CheckedJoinError","text":"

    Bases: Exception

    Source code in atomlib/util.py
    class CheckedJoinError(Exception):\n    def __init__(self, missing_keys: t.Sequence[t.Any]):\n        self.missing_keys: t.Tuple[t.Any, ...] = tuple(missing_keys)\n        super().__init__()\n\n    def __str__(self) -> str:\n        return f\"Missing match for key(s): '{', '.join(map(repr, self.missing_keys))}'\"\n
    "},{"location":"api/util/#atomlib.util.CheckedJoinError.missing_keys","title":"missing_keys instance-attribute","text":"
    missing_keys: Tuple[Any, ...] = tuple(missing_keys)\n
    "},{"location":"api/util/#atomlib.util.map_some","title":"map_some","text":"
    map_some(\n    f: Callable[[T], U], val: Optional[T]\n) -> Optional[U]\n

    Map f over val if not None.

    Source code in atomlib/util.py
    def map_some(f: t.Callable[[T], U], val: t.Optional[T]) -> t.Optional[U]:\n    \"\"\"\n    Map `f` over `val` if not `None`.\n    \"\"\"\n    return None if val is None else f(val)\n
    "},{"location":"api/util/#atomlib.util.open_file","title":"open_file","text":"
    open_file(\n    f: FileOrPath,\n    mode: Union[Literal[\"r\"], Literal[\"w\"]] = \"r\",\n    newline: Optional[str] = None,\n    encoding: Optional[str] = \"utf-8\",\n) -> AbstractContextManager[TextIOBase]\n

    Open the given file for text I/O.

    If given a path-like, opens it with the specified settings. Otherwise, make an effort to reconfigure the encoding, and check that it is readable/writable as specified.

    Source code in atomlib/util.py
    def open_file(f: FileOrPath,\n              mode: t.Union[t.Literal['r'], t.Literal['w']] = 'r',\n              newline: t.Optional[str] = None,\n              encoding: t.Optional[str] = 'utf-8') -> AbstractContextManager[TextIOBase]:\n    \"\"\"\n    Open the given file for text I/O.\n\n    If given a path-like, opens it with the specified settings.\n    Otherwise, make an effort to reconfigure the encoding, and\n    check that it is readable/writable as specified.\n    \"\"\"\n    if not isinstance(f, (IOBase, t.BinaryIO, t.TextIO)):\n        return open(f, mode, newline=newline, encoding=encoding)\n\n    if isinstance(f, TextIOWrapper):\n        f.reconfigure(newline=newline, encoding=encoding)\n    elif isinstance(f, t.TextIO):\n        f = TextIOWrapper(f.buffer, newline=newline, encoding=encoding)\n    elif isinstance(f, (BufferedIOBase, t.BinaryIO)):\n        f = TextIOWrapper(t.cast(t.BinaryIO, f), newline=newline, encoding=encoding)\n\n    _validate_file(f, mode)\n    return nullcontext(f)  # don't close a f we didn't open\n
    "},{"location":"api/util/#atomlib.util.open_file_binary","title":"open_file_binary","text":"
    open_file_binary(\n    f: BinaryFileOrPath,\n    mode: Union[Literal[\"r\"], Literal[\"w\"]] = \"r\",\n) -> AbstractContextManager[IOBase]\n

    Open the given file for binary I/O.

    If given a path-like, opens it with the specified settings. If given text I/O, reconfigure to binary. Make sure stream is readable/writable, as specified.

    Source code in atomlib/util.py
    def open_file_binary(f: BinaryFileOrPath,\n                     mode: t.Union[t.Literal['r'], t.Literal['w']] = 'r') -> AbstractContextManager[IOBase]:\n    \"\"\"\n    Open the given file for binary I/O.\n\n    If given a path-like, opens it with the specified settings. If given text I/O,\n    reconfigure to binary. Make sure stream is readable/writable, as specified.\n    \"\"\"\n    if not isinstance(f, (IOBase, t.BinaryIO, t.TextIO)):\n        return t.cast(IOBase, open(f, mode + 'b'))\n\n    if isinstance(f, (TextIOWrapper, t.TextIO)):\n        try:\n            f = f.buffer\n        except AttributeError:\n            raise ValueError(\"Error: Couldn't get raw buffer from text file.\")\n    elif isinstance(f, StringIO):\n        if mode == 'w':\n            raise ValueError(\"Can't write binary stream to StringIO.\")\n        return BytesIO(f.getvalue().encode('utf-8'))\n    elif isinstance(f, TextIOBase):\n        raise ValueError(f\"Error: Couldn't get binary stream from text stream of type '{type(f)}'.\")\n\n    _validate_file(f, mode)\n    return nullcontext(t.cast(IOBase, f))  # don't close a file we didn't open\n
    "},{"location":"api/util/#atomlib.util.localtime","title":"localtime","text":"
    localtime() -> datetime\n

    Return the current time in a timezone-aware datetime object.

    Source code in atomlib/util.py
    def localtime() -> datetime.datetime:\n    \"\"\"Return the current time in a timezone-aware [datetime][datetime.datetime] object.\"\"\"\n    ltime = time.localtime()\n    tz = datetime.timezone(datetime.timedelta(seconds=ltime.tm_gmtoff), ltime.tm_zone)\n    return datetime.datetime.now(tz)\n
    "},{"location":"api/util/#atomlib.util.proc_seed","title":"proc_seed","text":"
    proc_seed(\n    seed: Optional[object], entropy: object\n) -> Optional[NDArray[uint32]]\n

    Process a random seed, which can be any object (or None for a random seed). Return it in a form which can be passed to numpy.random.default_rng.

    Uses a SHA-256 sum under the hood.

    entropy should be a routine-specific object, to ensure that separate random routines called using the same seed return uncorrelated results.

    Source code in atomlib/util.py
    def proc_seed(seed: t.Optional[object], entropy: object) -> t.Optional[NDArray[numpy.uint32]]:\n    \"\"\"\n    Process a random seed, which can be any object (or `None` for a random seed).\n    Return it in a form which can be passed to [numpy.random.default_rng][].\n\n    Uses a SHA-256 sum under the hood.\n\n    `entropy` should be a routine-specific object, to ensure that separate random\n    routines called using the same seed return uncorrelated results.\n    \"\"\"\n    if seed is None:\n        return None\n    # hash our seed and our extra entropy\n    state = sha256()\n    state.update(str(seed).encode('utf-8'))\n    state.update(json.dumps(entropy).encode('utf-8'))\n    return numpy.frombuffer(state.digest(), dtype=numpy.uint32)\n
    "},{"location":"api/util/#atomlib.util.checked_left_join","title":"checked_left_join","text":"
    checked_left_join(\n    lhs: DataFrame,\n    rhs: DataFrame,\n    on: Optional[str] = None,\n    *,\n    left_on: Optional[str] = None,\n    right_on: Optional[str] = None\n) -> DataFrame\n
    Source code in atomlib/util.py
    def checked_left_join(lhs: polars.DataFrame, rhs: polars.DataFrame, on: t.Optional[str] = None, *,\n                      left_on: t.Optional[str] = None, right_on: t.Optional[str] = None) -> polars.DataFrame:\n    df = lhs.join(rhs, how='inner', on=on, left_on=left_on, right_on=right_on, validate='m:1')\n\n    if len(df) < len(lhs):\n        missing_rows = lhs.join(rhs, how='anti', on=on, left_on=left_on, right_on=right_on)\n        col = t.cast(str, left_on or on)\n        missing = missing_rows.select(polars.col(col).unique()).to_series()\n        raise CheckedJoinError(tuple(missing))\n\n    return df\n
    "},{"location":"api/vec/","title":"atomlib.vec","text":"

    Helper functions for spatial vectors.

    "},{"location":"api/vec/#atomlib.vec.WindingRule","title":"WindingRule module-attribute","text":"
    WindingRule = Literal[\n    \"nonzero\", \"evenodd\", \"positive\", \"negative\"\n]\n
    "},{"location":"api/vec/#atomlib.vec.to_vec3","title":"to_vec3","text":"
    to_vec3(v: VecLike, dtype: None = None) -> NDArray[float64]\n
    to_vec3(\n    v: VecLike, dtype: Type[ScalarT]\n) -> NDArray[ScalarT]\n
    to_vec3(\n    v: VecLike, dtype: Optional[Type[generic]] = None\n) -> NDArray[generic]\n

    Broadcast and coerce v to a Vec3 of type dtype.

    Source code in atomlib/types.py
    def to_vec3(v: VecLike, dtype: t.Optional[t.Type[numpy.generic]] = None) -> NDArray[numpy.generic]:\n    \"\"\"\n    Broadcast and coerce `v` to a [`Vec3`][atomlib.types.Vec3] of type `dtype`.\n    \"\"\"\n\n    try:\n        v = numpy.broadcast_to(v, (3,)).astype(dtype or numpy.float64)\n    except (ValueError, TypeError):\n        raise TypeError(\"Expected a vector of 3 elements.\") from None\n    return v\n
    "},{"location":"api/vec/#atomlib.vec.dot","title":"dot","text":"
    dot(\n    v1: ArrayLike,\n    v2: ArrayLike,\n    axis: int = -1,\n    keepdims: bool = True,\n) -> NDArray[floating]\n

    Take the dot product between two vectors, along axis axis.

    Source code in atomlib/vec.py
    def dot(v1: ArrayLike, v2: ArrayLike, axis: int = -1, keepdims: bool = True) -> NDArray[numpy.floating]:\n    \"\"\"\n    Take the dot product between two vectors, along axis `axis`.\n    \"\"\"\n    return numpy.add.reduce(numpy.atleast_1d(v1) * numpy.atleast_1d(v2), axis=axis, keepdims=keepdims)\n
    "},{"location":"api/vec/#atomlib.vec.norm","title":"norm","text":"
    norm(v: ArrayLike) -> floating\n

    Return the norm of the vector v.

    Source code in atomlib/vec.py
    def norm(v: ArrayLike) -> numpy.floating:\n    \"\"\"\n    Return the norm of the vector `v`.\n    \"\"\"\n    return numpy.linalg.norm(v)\n
    "},{"location":"api/vec/#atomlib.vec.perp","title":"perp","text":"
    perp(v1: ArrayLike, v2: ArrayLike) -> NDArray[floating]\n

    Return the component of v1 perpendicular to v2.

    Source code in atomlib/vec.py
    def perp(v1: ArrayLike, v2: ArrayLike) -> NDArray[numpy.floating]:\n    \"\"\"Return the component of `v1` perpendicular to `v2`.\"\"\"\n    v1 = numpy.atleast_1d(v1)\n    v2 = numpy.atleast_1d(v2)\n    v2 /= norm(v2)\n    return v1 - v2 * dot(v1, v2)\n
    "},{"location":"api/vec/#atomlib.vec.para","title":"para","text":"
    para(v1: ArrayLike, v2: ArrayLike) -> NDArray[floating]\n

    Return the component of v1 parallel to v2.

    Source code in atomlib/vec.py
    def para(v1: ArrayLike, v2: ArrayLike) -> NDArray[numpy.floating]:\n    \"\"\"Return the component of `v1` parallel to `v2`.\"\"\"\n    v1 = numpy.atleast_1d(v1)\n    v2 = numpy.atleast_1d(v2)\n    v2 /= norm(v2)\n    return v2 * dot(v1, v2)\n
    "},{"location":"api/vec/#atomlib.vec.is_diagonal","title":"is_diagonal","text":"
    is_diagonal(matrix: ndarray, tol: float = 1e-10) -> bool\n

    Return if matrix is diagonal, to tolerance tol.

    Source code in atomlib/vec.py
    def is_diagonal(matrix: numpy.ndarray, tol: float = 1e-10) -> bool:\n    \"\"\"\n    Return if `matrix` is diagonal, to tolerance `tol`.\n    \"\"\"\n    d = matrix.shape[0]\n    assert matrix.shape == (d, d)\n    p, q = matrix.strides\n    offdiag = numpy.lib.stride_tricks.as_strided(matrix[:, 1:], (d-1, d), (p+q, q))\n    return bool((numpy.abs(offdiag) < tol).all())\n
    "},{"location":"api/vec/#atomlib.vec.split_arr","title":"split_arr","text":"
    split_arr(\n    a: NDArray[ScalarT], axis: int = 0\n) -> Iterator[NDArray[ScalarT]]\n

    Split the array along the axis axis.

    Source code in atomlib/vec.py
    def split_arr(a: NDArray[ScalarT], axis: int = 0) -> t.Iterator[NDArray[ScalarT]]:\n    \"\"\"\n    Split the array along the axis `axis`.\n    \"\"\"\n    return (numpy.squeeze(sub_a, axis) for sub_a in numpy.split(a, a.shape[axis], axis))\n
    "},{"location":"api/vec/#atomlib.vec.polygon_solid_angle","title":"polygon_solid_angle","text":"
    polygon_solid_angle(\n    poly: ArrayLike,\n    pts: Optional[ArrayLike] = None,\n    winding: Optional[ArrayLike] = None,\n) -> NDArray[float64]\n

    Return the signed solid angle of the polygon poly in the xy plane, as viewed from pts.

    PARAMETER DESCRIPTION poly

    Polygon(s) to compute the angle of, array of shape (..., N, 2)

    TYPE: ArrayLike

    pts

    Point(s) to view the polygons from, array of shape (..., 3)

    TYPE: Optional[ArrayLike] DEFAULT: None

    RETURNS DESCRIPTION NDArray[float64]

    A ndarray of shape broadcast(poly.shape[:-2], pts.shape[:-1]), containing signed solid angles.

    Source code in atomlib/vec.py
    def polygon_solid_angle(poly: ArrayLike, pts: t.Optional[ArrayLike] = None,\n                        winding: t.Optional[ArrayLike] = None) -> NDArray[numpy.float64]:\n    \"\"\"\n    Return the signed solid angle of the polygon `poly` in the xy plane, as viewed from `pts`.\n\n    Args:\n      poly: Polygon(s) to compute the angle of, array of shape `(..., N, 2)`\n      pts: Point(s) to view the polygons from, array of shape `(..., 3)`\n\n    Returns:\n      A ndarray of shape `broadcast(poly.shape[:-2], pts.shape[:-1])`, containing signed solid angles.\n    \"\"\"\n    poly = numpy.atleast_2d(poly).astype(numpy.float64)\n    pts = (numpy.array([0., 0., 0.]) if pts is None else numpy.atleast_1d(pts)).astype(numpy.float64)\n\n    if poly.shape[-1] == 3:\n        raise ValueError(\"Only 2d polygons are supported.\")\n    if poly.shape[-1] != 2:\n        raise ValueError(\"`poly` must be a list of 2d points.\")\n    if winding is None:\n        # calculate winding\n        winding = polygon_winding(poly)\n    else:\n        winding = numpy.asarray(winding, dtype=int)\n    # extend to 3d\n    poly = numpy.concatenate((poly, numpy.zeros_like(poly, shape=(*poly.shape[:-1], 1))), axis=-1)\n\n    if pts.shape[-1] != 3:\n        raise ValueError(\"`pts` must be a list of 3d points.\")\n\n    poly = poly - pts[..., None, :]\n    # normalize polygon points to unit sphere\n    numpy.divide(poly, numpy.linalg.norm(poly, axis=-1, keepdims=True), out=poly)\n\n    def _dot(v1: NDArray[numpy.float64], v2: NDArray[numpy.float64]) -> NDArray[numpy.float64]:\n        return numpy.add.reduce(v1 * v2, axis=-1)\n\n    # next and previous points in polygon\n    poly_n = numpy.roll(poly, -1, axis=-2)\n    poly_p = numpy.roll(poly, 1, axis=-2)\n\n    # spherical angle is 2*pi - sum(atan2(-|v1v2v3|, v1 dot v2 * v2 dot v3 - v1 dot v3))\n    angles = numpy.arctan2(_dot(poly_p, numpy.cross(poly, poly_n)), _dot(poly_p, poly) * _dot(poly, poly_n) - _dot(poly_p, poly_n))\n    angle = numpy.sum(angles, axis=-1)\n\n    # when winding is nonzero, we have to offset the calculated angle by the angle created by winding.\n    numpy.mod(angle, 4*numpy.pi*winding, out=angle, where=(winding != 0))\n    return angle - 2*numpy.pi*winding\n
    "},{"location":"api/vec/#atomlib.vec.polygon_winding","title":"polygon_winding","text":"
    polygon_winding(\n    poly: ArrayLike, pt: Optional[ArrayLike] = None\n) -> NDArray[int64]\n

    Return the winding number of the given 2d polygon poly around the point pt. If pt is not specified, return the polygon's total winding number (turning number).

    Vectorized. CCW winding is defined as positive.

    Source code in atomlib/vec.py
    def polygon_winding(poly: ArrayLike, pt: t.Optional[ArrayLike] = None) -> NDArray[numpy.int64]:\n    \"\"\"\n    Return the winding number of the given 2d polygon ``poly`` around the point ``pt``.\n    If ``pt`` is not specified, return the polygon's total winding number (turning number).\n\n    Vectorized. CCW winding is defined as positive.\n    \"\"\"\n    poly = numpy.atleast_2d(poly)\n    if poly.dtype == object:\n        raise ValueError(\"Ragged arrays not supported.\")\n    poly = poly.astype(numpy.float64)\n\n    if pt is None:\n        # return polygon's total winding number (turning number)\n        poly_next = numpy.roll(poly, -1, axis=-2)\n        # equivalent to the turning number of velocity vectors (difference vectors)\n        poly = poly_next - poly\n        # about the origin\n        pt = numpy.array([0., 0.])\n\n        # remove points at the origin (duplicate points)\n        zero_pts = (numpy.isclose(poly[..., 0], 0., atol=1e-10) & \n                    numpy.isclose(poly[..., 1], 0., atol=1e-10))\n        poly = poly[~zero_pts]\n\n    pt = numpy.atleast_1d(pt)[..., None, :].astype(numpy.float64)\n\n    # shift the polygon's origin to `pt`.\n    poly = poly - pt\n    poly_next = numpy.roll(poly, -1, axis=-2)\n    (x, y) = split_arr(poly, axis=-1)\n    (xn, yn) = split_arr(poly_next, axis=-1)\n\n    # |p1 cross (p2 - p1)| -> (p2 - p1) to right or left of origin\n    x_pos = x*(yn - y) - y*(xn - x)  # type: ignore\n    # count up crossings and down crossings\n    up_crossing = (y <= 0) & (yn > 0) & (x_pos > 0)\n    down_crossing = (y > 0) & (yn <= 0) & (x_pos < 0)\n\n    # reduce and return\n    return (numpy.sum(up_crossing, axis=-1) - numpy.sum(down_crossing, axis=-1)).astype(numpy.int64)\n
    "},{"location":"api/vec/#atomlib.vec.in_polygon","title":"in_polygon","text":"
    in_polygon(\n    poly: ndarray,\n    pt: Literal[None] = None,\n    *,\n    rule: WindingRule = \"evenodd\"\n) -> Callable[[ndarray], NDArray[bool_]]\n
    in_polygon(\n    poly: ndarray,\n    pt: ndarray,\n    *,\n    rule: WindingRule = \"evenodd\"\n) -> NDArray[bool_]\n
    in_polygon(\n    poly: ndarray,\n    pt: Optional[ndarray] = None,\n    *,\n    rule: WindingRule = \"evenodd\"\n) -> Union[\n    NDArray[bool_], Callable[[ndarray], NDArray[bool_]]\n]\n

    Return whether pt is in poly, under the given winding rule. In the one-argument form, return a closure which tests poly for the given point.

    Source code in atomlib/vec.py
    def in_polygon(poly: numpy.ndarray, pt: t.Optional[numpy.ndarray] = None, *,\n               rule: WindingRule = 'evenodd') -> t.Union[NDArray[numpy.bool_], t.Callable[[numpy.ndarray], NDArray[numpy.bool_]]]:\n    \"\"\"\n    Return whether `pt` is in `poly`, under the given winding rule.\n    In the one-argument form, return a closure which tests `poly` for the given point.\n    \"\"\"\n    if pt is None:\n        return lambda pt: in_polygon(poly, pt, rule=rule)\n    winding = polygon_winding(poly, pt)\n\n    rule = t.cast(WindingRule, rule.lower())\n    if rule == 'nonzero':\n        return winding.astype(numpy.bool_)\n    elif rule == 'evenodd':\n        return (winding & 1) > 0\n    elif rule == 'positive':\n        return winding > 0\n    elif rule == 'negative':\n        return winding < 0\n    raise ValueError(f\"Unknown winding rule '{rule}'. Expected one of \"\n                     \"'nonzero', 'evenodd', 'positive', or 'negative'.\")\n
    "},{"location":"api/vec/#atomlib.vec.reduce_vec","title":"reduce_vec","text":"
    reduce_vec(\n    arr: ArrayLike, max_denom: int = 10000\n) -> NDArray[int64]\n

    Reduce a crystallographic vector (int or float) to lowest common terms.

    "},{"location":"api/vec/#atomlib.vec.reduce_vec--examples","title":"Examples","text":"
    >>> reduce_vec([3, 6, 9])\n[1, 2, 3]\n>>> reduce_vec([0.5, 0.25, 0.25])\n[2, 1, 1]\n
    Source code in atomlib/vec.py
    def reduce_vec(arr: ArrayLike, max_denom: int = 10000) -> NDArray[numpy.int64]:\n    \"\"\"\n    Reduce a crystallographic vector (int or float) to lowest common terms.\n\n    # Examples\n    ```python\n    >>> reduce_vec([3, 6, 9])\n    [1, 2, 3]\n    >>> reduce_vec([0.5, 0.25, 0.25])\n    [2, 1, 1]\n    ```\n    \"\"\"\n\n    a = numpy.atleast_1d(arr)\n    if not numpy.issubdtype(a.dtype, numpy.floating):\n        return a // numpy.gcd.reduce(a, axis=-1, keepdims=True)\n\n    a = a / numpy.max(numpy.abs(a))\n\n    n = numpy.empty(shape=a.shape, dtype=numpy.int64)\n    d = numpy.empty(shape=a.shape, dtype=numpy.int64)\n    with numpy.nditer([a, n, d], ['refs_ok'], [['readonly'], ['writeonly'], ['writeonly']]) as it:  # type: ignore\n        for (v, n_, d_) in it:\n            (n_[()], d_[()]) = Fraction(float(v)).limit_denominator(max_denom).as_integer_ratio()\n\n    # reduce to common denominator\n    factors = numpy.lcm.reduce(d, axis=-1, keepdims=True) // d\n    n *= factors\n    # and then reduce numerators\n    return n // numpy.gcd.reduce(n, axis=-1, keepdims=True)\n
    "},{"location":"api/vec/#atomlib.vec.miller_4_to_3_vec","title":"miller_4_to_3_vec","text":"
    miller_4_to_3_vec(\n    a: NDArray[number],\n    reduce: bool = True,\n    max_denom: int = 10000,\n) -> NDArray[number]\n

    Convert a vector in 4-axis Miller-Bravais notation to 3-axis Miller notation.

    Source code in atomlib/vec.py
    def miller_4_to_3_vec(a: NDArray[numpy.number], reduce: bool = True, max_denom: int = 10000) -> NDArray[numpy.number]:\n    \"\"\"Convert a vector in 4-axis Miller-Bravais notation to 3-axis Miller notation.\"\"\"\n    a = numpy.atleast_1d(a)\n    assert a.shape[-1] == 4\n    U, V, T, W = numpy.split(a, 4, axis=-1)\n    assert numpy.allclose(-T, U + V, equal_nan=True)\n    out = numpy.concatenate((2*U + V, 2*V + U, W), axis=-1)\n    return reduce_vec(out, max_denom) if reduce else out\n
    "},{"location":"api/vec/#atomlib.vec.miller_3_to_4_vec","title":"miller_3_to_4_vec","text":"
    miller_3_to_4_vec(\n    a: NDArray[number],\n    reduce: bool = True,\n    max_denom: int = 10000,\n) -> NDArray[number]\n

    Convert a vector in 3-axis Miller notation to 4-axis Miller-Bravais notation.

    Source code in atomlib/vec.py
    def miller_3_to_4_vec(a: NDArray[numpy.number], reduce: bool = True, max_denom: int = 10000) -> NDArray[numpy.number]:\n    \"\"\"Convert a vector in 3-axis Miller notation to 4-axis Miller-Bravais notation.\"\"\"\n    a = numpy.atleast_1d(a)\n    assert a.shape[-1] == 3\n    u, v, w = numpy.split(a, 3, axis=-1)\n    U = 2*u - v\n    V = 2*v - u\n    W = 3*w\n    out = numpy.concatenate((U, V, -(U + V), W), axis=-1)\n    return reduce_vec(out, max_denom) if reduce else out\n
    "},{"location":"api/vec/#atomlib.vec.miller_4_to_3_plane","title":"miller_4_to_3_plane","text":"
    miller_4_to_3_plane(\n    a: NDArray[number],\n    reduce: bool = True,\n    max_denom: int = 10000,\n) -> NDArray[number]\n

    Convert a plane in 4-axis Miller-Bravais notation to 3-axis Miller notation.

    Source code in atomlib/vec.py
    def miller_4_to_3_plane(a: NDArray[numpy.number], reduce: bool = True, max_denom: int = 10000) -> NDArray[numpy.number]:\n    \"\"\"Convert a plane in 4-axis Miller-Bravais notation to 3-axis Miller notation.\"\"\"\n    a = numpy.atleast_1d(a)\n    assert a.shape[-1] == 4\n    h, k, i, l = numpy.split(a, 4, axis=-1)  # noqa: E741\n    assert numpy.allclose(-i, h + k, equal_nan=True)\n    out = numpy.concatenate((h, k, l), axis=-1)\n    return reduce_vec(out, max_denom) if reduce else out\n
    "},{"location":"api/vec/#atomlib.vec.miller_3_to_4_plane","title":"miller_3_to_4_plane","text":"
    miller_3_to_4_plane(\n    a: NDArray[number],\n    reduce: bool = True,\n    max_denom: int = 10000,\n) -> NDArray[number]\n

    Convert a plane in 3-axis Miller notation to 4-axis Miller-Bravais notation.

    Source code in atomlib/vec.py
    def miller_3_to_4_plane(a: NDArray[numpy.number], reduce: bool = True, max_denom: int = 10000) -> NDArray[numpy.number]:\n    \"\"\"Convert a plane in 3-axis Miller notation to 4-axis Miller-Bravais notation.\"\"\"\n    a = numpy.atleast_1d(a)\n    assert a.shape[-1] == 3\n    h, k, l = numpy.split(a, 3, axis=-1)  # noqa: E741\n    out = numpy.concatenate((h, k, -(h + k), l), axis=-1)\n    return reduce_vec(out, max_denom) if reduce else out\n
    "},{"location":"api/visualize/","title":"atomlib.visualize","text":"

    Visualization of atomic structures. Useful for debugging.

    "},{"location":"api/visualize/#atomlib.visualize.BackendName","title":"BackendName module-attribute","text":"
    BackendName: TypeAlias = Literal['mpl', 'ase']\n
    "},{"location":"api/visualize/#atomlib.visualize.AtomStyle","title":"AtomStyle module-attribute","text":"
    AtomStyle: TypeAlias = Literal[\n    \"spacefill\", \"ballstick\", \"small\"\n]\n
    "},{"location":"api/visualize/#atomlib.visualize.AtomImage","title":"AtomImage","text":"

    Bases: ABC

    Source code in atomlib/visualize/__init__.py
    class AtomImage(ABC):\n    @abstractmethod\n    def save(self, f: FileOrPath):\n        ...\n
    "},{"location":"api/visualize/#atomlib.visualize.AtomImage.save","title":"save abstractmethod","text":"
    save(f: FileOrPath)\n
    Source code in atomlib/visualize/__init__.py
    @abstractmethod\ndef save(self, f: FileOrPath):\n    ...\n
    "},{"location":"api/visualize/#atomlib.visualize.AtomImageMpl","title":"AtomImageMpl","text":"

    Bases: Figure, AtomImage

    Source code in atomlib/visualize/__init__.py
    class AtomImageMpl(Figure, AtomImage):\n    def __new__(cls, fig: Figure):\n        fig.__class__ = cls\n        return fig\n\n    def __init__(self, fig: Figure):\n        ...\n\n    def save(self, f: FileOrPath):\n        return self.savefig(f)  # type: ignore\n
    "},{"location":"api/visualize/#atomlib.visualize.AtomImageMpl.save","title":"save","text":"
    save(f: FileOrPath)\n
    Source code in atomlib/visualize/__init__.py
    def save(self, f: FileOrPath):\n    return self.savefig(f)  # type: ignore\n
    "},{"location":"api/visualize/#atomlib.visualize.show_atoms_3d","title":"show_atoms_3d","text":"
    show_atoms_3d(\n    atoms: HasAtoms,\n    *,\n    zone: Optional[VecLike] = None,\n    plane: Optional[VecLike] = None,\n    backend: BackendName = \"mpl\",\n    style: AtomStyle = \"small\",\n    **kwargs: Any\n) -> AtomImage\n

    Show atoms on a 3D plot, using backend backend (defaults to matplotlib).

    Source code in atomlib/visualize/__init__.py
    def show_atoms_3d(atoms: HasAtoms, *,\n                  zone: t.Optional[VecLike] = None,\n                  plane: t.Optional[VecLike] = None,\n                  backend: BackendName = 'mpl',\n                  style: AtomStyle = 'small', **kwargs: t.Any) -> AtomImage:\n    \"\"\"\n    Show `atoms` on a 3D plot, using backend `backend` (defaults to matplotlib).\n    \"\"\"\n\n    backend = t.cast(BackendName, backend.lower())\n    if backend == 'mpl':\n        return show_atoms_mpl_3d(atoms, zone=zone, plane=plane, style=style, **kwargs)\n    elif backend == 'ase':\n        raise NotImplementedError()\n\n    raise ValueError(f\"Unknown backend '{backend}'\")\n
    "},{"location":"api/visualize/#atomlib.visualize.show_atoms_2d","title":"show_atoms_2d","text":"
    show_atoms_2d(\n    atoms: HasAtoms,\n    *,\n    zone: Optional[VecLike] = None,\n    plane: Optional[VecLike] = None,\n    horz: Optional[VecLike] = None,\n    backend: BackendName = \"mpl\",\n    style: AtomStyle = \"small\",\n    **kwargs: Any\n) -> AtomImage\n

    Show atoms on a 2D plot, using backend backend (defaults to matplotlib).

    Source code in atomlib/visualize/__init__.py
    def show_atoms_2d(atoms: HasAtoms, *,\n                  zone: t.Optional[VecLike] = None,\n                  plane: t.Optional[VecLike] = None,\n                  horz: t.Optional[VecLike] = None,\n                  backend: BackendName = 'mpl',\n                  style: AtomStyle = 'small', **kwargs: t.Any) -> AtomImage:\n    \"\"\"\n    Show `atoms` on a 2D plot, using backend `backend` (defaults to matplotlib).\n    \"\"\"\n\n    backend = t.cast(BackendName, backend.lower())\n    if backend == 'mpl':\n        return show_atoms_mpl_2d(atoms, zone=zone, plane=plane, horz=horz, style=style, **kwargs)\n    elif backend == 'ase':\n        raise NotImplementedError()\n\n    raise ValueError(f\"Unknown backend '{backend}'\")\n
    "},{"location":"api/visualize/#atomlib.visualize.get_elem_color","title":"get_elem_color","text":"
    get_elem_color(elem: int) -> List[int]\n

    Get the color to use for element elem.

    Source code in atomlib/visualize/__init__.py
    def get_elem_color(elem: int) -> t.List[int]:\n    \"\"\"Get the color to use for element `elem`.\"\"\"\n    # grey fallback\n    return _ELEM_MAP.get(elem, [80, 80, 80])\n
    "},{"location":"api/visualize/#atomlib.visualize.get_zone","title":"get_zone","text":"
    get_zone(\n    atoms: HasAtoms,\n    zone: Optional[VecLike] = None,\n    plane: Optional[VecLike] = None,\n    default: Optional[VecLike] = None,\n) -> NDArray[float64]\n

    Get the zone axis corresponding to the arguments zone, plane, and default.

    Source code in atomlib/visualize/__init__.py
    def get_zone(atoms: HasAtoms, zone: t.Optional[VecLike] = None,\n             plane: t.Optional[VecLike] = None,\n             default: t.Optional[VecLike] = None) -> NDArray[numpy.float64]:\n    \"\"\"\n    Get the zone axis corresponding to the arguments `zone`, `plane`, and `default`.\n    \"\"\"\n\n    if zone is not None and plane is not None:\n        raise ValueError(\"'zone' and 'plane' can't both be specified.\")\n    if plane is not None:\n        if isinstance(atoms, AtomCell) and not atoms.is_orthogonal():\n            # convert plane into zone\n            raise NotImplementedError()\n        zone = plane\n    if zone is not None:\n        return numpy.broadcast_to(zone, 3).astype(numpy.float64)\n    if default is not None:\n        return numpy.broadcast_to(default, 3).astype(numpy.float64)\n    return numpy.array([0., 0., 1.], dtype=numpy.float64)\n
    "},{"location":"api/visualize/#atomlib.visualize.get_plot_radii","title":"get_plot_radii","text":"
    get_plot_radii(\n    atoms: HasAtoms,\n    min_r: Optional[float] = 1.0,\n    style: AtomStyle = \"small\",\n) -> NDArray[float64]\n

    Get the radii to use for each atom in atoms.

    Source code in atomlib/visualize/__init__.py
    def get_plot_radii(atoms: HasAtoms, min_r: t.Optional[float] = 1.0, style: AtomStyle = 'small') -> NDArray[numpy.float64]:\n    \"\"\"Get the radii to use for each atom in `atoms`.\"\"\"\n    radii = get_radius(atoms['elem']).to_numpy()\n    if style == 'small':\n        radii = radii * 0.6\n    elif style == 'ballstick':\n        radii = radii * 0.5\n    elif style == 'spacefill':\n        radii = radii * 1.0\n    else:\n        raise ValueError(f\"Unknown atom style '{style}'. Expected 'spacefill', 'ballstick', or 'small'.\")\n    if min_r is not None:\n        return numpy.maximum(min_r, radii)\n    return radii\n
    "},{"location":"api/visualize/#atomlib.visualize.get_azim_elev","title":"get_azim_elev","text":"
    get_azim_elev(zone: VecLike) -> Tuple[float, float]\n

    Get the azimuth and elevation corresponding to the zone zone.

    Source code in atomlib/visualize/__init__.py
    def get_azim_elev(zone: VecLike) -> t.Tuple[float, float]:\n    \"\"\"Get the azimuth and elevation corresponding to the zone `zone`.\"\"\"\n    (a, b, c) = -to_vec3(zone)  # look down zone\n    # todo: aren't these just arctan2s?\n    return (numpy.angle(a + b*1.j, deg=True), numpy.angle(numpy.sqrt(a**2 + b**2) + c*1.j, deg=True))  # type: ignore\n
    "},{"location":"api/visualize/#atomlib.visualize.show_atoms_mpl_3d","title":"show_atoms_mpl_3d","text":"
    show_atoms_mpl_3d(\n    atoms: HasAtoms,\n    *,\n    fig: Optional[Figure] = None,\n    zone: Optional[VecLike] = None,\n    plane: Optional[VecLike] = None,\n    min_r: Optional[float] = 1.0,\n    style: AtomStyle = \"small\"\n) -> AtomImageMpl\n

    Show atoms on a 3D plot using matplotlib.

    Source code in atomlib/visualize/__init__.py
    def show_atoms_mpl_3d(atoms: HasAtoms, *, fig: t.Optional[Figure] = None,\n                      zone: t.Optional[VecLike] = None,\n                      plane: t.Optional[VecLike] = None,\n                      min_r: t.Optional[float] = 1.0,\n                      style: AtomStyle = 'small') -> AtomImageMpl:\n    \"\"\"Show `atoms` on a 3D plot using matplotlib.\"\"\"\n\n    fig = AtomImageMpl(fig or pyplot.figure())  # type: ignore\n\n    zone = get_zone(atoms, zone, plane, [1., 2., 4.])\n    (azim, elev) = get_azim_elev(zone)\n\n    rect = (0., 0., 1., 1.)\n    ax: Axes3D = fig.add_axes(rect, axes_class=Axes3D, proj_type='ortho', azim=azim, elev=elev)  # type: ignore\n    ax.grid(False)\n\n    bbox = atoms.bbox().pad(0.2)\n    ax.set_xlim3d(bbox.x)  # type: ignore\n    ax.set_ylim3d(bbox.y)  # type: ignore\n    ax.set_zlim3d(bbox.z)  # type: ignore\n    ax.set_box_aspect(bbox.size)\n\n    ax.set_xlabel('X')\n    ax.set_ylabel('Y')\n    ax.set_zlabel('Z')\n\n    frame = atoms.get_atoms('local')\n    #radii = get_plot_radii(atoms, min_r=min_r, style=style)\n    coords = frame.coords()\n    elem_colors = numpy.array(list(map(get_elem_color, frame['elem']))) / 255.\n    s = 100\n\n    if isinstance(atoms, HasCell):\n        # plot cell corners\n        corners = atoms.corners('global')\n        faces = [\n            numpy.array([\n                corners[(val*2**axis + v1*2**((axis+1) % 3) + v2*2**((axis+2) % 3))]\n                for (v1, v2) in [(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]\n            ])\n            for axis in (0, 1, 2)\n            for val in (0, 1)\n        ]\n        for face in faces:\n            ax.plot3D(*split_arr(face, axis=-1), '.-k', alpha=1, markersize=8)\n\n    ax.scatter(coords[:, 0], coords[:, 1], coords[:, 2], c=elem_colors, alpha=1, s=s)  # type: ignore\n\n    return t.cast(AtomImageMpl, fig)\n
    "},{"location":"api/visualize/#atomlib.visualize.show_atoms_mpl_2d","title":"show_atoms_mpl_2d","text":"
    show_atoms_mpl_2d(\n    atoms: HasAtoms,\n    *,\n    fig: Optional[Figure] = None,\n    zone: Optional[VecLike] = None,\n    plane: Optional[VecLike] = None,\n    horz: Optional[VecLike] = None,\n    min_r: Optional[float] = 1.0,\n    style: AtomStyle = \"small\"\n) -> AtomImageMpl\n

    Show atoms on a 2D plot using matplotlib.

    Source code in atomlib/visualize/__init__.py
    def show_atoms_mpl_2d(atoms: HasAtoms, *, fig: t.Optional[Figure] = None,\n                      zone: t.Optional[VecLike] = None,\n                      plane: t.Optional[VecLike] = None,\n                      horz: t.Optional[VecLike] = None,\n                      min_r: t.Optional[float] = 1.0,\n                      style: AtomStyle = 'small') -> AtomImageMpl:\n    \"\"\"Show `atoms` on a 2D plot using matplotlib.\"\"\"\n\n    zone = get_zone(atoms, zone, plane, [0., 0., 1.])\n    fig = AtomImageMpl(fig or pyplot.figure())  # type: ignore\n\n    rect = (0.05, 0.05, 0.95, 0.95)\n    ax: Axes = fig.add_axes(rect)\n    ax.set_aspect('equal')\n\n    frame = atoms.get_atoms('local')\n    coords = frame.coords()\n    elem_colors = numpy.array(list(map(get_elem_color, frame['elem']))) / 255.\n    radii = get_plot_radii(frame, min_r=min_r, style=style)\n\n    # look down zone\n    if horz is None:\n        transform = LinearTransform3D.align_to(zone, [0, 0, -1])\n    else:\n        transform = LinearTransform3D.align_to(zone, [0, 0, -1], horz, [1, 0, 0])\n    bbox_2d = transform @ atoms.bbox()\n    coords = transform @ coords\n    # sort by z-order\n    sort = numpy.argsort(coords[..., 2])\n    coords = coords[sort]\n    elem_colors = elem_colors[sort]\n    radii = radii[sort]\n\n    ax.set_xbound(*bbox_2d.x)\n    ax.set_ybound(*bbox_2d.y)\n\n    # old plotting method\n    # ax.scatter(coords[:, 0], coords[:, 1], c=elem_colors, alpha=1., s=s)\n\n    ax.add_collection(EllipseCollection(\n        radii, radii, numpy.zeros_like(radii), units='xy', facecolors=elem_colors, ec='black',\n        offsets=coords[..., :2], offset_transform=ax.transData,\n    ))  # type: ignore (bad api typing)\n\n    return t.cast(AtomImageMpl, fig)\n
    "},{"location":"api/io/","title":"atomlib.io","text":""},{"location":"api/io/#atomlib.io.FileType","title":"FileType module-attribute","text":"
    FileType: TypeAlias = Literal[\n    \"cif\", \"xyz\", \"xsf\", \"cfg\", \"lmp\", \"mslice\", \"qe\"\n]\n
    "},{"location":"api/io/#atomlib.io.ReadFunc","title":"ReadFunc module-attribute","text":"
    ReadFunc = Callable[[FileOrPath], HasAtoms]\n
    "},{"location":"api/io/#atomlib.io.WriteFunc","title":"WriteFunc module-attribute","text":"
    WriteFunc = Callable[[HasAtoms, FileOrPath], None]\n
    "},{"location":"api/io/#atomlib.io.CIF","title":"CIF dataclass","text":"Source code in atomlib/io/cif.py
    @dataclass\nclass CIF:\n    data_blocks: t.Tuple[CIFDataBlock, ...]\n\n    def __post_init__(self):\n        # ensure that all data_blocks after the first have a name\n        for data_block in self.data_blocks[1:]:\n            if data_block.name is None:\n                data_block.name = \"\"\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> CIF:\n        return CIF(tuple(CIFDataBlock.from_file(file)))\n\n    def __len__(self) -> int:\n        return self.data_blocks.__len__()\n\n    def get_block(self, block: t.Union[int, str]) -> CIFDataBlock:\n        try:\n            if isinstance(block, int):\n                return self.data_blocks[block]\n            return next(b for b in self.data_blocks if b.name == block)\n        except (IndexError, StopIteration):\n            raise ValueError(f\"Couldn't find block '{block}' in CIF file. File has {len(self)} blocks.\")\n\n    def write(self, file: FileOrPath):\n        with open_file(file, 'w') as f:\n            print(\"# generated by atomlib\", file=f, end=None)\n            for data_block in self.data_blocks:\n                print(file=f)\n                data_block._write(f)\n
    "},{"location":"api/io/#atomlib.io.CIF.data_blocks","title":"data_blocks instance-attribute","text":"
    data_blocks: Tuple[CIFDataBlock, ...]\n
    "},{"location":"api/io/#atomlib.io.CIF.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> CIF\n
    Source code in atomlib/io/cif.py
    @staticmethod\ndef from_file(file: FileOrPath) -> CIF:\n    return CIF(tuple(CIFDataBlock.from_file(file)))\n
    "},{"location":"api/io/#atomlib.io.CIF.get_block","title":"get_block","text":"
    get_block(block: Union[int, str]) -> CIFDataBlock\n
    Source code in atomlib/io/cif.py
    def get_block(self, block: t.Union[int, str]) -> CIFDataBlock:\n    try:\n        if isinstance(block, int):\n            return self.data_blocks[block]\n        return next(b for b in self.data_blocks if b.name == block)\n    except (IndexError, StopIteration):\n        raise ValueError(f\"Couldn't find block '{block}' in CIF file. File has {len(self)} blocks.\")\n
    "},{"location":"api/io/#atomlib.io.CIF.write","title":"write","text":"
    write(file: FileOrPath)\n
    Source code in atomlib/io/cif.py
    def write(self, file: FileOrPath):\n    with open_file(file, 'w') as f:\n        print(\"# generated by atomlib\", file=f, end=None)\n        for data_block in self.data_blocks:\n            print(file=f)\n            data_block._write(f)\n
    "},{"location":"api/io/#atomlib.io.XYZ","title":"XYZ dataclass","text":"Source code in atomlib/io/xyz.py
    @dataclass\nclass XYZ:\n    atoms: polars.DataFrame\n    comment: t.Optional[str] = None\n    params: t.Dict[str, str] = field(default_factory=dict)\n\n    @staticmethod\n    def from_atoms(atoms: HasAtoms) -> XYZ:\n        params = {}\n        if isinstance(atoms, HasAtomCell):\n            coords = atoms.get_cell().to_ortho().to_linear().inner.ravel()\n            lattice_str = \" \".join((f\"{c:.8f}\" for c in coords))\n            params['Lattice'] = lattice_str\n\n            pbc_str = \" \".join(str(int(v)) for v in atoms.get_cell().pbc)\n            params['pbc'] = pbc_str\n\n        return XYZ(\n            atoms.get_atoms('local')._get_frame(),\n            params=params\n        )\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> XYZ:\n        logging.info(f\"Loading XYZ {file.name if hasattr(file, 'name') else file!r}...\")  # type: ignore\n\n        with open_file(file, 'r') as f:\n            try:\n                # TODO be more gracious about whitespace here\n                length = int(f.readline())\n            except ValueError:\n                raise ValueError(\"Error parsing XYZ file: Invalid length\") from None\n            except IOError as e:\n                raise IOError(f\"Error parsing XYZ file: {e}\") from None\n\n            comment = f.readline().rstrip('\\n')\n            # TODO handle if there's not a gap here\n\n            try:\n                params = ExtXYZParser(comment).parse()\n            except ValueError:\n                params = None\n\n            schema = _get_columns_from_params(params)\n\n            df = parse_whitespace_separated(f, schema, start_line=1)\n\n            # map atomic numbers -> symbols (on columns which are Int8)\n            df = df.with_columns(\n                get_sym(df.select(polars.col('symbol').cast(polars.Int8, strict=False)).to_series())\n                    .fill_null(df['symbol']).alias('symbol')\n            )\n            # ensure all symbols are recognizable (this will raise ValueError if not)\n            get_elem(df['symbol'])\n\n            if length < len(df):\n                warnings.warn(f\"Warning: truncating structure of length {len(df)} \"\n                            f\"to match declared length of {length}\")\n                df = df[:length]\n            elif length > len(df):\n                warnings.warn(f\"Warning: structure length {len(df)} doesn't match \"\n                            f\"declared length {length}.\\nData could be corrupted.\")\n\n            try:\n                params = ExtXYZParser(comment).parse()\n                return XYZ(df, comment, params)\n            except ValueError:\n                pass\n\n            return XYZ(df, comment)\n\n    def write(self, file: FileOrPath, fmt: XYZFormat = 'exyz'):\n        with open_file(file, 'w', newline='\\r\\n') as f:\n\n            f.write(f\"{len(self.atoms)}\\n\")\n            if len(self.params) > 0 and fmt == 'exyz':\n                f.write(\" \".join(_param_strings(self.params)))\n            else:\n                f.write(self.comment or \"\")\n            f.write(\"\\n\")\n\n            # not my best work\n            col_space = (3, 12, 12, 12)\n            f.writelines(\n                \"\".join(\n                    f\"{val:< {space}.8f}\" if isinstance(val, float) else f\"{val:<{space}}\" for (val, space) in zip(_flatten(row), col_space)\n                ).strip() + '\\n' for row in self.atoms.select(('symbol', 'coords')).rows()\n            )\n\n    def cell_matrix(self) -> t.Optional[NDArray[numpy.float64]]:\n        if (s := self.params.get('Lattice')) is None:\n            return None\n\n        try:\n            items = list(map(float, s.split()))\n            if not len(items) == 9:\n                raise ValueError(\"Invalid length\")\n            return numpy.array(items, dtype=numpy.float64).reshape((3, 3)).T\n        except ValueError:\n            warnings.warn(f\"Warning: Invalid format for key 'Lattice=\\\"{s}\\\"'\")\n        return None\n\n    def pbc(self) -> t.Optional[NDArray[numpy.bool_]]:\n        if (s := self.params.get('pbc')) is None:\n            return None\n\n        val_map = {'0': False, 'f': False, '1': True, 't': True}\n        try:\n            items = [val_map[v.lower()] for v in s.split()]\n            if not len(items) == 3:\n                raise ValueError(\"Invalid length\")\n            return numpy.array(items, dtype=numpy.bool_)\n        except ValueError:\n            warnings.warn(f\"Warning: Invalid format for key 'pbc=\\\"{s}\\\"'\")\n        return None\n
    "},{"location":"api/io/#atomlib.io.XYZ.atoms","title":"atoms instance-attribute","text":"
    atoms: DataFrame\n
    "},{"location":"api/io/#atomlib.io.XYZ.comment","title":"comment class-attribute instance-attribute","text":"
    comment: Optional[str] = None\n
    "},{"location":"api/io/#atomlib.io.XYZ.params","title":"params class-attribute instance-attribute","text":"
    params: Dict[str, str] = field(default_factory=dict)\n
    "},{"location":"api/io/#atomlib.io.XYZ.from_atoms","title":"from_atoms staticmethod","text":"
    from_atoms(atoms: HasAtoms) -> XYZ\n
    Source code in atomlib/io/xyz.py
    @staticmethod\ndef from_atoms(atoms: HasAtoms) -> XYZ:\n    params = {}\n    if isinstance(atoms, HasAtomCell):\n        coords = atoms.get_cell().to_ortho().to_linear().inner.ravel()\n        lattice_str = \" \".join((f\"{c:.8f}\" for c in coords))\n        params['Lattice'] = lattice_str\n\n        pbc_str = \" \".join(str(int(v)) for v in atoms.get_cell().pbc)\n        params['pbc'] = pbc_str\n\n    return XYZ(\n        atoms.get_atoms('local')._get_frame(),\n        params=params\n    )\n
    "},{"location":"api/io/#atomlib.io.XYZ.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> XYZ\n
    Source code in atomlib/io/xyz.py
    @staticmethod\ndef from_file(file: FileOrPath) -> XYZ:\n    logging.info(f\"Loading XYZ {file.name if hasattr(file, 'name') else file!r}...\")  # type: ignore\n\n    with open_file(file, 'r') as f:\n        try:\n            # TODO be more gracious about whitespace here\n            length = int(f.readline())\n        except ValueError:\n            raise ValueError(\"Error parsing XYZ file: Invalid length\") from None\n        except IOError as e:\n            raise IOError(f\"Error parsing XYZ file: {e}\") from None\n\n        comment = f.readline().rstrip('\\n')\n        # TODO handle if there's not a gap here\n\n        try:\n            params = ExtXYZParser(comment).parse()\n        except ValueError:\n            params = None\n\n        schema = _get_columns_from_params(params)\n\n        df = parse_whitespace_separated(f, schema, start_line=1)\n\n        # map atomic numbers -> symbols (on columns which are Int8)\n        df = df.with_columns(\n            get_sym(df.select(polars.col('symbol').cast(polars.Int8, strict=False)).to_series())\n                .fill_null(df['symbol']).alias('symbol')\n        )\n        # ensure all symbols are recognizable (this will raise ValueError if not)\n        get_elem(df['symbol'])\n\n        if length < len(df):\n            warnings.warn(f\"Warning: truncating structure of length {len(df)} \"\n                        f\"to match declared length of {length}\")\n            df = df[:length]\n        elif length > len(df):\n            warnings.warn(f\"Warning: structure length {len(df)} doesn't match \"\n                        f\"declared length {length}.\\nData could be corrupted.\")\n\n        try:\n            params = ExtXYZParser(comment).parse()\n            return XYZ(df, comment, params)\n        except ValueError:\n            pass\n\n        return XYZ(df, comment)\n
    "},{"location":"api/io/#atomlib.io.XYZ.write","title":"write","text":"
    write(file: FileOrPath, fmt: XYZFormat = 'exyz')\n
    Source code in atomlib/io/xyz.py
    def write(self, file: FileOrPath, fmt: XYZFormat = 'exyz'):\n    with open_file(file, 'w', newline='\\r\\n') as f:\n\n        f.write(f\"{len(self.atoms)}\\n\")\n        if len(self.params) > 0 and fmt == 'exyz':\n            f.write(\" \".join(_param_strings(self.params)))\n        else:\n            f.write(self.comment or \"\")\n        f.write(\"\\n\")\n\n        # not my best work\n        col_space = (3, 12, 12, 12)\n        f.writelines(\n            \"\".join(\n                f\"{val:< {space}.8f}\" if isinstance(val, float) else f\"{val:<{space}}\" for (val, space) in zip(_flatten(row), col_space)\n            ).strip() + '\\n' for row in self.atoms.select(('symbol', 'coords')).rows()\n        )\n
    "},{"location":"api/io/#atomlib.io.XYZ.cell_matrix","title":"cell_matrix","text":"
    cell_matrix() -> Optional[NDArray[float64]]\n
    Source code in atomlib/io/xyz.py
    def cell_matrix(self) -> t.Optional[NDArray[numpy.float64]]:\n    if (s := self.params.get('Lattice')) is None:\n        return None\n\n    try:\n        items = list(map(float, s.split()))\n        if not len(items) == 9:\n            raise ValueError(\"Invalid length\")\n        return numpy.array(items, dtype=numpy.float64).reshape((3, 3)).T\n    except ValueError:\n        warnings.warn(f\"Warning: Invalid format for key 'Lattice=\\\"{s}\\\"'\")\n    return None\n
    "},{"location":"api/io/#atomlib.io.XYZ.pbc","title":"pbc","text":"
    pbc() -> Optional[NDArray[bool_]]\n
    Source code in atomlib/io/xyz.py
    def pbc(self) -> t.Optional[NDArray[numpy.bool_]]:\n    if (s := self.params.get('pbc')) is None:\n        return None\n\n    val_map = {'0': False, 'f': False, '1': True, 't': True}\n    try:\n        items = [val_map[v.lower()] for v in s.split()]\n        if not len(items) == 3:\n            raise ValueError(\"Invalid length\")\n        return numpy.array(items, dtype=numpy.bool_)\n    except ValueError:\n        warnings.warn(f\"Warning: Invalid format for key 'pbc=\\\"{s}\\\"'\")\n    return None\n
    "},{"location":"api/io/#atomlib.io.XSF","title":"XSF dataclass","text":"Source code in atomlib/io/xsf.py
    @dataclass\nclass XSF:\n    periodicity: Periodicity = 'crystal'\n    primitive_cell: t.Optional[LinearTransform3D] = None\n    conventional_cell: t.Optional[LinearTransform3D] = None\n\n    prim_coords: t.Optional[polars.DataFrame] = None\n    conv_coords: t.Optional[polars.DataFrame] = None\n    atoms: t.Optional[polars.DataFrame] = None\n\n    def get_atoms(self) -> polars.DataFrame:\n        if self.prim_coords is not None:\n            return self.prim_coords\n        if self.atoms is not None:\n            return self.atoms\n        if self.conv_coords is not None:\n            raise NotImplementedError()  # TODO untransform conv_coords by conventional_cell?\n        raise ValueError(\"No coordinates specified in XSF file.\")\n\n    def get_pbc(self) -> NDArray[numpy.bool_]:\n        return _periodicity_to_pbc(self.periodicity)\n\n    @staticmethod\n    def from_cell(cell: HasAtomCell) -> XSF:\n        ortho = cell.get_transform('local', 'cell_box').to_linear()\n        return XSF(\n            primitive_cell=ortho,\n            conventional_cell=ortho,\n            prim_coords=cell.get_atoms('linear').inner,\n            periodicity=_pbc_to_periodicity(cell.pbc)\n        )\n\n    @staticmethod\n    def from_atoms(atoms: HasAtoms) -> XSF:\n        return XSF(\n            periodicity='molecule',\n            atoms=atoms.get_atoms('local').inner\n        )\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> XSF:\n        logging.info(f\"Loading XSF {file.name if hasattr(file, 'name') else file!r}...\")  # type: ignore\n        with open_file(file) as f:\n            return XSFParser(f).parse()\n\n    def __post_init__(self):\n        if self.prim_coords is None and self.conv_coords is None and self.atoms is None:\n            raise ValueError(\"Error: No coordinates are specified (atoms, primitive, or conventional).\")\n\n        if self.prim_coords is not None and self.conv_coords is not None:\n            logging.warning(\"Warning: Both 'primcoord' and 'convcoord' are specified. 'convcoord' will be ignored.\")\n        elif self.conv_coords is not None and self.conventional_cell is None:\n            raise ValueError(\"If 'convcoord' is specified, 'convvec' must be specified as well.\")\n\n        if self.periodicity == 'molecule':\n            if self.atoms is None:\n                raise ValueError(\"'atoms' must be specified for molecules.\")\n\n    def write(self, path: FileOrPath):\n        with open_file(path, 'w') as f:\n            print(self.periodicity.upper(), file=f)\n            if self.primitive_cell is not None:\n                print('PRIMVEC', file=f)\n                self._write_cell(f, self.primitive_cell)\n            if self.conventional_cell is not None:\n                print('CONVVEC', file=f)\n                self._write_cell(f, self.conventional_cell)\n            print(file=f)\n\n            if self.prim_coords is not None:\n                print(\"PRIMCOORD\", file=f)\n                print(f\"{len(self.prim_coords)} 1\", file=f)\n                self._write_coords(f, self.prim_coords)\n            if self.conv_coords is not None:\n                print(\"CONVCOORD\", file=f)\n                print(f\"{len(self.conv_coords)} 1\", file=f)\n                self._write_coords(f, self.conv_coords)\n            if self.atoms is not None:\n                print(\"ATOMS\", file=f)\n                self._write_coords(f, self.atoms)\n\n    def _write_cell(self, f: TextIOBase, cell: LinearTransform3D):\n        for row in cell.inner.T:\n            for val in row:\n                f.write(f\"{val:12.7f}\")\n            f.write('\\n')\n\n    def _write_coords(self, f: TextIOBase, coords: polars.DataFrame):\n        for (elem, [x, y, z]) in coords.select(['elem', 'coords']).rows():\n            print(f\"{elem:2d} {x:11.6f} {y:11.6f} {z:11.6f}\", file=f)\n        print(file=f)\n
    "},{"location":"api/io/#atomlib.io.XSF.periodicity","title":"periodicity class-attribute instance-attribute","text":"
    periodicity: Periodicity = 'crystal'\n
    "},{"location":"api/io/#atomlib.io.XSF.primitive_cell","title":"primitive_cell class-attribute instance-attribute","text":"
    primitive_cell: Optional[LinearTransform3D] = None\n
    "},{"location":"api/io/#atomlib.io.XSF.conventional_cell","title":"conventional_cell class-attribute instance-attribute","text":"
    conventional_cell: Optional[LinearTransform3D] = None\n
    "},{"location":"api/io/#atomlib.io.XSF.prim_coords","title":"prim_coords class-attribute instance-attribute","text":"
    prim_coords: Optional[DataFrame] = None\n
    "},{"location":"api/io/#atomlib.io.XSF.conv_coords","title":"conv_coords class-attribute instance-attribute","text":"
    conv_coords: Optional[DataFrame] = None\n
    "},{"location":"api/io/#atomlib.io.XSF.atoms","title":"atoms class-attribute instance-attribute","text":"
    atoms: Optional[DataFrame] = None\n
    "},{"location":"api/io/#atomlib.io.XSF.get_atoms","title":"get_atoms","text":"
    get_atoms() -> DataFrame\n
    Source code in atomlib/io/xsf.py
    def get_atoms(self) -> polars.DataFrame:\n    if self.prim_coords is not None:\n        return self.prim_coords\n    if self.atoms is not None:\n        return self.atoms\n    if self.conv_coords is not None:\n        raise NotImplementedError()  # TODO untransform conv_coords by conventional_cell?\n    raise ValueError(\"No coordinates specified in XSF file.\")\n
    "},{"location":"api/io/#atomlib.io.XSF.get_pbc","title":"get_pbc","text":"
    get_pbc() -> NDArray[bool_]\n
    Source code in atomlib/io/xsf.py
    def get_pbc(self) -> NDArray[numpy.bool_]:\n    return _periodicity_to_pbc(self.periodicity)\n
    "},{"location":"api/io/#atomlib.io.XSF.from_cell","title":"from_cell staticmethod","text":"
    from_cell(cell: HasAtomCell) -> XSF\n
    Source code in atomlib/io/xsf.py
    @staticmethod\ndef from_cell(cell: HasAtomCell) -> XSF:\n    ortho = cell.get_transform('local', 'cell_box').to_linear()\n    return XSF(\n        primitive_cell=ortho,\n        conventional_cell=ortho,\n        prim_coords=cell.get_atoms('linear').inner,\n        periodicity=_pbc_to_periodicity(cell.pbc)\n    )\n
    "},{"location":"api/io/#atomlib.io.XSF.from_atoms","title":"from_atoms staticmethod","text":"
    from_atoms(atoms: HasAtoms) -> XSF\n
    Source code in atomlib/io/xsf.py
    @staticmethod\ndef from_atoms(atoms: HasAtoms) -> XSF:\n    return XSF(\n        periodicity='molecule',\n        atoms=atoms.get_atoms('local').inner\n    )\n
    "},{"location":"api/io/#atomlib.io.XSF.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> XSF\n
    Source code in atomlib/io/xsf.py
    @staticmethod\ndef from_file(file: FileOrPath) -> XSF:\n    logging.info(f\"Loading XSF {file.name if hasattr(file, 'name') else file!r}...\")  # type: ignore\n    with open_file(file) as f:\n        return XSFParser(f).parse()\n
    "},{"location":"api/io/#atomlib.io.XSF.write","title":"write","text":"
    write(path: FileOrPath)\n
    Source code in atomlib/io/xsf.py
    def write(self, path: FileOrPath):\n    with open_file(path, 'w') as f:\n        print(self.periodicity.upper(), file=f)\n        if self.primitive_cell is not None:\n            print('PRIMVEC', file=f)\n            self._write_cell(f, self.primitive_cell)\n        if self.conventional_cell is not None:\n            print('CONVVEC', file=f)\n            self._write_cell(f, self.conventional_cell)\n        print(file=f)\n\n        if self.prim_coords is not None:\n            print(\"PRIMCOORD\", file=f)\n            print(f\"{len(self.prim_coords)} 1\", file=f)\n            self._write_coords(f, self.prim_coords)\n        if self.conv_coords is not None:\n            print(\"CONVCOORD\", file=f)\n            print(f\"{len(self.conv_coords)} 1\", file=f)\n            self._write_coords(f, self.conv_coords)\n        if self.atoms is not None:\n            print(\"ATOMS\", file=f)\n            self._write_coords(f, self.atoms)\n
    "},{"location":"api/io/#atomlib.io.CFG","title":"CFG dataclass","text":"Source code in atomlib/io/cfg.py
    @dataclass\nclass CFG:\n    atoms: polars.DataFrame\n\n    cell: LinearTransform3D\n    transform: t.Optional[LinearTransform3D] = None\n    eta: t.Optional[LinearTransform3D] = None\n\n    length_scale: t.Optional[float] = None\n    length_unit: t.Optional[str] = None\n    rate_scale: t.Optional[float] = None\n    rate_unit: t.Optional[str] = None\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> CFG:\n        with open_file(file, 'r') as f:\n            return CFGParser(f).parse()\n\n    @staticmethod\n    def from_atoms(atoms: HasAtoms) -> CFG:\n        if isinstance(atoms, HasAtomCell):\n            cell = atoms.get_transform('cell_box').inverse().to_linear()\n            atoms = atoms.get_atoms('cell_box')\n        else:\n            cell = LinearTransform3D.identity()\n\n        # ensure we have masses and velocities\n        atoms = atoms.with_mass().with_velocity()\n        return CFG(atoms._get_frame(), cell, length_scale=1.0, length_unit=\"Angstrom\")\n\n    def write(self, file: FileOrPath):\n        with open_file(file, 'w', newline='\\r\\n') as f:\n            f.write(f\"Number of particles = {len(self.atoms)}\\n\")\n\n            if self.length_scale is not None:\n                unit = f\" [{self.length_unit}]\" if self.length_unit is not None else \"\"\n                f.write(f\"\\nA = {self.length_scale:.8}{unit}\\n\\n\")\n\n            cell = self.cell.inner\n            for (i, j) in product(range(3), repeat=2):\n                f.write(f\"H0({i+1},{j+1}) = {cell[j,i]:.8} A\\n\")\n\n            if self.transform is not None:\n                f.write(\"\\n\")\n                transform = self.transform.inner\n                for (i, j) in product(range(3), repeat=2):\n                    f.write(f\"Transform({i+1},{j+1}) = {transform[j,i]:.8}\\n\")\n\n            if self.eta is not None:\n                f.write(\"\\n\")\n                eta = self.eta.inner\n                for i in range(3):\n                    for j in range(i, 3):\n                        f.write(f\"eta({i+1},{j+1}) = {eta[j,i]:.8}\\n\")\n\n            if self.rate_scale is not None:\n                unit = f\" [{self.rate_unit}]\" if self.rate_unit is not None else \"\"\n                f.write(f\"\\nR = {self.rate_scale:.8}{unit}\\n\")\n\n            f.write(\"\\n\")\n            for row in self.atoms.select(('mass', 'symbol', 'coords', 'velocity')).rows():\n                (mass, sym, (x, y, z), (v_x, v_y, v_z)) = row\n                f.write(f\"{mass:.4f} {sym:>2} {x:.8} {y:.8} {z:.8} {v_x:.8} {v_y:.8} {v_z:.8}\\n\")\n
    "},{"location":"api/io/#atomlib.io.CFG.atoms","title":"atoms instance-attribute","text":"
    atoms: DataFrame\n
    "},{"location":"api/io/#atomlib.io.CFG.cell","title":"cell instance-attribute","text":"
    cell: LinearTransform3D\n
    "},{"location":"api/io/#atomlib.io.CFG.transform","title":"transform class-attribute instance-attribute","text":"
    transform: Optional[LinearTransform3D] = None\n
    "},{"location":"api/io/#atomlib.io.CFG.eta","title":"eta class-attribute instance-attribute","text":"
    eta: Optional[LinearTransform3D] = None\n
    "},{"location":"api/io/#atomlib.io.CFG.length_scale","title":"length_scale class-attribute instance-attribute","text":"
    length_scale: Optional[float] = None\n
    "},{"location":"api/io/#atomlib.io.CFG.length_unit","title":"length_unit class-attribute instance-attribute","text":"
    length_unit: Optional[str] = None\n
    "},{"location":"api/io/#atomlib.io.CFG.rate_scale","title":"rate_scale class-attribute instance-attribute","text":"
    rate_scale: Optional[float] = None\n
    "},{"location":"api/io/#atomlib.io.CFG.rate_unit","title":"rate_unit class-attribute instance-attribute","text":"
    rate_unit: Optional[str] = None\n
    "},{"location":"api/io/#atomlib.io.CFG.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> CFG\n
    Source code in atomlib/io/cfg.py
    @staticmethod\ndef from_file(file: FileOrPath) -> CFG:\n    with open_file(file, 'r') as f:\n        return CFGParser(f).parse()\n
    "},{"location":"api/io/#atomlib.io.CFG.from_atoms","title":"from_atoms staticmethod","text":"
    from_atoms(atoms: HasAtoms) -> CFG\n
    Source code in atomlib/io/cfg.py
    @staticmethod\ndef from_atoms(atoms: HasAtoms) -> CFG:\n    if isinstance(atoms, HasAtomCell):\n        cell = atoms.get_transform('cell_box').inverse().to_linear()\n        atoms = atoms.get_atoms('cell_box')\n    else:\n        cell = LinearTransform3D.identity()\n\n    # ensure we have masses and velocities\n    atoms = atoms.with_mass().with_velocity()\n    return CFG(atoms._get_frame(), cell, length_scale=1.0, length_unit=\"Angstrom\")\n
    "},{"location":"api/io/#atomlib.io.CFG.write","title":"write","text":"
    write(file: FileOrPath)\n
    Source code in atomlib/io/cfg.py
    def write(self, file: FileOrPath):\n    with open_file(file, 'w', newline='\\r\\n') as f:\n        f.write(f\"Number of particles = {len(self.atoms)}\\n\")\n\n        if self.length_scale is not None:\n            unit = f\" [{self.length_unit}]\" if self.length_unit is not None else \"\"\n            f.write(f\"\\nA = {self.length_scale:.8}{unit}\\n\\n\")\n\n        cell = self.cell.inner\n        for (i, j) in product(range(3), repeat=2):\n            f.write(f\"H0({i+1},{j+1}) = {cell[j,i]:.8} A\\n\")\n\n        if self.transform is not None:\n            f.write(\"\\n\")\n            transform = self.transform.inner\n            for (i, j) in product(range(3), repeat=2):\n                f.write(f\"Transform({i+1},{j+1}) = {transform[j,i]:.8}\\n\")\n\n        if self.eta is not None:\n            f.write(\"\\n\")\n            eta = self.eta.inner\n            for i in range(3):\n                for j in range(i, 3):\n                    f.write(f\"eta({i+1},{j+1}) = {eta[j,i]:.8}\\n\")\n\n        if self.rate_scale is not None:\n            unit = f\" [{self.rate_unit}]\" if self.rate_unit is not None else \"\"\n            f.write(f\"\\nR = {self.rate_scale:.8}{unit}\\n\")\n\n        f.write(\"\\n\")\n        for row in self.atoms.select(('mass', 'symbol', 'coords', 'velocity')).rows():\n            (mass, sym, (x, y, z), (v_x, v_y, v_z)) = row\n            f.write(f\"{mass:.4f} {sym:>2} {x:.8} {y:.8} {z:.8} {v_x:.8} {v_y:.8} {v_z:.8}\\n\")\n
    "},{"location":"api/io/#atomlib.io.LMP","title":"LMP dataclass","text":"Source code in atomlib/io/lmp.py
    @dataclass\nclass LMP:\n    comment: t.Optional[str]\n    headers: t.Dict[str, t.Any]\n    sections: t.Tuple[LMPSection, ...]\n\n    def get_cell(self) -> Cell:\n        dims = numpy.array([\n            self.headers.get(f\"{c}lo {c}hi\", (-0.5, 0.5))\n            for c in \"xyz\"\n        ])\n        origin = dims[:, 0]\n        tilts = self.headers.get(\"xy xz yz\", (0., 0., 0.))\n\n        ortho = numpy.diag(dims[:, 1] - dims[:, 0])\n        (ortho[0, 1], ortho[0, 2], ortho[1, 2]) = tilts\n\n        return Cell.from_ortho(LinearTransform3D(ortho).translate(origin))\n\n    def get_atoms(self, type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> AtomCell:\n        if type_map is not None:\n            try:\n                type_map_df = polars.DataFrame({\n                    'type': polars.Series(type_map.keys(), dtype=polars.Int32),\n                    'elem': polars.Series(list(map(get_elem, type_map.values())), dtype=polars.UInt8),\n                    'symbol': polars.Series([get_sym(v) if isinstance(v, int) else v for v in type_map.values()], dtype=polars.Utf8),\n                })\n            except ValueError as e:\n                raise ValueError(\"Invalid type map\") from e\n        else:\n            type_map_df = None\n\n        cell = self.get_cell()\n\n        def _apply_type_labels(df: polars.DataFrame, section_name: str, labels: t.Optional[polars.DataFrame] = None) -> polars.DataFrame:\n            if labels is not None:\n                #df = df.with_columns(polars.col('type').replace(d, default=polars.col('type').cast(polars.Int32, strict=False), return_dtype=polars.Int32))\n                df = df.with_columns(polars.col('type').replace_strict(labels['symbol'], labels['type'], default=polars.col('type').cast(polars.Int32, strict=False), return_dtype=polars.Int32))\n                if df['type'].is_null().any():\n                    raise ValueError(f\"While parsing section {section_name}: Unknown atom label or invalid atom type\")\n            try:\n                return df.with_columns(polars.col('type').cast(polars.Int32))\n            except polars.ComputeError:\n                raise ValueError(f\"While parsing section {section_name}: Invalid atom type(s)\")\n\n        atoms: t.Optional[polars.DataFrame] = None\n        labels: t.Optional[polars.DataFrame] = None\n        masses: t.Optional[polars.DataFrame] = None\n        velocities = None\n\n        for section in self.sections:\n            start_line = section.start_line + 1\n\n            if section.name == 'Atoms':\n                if section.style not in (None, 'atomic'):\n                    # TODO support other styles\n                    raise ValueError(f\"Only 'atomic' atom_style is supported, instead got '{section.style}'\")\n\n                atoms = parse_whitespace_separated(section.body, {\n                    'i': polars.Int64, 'type': polars.Utf8,\n                    'coords': polars.Array(polars.Float64, 3),\n                }, start_line=start_line)\n                atoms = _apply_type_labels(atoms, 'Atoms', labels)\n            elif section.name == 'Atom Type Labels':\n                labels = parse_whitespace_separated(section.body, {'type': polars.Int32, 'symbol': polars.Utf8}, start_line=start_line)\n            elif section.name == 'Masses':\n                masses = parse_whitespace_separated(section.body, {'type': polars.Utf8, 'mass': polars.Float64}, start_line=start_line)\n                masses = _apply_type_labels(masses, 'Masses', labels)\n            elif section.name == 'Velocities':\n                velocities = parse_whitespace_separated(section.body, {\n                    'i': polars.Int64, 'velocity': polars.Array(polars.Float64, 3),\n                }, start_line=start_line)\n\n        # now all 'type's should be in Int32\n\n        if atoms is None:\n            if self.headers['atoms'] > 0:\n                raise ValueError(\"Missing required section 'Atoms'\")\n            return AtomCell(Atoms.empty(), cell=cell, frame='local')\n\n        # next we need to assign element symbols\n        # first, if type_map is specified, use that:\n        #if type_map_elem is not None and type_map_sym is not None:\n        if type_map_df is not None:\n            try:\n                atoms = checked_left_join(atoms, type_map_df, on='type')\n            except CheckedJoinError as e:\n                raise ValueError(f\"Missing type_map specification for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n        elif labels is not None:\n            try:\n                labels = labels.with_columns(get_elem(labels['symbol']))\n            except ValueError as e:\n                raise ValueError(\"Failed to auto-detect elements from type labels. Please pass 'type_map' explicitly\") from e\n            try:\n                atoms = checked_left_join(atoms, labels, 'type')\n            except CheckedJoinError as e:\n                raise ValueError(f\"Missing labels for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n        # otherwise we have no way\n        else:\n            raise ValueError(\"Failed to auto-detect elements from type labels. Please pass 'type_map' explicitly\")\n\n        if velocities is not None:\n            # join velocities\n            try:\n                # TODO use join_asof here?\n                atoms = checked_left_join(atoms, velocities, 'i')\n            except CheckedJoinError as e:\n                raise ValueError(f\"Missing velocities for {len(e.missing_keys)}/{len(atoms)} atoms\")\n\n        if masses is not None:\n            # join masses\n            try:\n                atoms = checked_left_join(atoms, masses, 'type')\n            except CheckedJoinError as e:\n                raise ValueError(f\"Missing masses for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n\n        return AtomCell(atoms, cell=cell, frame='local')\n\n    @staticmethod\n    def from_atoms(atoms: HasAtoms) -> LMP:\n        if isinstance(atoms, HasAtomCell):\n            # we're basically converting everything to the ortho frame, but including the affine shift\n\n            # transform affine shift into ortho frame\n            origin = atoms.get_transform('ortho', 'local').to_linear().round_near_zero() \\\n                .transform(atoms.get_cell().affine.translation())\n\n            # get the orthogonalization transform only, without affine\n            ortho = atoms.get_transform('ortho', 'cell_box').to_linear().round_near_zero().inner\n\n            # get atoms in ortho frame, and then add the affine shift\n            frame = atoms.get_atoms('ortho').transform_atoms(AffineTransform3D.translate(origin)) \\\n                .round_near_zero().with_type()\n        else:\n            bbox = atoms.bbox_atoms()\n            ortho = numpy.diag(bbox.size)\n            origin = bbox.min\n\n            frame = atoms.get_atoms('local').with_type()\n\n        types = frame.unique(subset='type')\n        types = types.with_mass().sort('type')\n\n        now = localtime()\n        comment = f\"# Generated by atomlib on {now.isoformat(' ', 'seconds')}\"\n\n        headers = {}\n        sections = []\n\n        headers['atoms'] = len(frame)\n        headers['atom types'] = len(types)\n\n        for (s, low, diff) in zip(('x', 'y', 'z'), origin, ortho.diagonal()):\n            headers[f\"{s}lo {s}hi\"] = (low, low + diff)\n\n        headers['xy xz yz'] = (ortho[0, 1], ortho[0, 2], ortho[1, 2])\n\n        body = [\n            f\" {ty:8} {sym:>4}\\n\"\n            for (ty, sym) in types.select('type', 'symbol').rows()\n        ]\n        sections.append(LMPSection(\"Atom Type Labels\", tuple(body)))\n\n        if 'mass' in types:\n            body = [\n                f\" {ty:8} {mass:14.7f}  # {sym}\\n\"\n                for (ty, sym, mass) in types.select(('type', 'symbol', 'mass')).rows()\n            ]\n            sections.append(LMPSection(\"Masses\", tuple(body)))\n\n        body = [\n            f\" {i+1:8} {ty:4} {x:14.7f} {y:14.7f} {z:14.7f}\\n\"\n            for (i, (ty, (x, y, z))) in enumerate(frame.select(('type', 'coords')).rows())\n        ]\n        sections.append(LMPSection(\"Atoms\", tuple(body), 'atomic'))\n\n        if (velocities := frame.velocities()) is not None:\n            body = [\n                f\" {i+1:8} {v_x:14.7f} {v_y:14.7f} {v_z:14.7f}\\n\"\n                for (i, (v_x, v_y, v_z)) in enumerate(velocities)\n            ]\n            sections.append(LMPSection(\"Velocities\", tuple(body)))\n\n        return LMP(comment, headers, tuple(sections))\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> LMP:\n        with open_file(file, 'r') as f:\n            return LMPReader(f).parse()\n\n    def write(self, file: FileOrPath):\n        with open_file(file, 'w') as f:\n            print((self.comment or \"\") + '\\n', file=f)\n\n            # print headers\n            for (name, val) in self.headers.items():\n                val = _HEADER_FMT.get(name, lambda s: f\"{s:8}\")(val)\n                print(f\" {val} {name}\", file=f)\n\n            # print sections\n            for section in self.sections:\n                line = section.name\n                if section.style is not None:\n                    line += f'  # {section.style}'\n                print(f\"\\n{line}\\n\", file=f)\n\n                f.writelines(section.body)\n
    "},{"location":"api/io/#atomlib.io.LMP.comment","title":"comment instance-attribute","text":"
    comment: Optional[str]\n
    "},{"location":"api/io/#atomlib.io.LMP.headers","title":"headers instance-attribute","text":"
    headers: Dict[str, Any]\n
    "},{"location":"api/io/#atomlib.io.LMP.sections","title":"sections instance-attribute","text":"
    sections: Tuple[LMPSection, ...]\n
    "},{"location":"api/io/#atomlib.io.LMP.get_cell","title":"get_cell","text":"
    get_cell() -> Cell\n
    Source code in atomlib/io/lmp.py
    def get_cell(self) -> Cell:\n    dims = numpy.array([\n        self.headers.get(f\"{c}lo {c}hi\", (-0.5, 0.5))\n        for c in \"xyz\"\n    ])\n    origin = dims[:, 0]\n    tilts = self.headers.get(\"xy xz yz\", (0., 0., 0.))\n\n    ortho = numpy.diag(dims[:, 1] - dims[:, 0])\n    (ortho[0, 1], ortho[0, 2], ortho[1, 2]) = tilts\n\n    return Cell.from_ortho(LinearTransform3D(ortho).translate(origin))\n
    "},{"location":"api/io/#atomlib.io.LMP.get_atoms","title":"get_atoms","text":"
    get_atoms(\n    type_map: Optional[Dict[int, Union[str, int]]] = None\n) -> AtomCell\n
    Source code in atomlib/io/lmp.py
    def get_atoms(self, type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> AtomCell:\n    if type_map is not None:\n        try:\n            type_map_df = polars.DataFrame({\n                'type': polars.Series(type_map.keys(), dtype=polars.Int32),\n                'elem': polars.Series(list(map(get_elem, type_map.values())), dtype=polars.UInt8),\n                'symbol': polars.Series([get_sym(v) if isinstance(v, int) else v for v in type_map.values()], dtype=polars.Utf8),\n            })\n        except ValueError as e:\n            raise ValueError(\"Invalid type map\") from e\n    else:\n        type_map_df = None\n\n    cell = self.get_cell()\n\n    def _apply_type_labels(df: polars.DataFrame, section_name: str, labels: t.Optional[polars.DataFrame] = None) -> polars.DataFrame:\n        if labels is not None:\n            #df = df.with_columns(polars.col('type').replace(d, default=polars.col('type').cast(polars.Int32, strict=False), return_dtype=polars.Int32))\n            df = df.with_columns(polars.col('type').replace_strict(labels['symbol'], labels['type'], default=polars.col('type').cast(polars.Int32, strict=False), return_dtype=polars.Int32))\n            if df['type'].is_null().any():\n                raise ValueError(f\"While parsing section {section_name}: Unknown atom label or invalid atom type\")\n        try:\n            return df.with_columns(polars.col('type').cast(polars.Int32))\n        except polars.ComputeError:\n            raise ValueError(f\"While parsing section {section_name}: Invalid atom type(s)\")\n\n    atoms: t.Optional[polars.DataFrame] = None\n    labels: t.Optional[polars.DataFrame] = None\n    masses: t.Optional[polars.DataFrame] = None\n    velocities = None\n\n    for section in self.sections:\n        start_line = section.start_line + 1\n\n        if section.name == 'Atoms':\n            if section.style not in (None, 'atomic'):\n                # TODO support other styles\n                raise ValueError(f\"Only 'atomic' atom_style is supported, instead got '{section.style}'\")\n\n            atoms = parse_whitespace_separated(section.body, {\n                'i': polars.Int64, 'type': polars.Utf8,\n                'coords': polars.Array(polars.Float64, 3),\n            }, start_line=start_line)\n            atoms = _apply_type_labels(atoms, 'Atoms', labels)\n        elif section.name == 'Atom Type Labels':\n            labels = parse_whitespace_separated(section.body, {'type': polars.Int32, 'symbol': polars.Utf8}, start_line=start_line)\n        elif section.name == 'Masses':\n            masses = parse_whitespace_separated(section.body, {'type': polars.Utf8, 'mass': polars.Float64}, start_line=start_line)\n            masses = _apply_type_labels(masses, 'Masses', labels)\n        elif section.name == 'Velocities':\n            velocities = parse_whitespace_separated(section.body, {\n                'i': polars.Int64, 'velocity': polars.Array(polars.Float64, 3),\n            }, start_line=start_line)\n\n    # now all 'type's should be in Int32\n\n    if atoms is None:\n        if self.headers['atoms'] > 0:\n            raise ValueError(\"Missing required section 'Atoms'\")\n        return AtomCell(Atoms.empty(), cell=cell, frame='local')\n\n    # next we need to assign element symbols\n    # first, if type_map is specified, use that:\n    #if type_map_elem is not None and type_map_sym is not None:\n    if type_map_df is not None:\n        try:\n            atoms = checked_left_join(atoms, type_map_df, on='type')\n        except CheckedJoinError as e:\n            raise ValueError(f\"Missing type_map specification for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n    elif labels is not None:\n        try:\n            labels = labels.with_columns(get_elem(labels['symbol']))\n        except ValueError as e:\n            raise ValueError(\"Failed to auto-detect elements from type labels. Please pass 'type_map' explicitly\") from e\n        try:\n            atoms = checked_left_join(atoms, labels, 'type')\n        except CheckedJoinError as e:\n            raise ValueError(f\"Missing labels for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n    # otherwise we have no way\n    else:\n        raise ValueError(\"Failed to auto-detect elements from type labels. Please pass 'type_map' explicitly\")\n\n    if velocities is not None:\n        # join velocities\n        try:\n            # TODO use join_asof here?\n            atoms = checked_left_join(atoms, velocities, 'i')\n        except CheckedJoinError as e:\n            raise ValueError(f\"Missing velocities for {len(e.missing_keys)}/{len(atoms)} atoms\")\n\n    if masses is not None:\n        # join masses\n        try:\n            atoms = checked_left_join(atoms, masses, 'type')\n        except CheckedJoinError as e:\n            raise ValueError(f\"Missing masses for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n\n    return AtomCell(atoms, cell=cell, frame='local')\n
    "},{"location":"api/io/#atomlib.io.LMP.from_atoms","title":"from_atoms staticmethod","text":"
    from_atoms(atoms: HasAtoms) -> LMP\n
    Source code in atomlib/io/lmp.py
    @staticmethod\ndef from_atoms(atoms: HasAtoms) -> LMP:\n    if isinstance(atoms, HasAtomCell):\n        # we're basically converting everything to the ortho frame, but including the affine shift\n\n        # transform affine shift into ortho frame\n        origin = atoms.get_transform('ortho', 'local').to_linear().round_near_zero() \\\n            .transform(atoms.get_cell().affine.translation())\n\n        # get the orthogonalization transform only, without affine\n        ortho = atoms.get_transform('ortho', 'cell_box').to_linear().round_near_zero().inner\n\n        # get atoms in ortho frame, and then add the affine shift\n        frame = atoms.get_atoms('ortho').transform_atoms(AffineTransform3D.translate(origin)) \\\n            .round_near_zero().with_type()\n    else:\n        bbox = atoms.bbox_atoms()\n        ortho = numpy.diag(bbox.size)\n        origin = bbox.min\n\n        frame = atoms.get_atoms('local').with_type()\n\n    types = frame.unique(subset='type')\n    types = types.with_mass().sort('type')\n\n    now = localtime()\n    comment = f\"# Generated by atomlib on {now.isoformat(' ', 'seconds')}\"\n\n    headers = {}\n    sections = []\n\n    headers['atoms'] = len(frame)\n    headers['atom types'] = len(types)\n\n    for (s, low, diff) in zip(('x', 'y', 'z'), origin, ortho.diagonal()):\n        headers[f\"{s}lo {s}hi\"] = (low, low + diff)\n\n    headers['xy xz yz'] = (ortho[0, 1], ortho[0, 2], ortho[1, 2])\n\n    body = [\n        f\" {ty:8} {sym:>4}\\n\"\n        for (ty, sym) in types.select('type', 'symbol').rows()\n    ]\n    sections.append(LMPSection(\"Atom Type Labels\", tuple(body)))\n\n    if 'mass' in types:\n        body = [\n            f\" {ty:8} {mass:14.7f}  # {sym}\\n\"\n            for (ty, sym, mass) in types.select(('type', 'symbol', 'mass')).rows()\n        ]\n        sections.append(LMPSection(\"Masses\", tuple(body)))\n\n    body = [\n        f\" {i+1:8} {ty:4} {x:14.7f} {y:14.7f} {z:14.7f}\\n\"\n        for (i, (ty, (x, y, z))) in enumerate(frame.select(('type', 'coords')).rows())\n    ]\n    sections.append(LMPSection(\"Atoms\", tuple(body), 'atomic'))\n\n    if (velocities := frame.velocities()) is not None:\n        body = [\n            f\" {i+1:8} {v_x:14.7f} {v_y:14.7f} {v_z:14.7f}\\n\"\n            for (i, (v_x, v_y, v_z)) in enumerate(velocities)\n        ]\n        sections.append(LMPSection(\"Velocities\", tuple(body)))\n\n    return LMP(comment, headers, tuple(sections))\n
    "},{"location":"api/io/#atomlib.io.LMP.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> LMP\n
    Source code in atomlib/io/lmp.py
    @staticmethod\ndef from_file(file: FileOrPath) -> LMP:\n    with open_file(file, 'r') as f:\n        return LMPReader(f).parse()\n
    "},{"location":"api/io/#atomlib.io.LMP.write","title":"write","text":"
    write(file: FileOrPath)\n
    Source code in atomlib/io/lmp.py
    def write(self, file: FileOrPath):\n    with open_file(file, 'w') as f:\n        print((self.comment or \"\") + '\\n', file=f)\n\n        # print headers\n        for (name, val) in self.headers.items():\n            val = _HEADER_FMT.get(name, lambda s: f\"{s:8}\")(val)\n            print(f\" {val} {name}\", file=f)\n\n        # print sections\n        for section in self.sections:\n            line = section.name\n            if section.style is not None:\n                line += f'  # {section.style}'\n            print(f\"\\n{line}\\n\", file=f)\n\n            f.writelines(section.body)\n
    "},{"location":"api/io/#atomlib.io.write_mslice","title":"write_mslice","text":"
    write_mslice(\n    cell: HasAtomCell,\n    f: BinaryFileOrPath,\n    template: Optional[MSliceFile] = None,\n    *,\n    slice_thickness: Optional[float] = None,\n    scan_points: Optional[ArrayLike] = None,\n    scan_extent: Optional[ArrayLike] = None,\n    noise_sigma: Optional[float] = None,\n    conv_angle: Optional[float] = None,\n    energy: Optional[float] = None,\n    defocus: Optional[float] = None,\n    tilt: Optional[Tuple[float, float]] = None,\n    tds: Optional[bool] = None,\n    n_cells: Optional[ArrayLike] = None\n)\n

    Write a structure to an mslice file. The structure must be orthogonal and aligned with the local coordinate system. It should be periodic in X and Y.

    template may be a file, path, or ElementTree containing an existing mslice file. Its structure will be modified to make the final output. If not specified, a default template will be used.

    Additional options modify simulation properties. If an option is not specified, the template's properties are used.

    Source code in atomlib/io/mslice.py
    def write_mslice(cell: HasAtomCell, f: BinaryFileOrPath, template: t.Optional[MSliceFile] = None, *,\n                 slice_thickness: t.Optional[float] = None,  # angstrom\n                 scan_points: t.Optional[ArrayLike] = None,\n                 scan_extent: t.Optional[ArrayLike] = None,\n                 noise_sigma: t.Optional[float] = None,  # angstrom\n                 conv_angle: t.Optional[float] = None,  # mrad\n                 energy: t.Optional[float] = None,  # keV\n                 defocus: t.Optional[float] = None,  # angstrom\n                 tilt: t.Optional[t.Tuple[float, float]] = None,  # (mrad, mrad)\n                 tds: t.Optional[bool] = None,\n                 n_cells: t.Optional[ArrayLike] = None):\n    \"\"\"\n    Write a structure to an mslice file. The structure must be orthogonal and aligned\n    with the local coordinate system. It should be periodic in X and Y.\n\n    ``template`` may be a file, path, or ElementTree containing an existing mslice file.\n    Its structure will be modified to make the final output. If not specified, a default\n    template will be used.\n\n    Additional options modify simulation properties. If an option is not specified, the\n    template's properties are used.\n    \"\"\"\n    #if not issubclass(type(cell), HasAtomCell):\n    #    raise TypeError(\"mslice format requires an AtomCell.\")\n\n    if not cell.is_orthogonal_in_local():\n        raise ValueError(\"mslice requires an orthogonal AtomCell.\")\n\n    if not numpy.all(cell.pbc[:2]):\n        warn(\"AtomCell may not be periodic\", UserWarning, stacklevel=2)\n\n    box_size = cell._box_size_in_local()\n\n    # get atoms in local frame (which we verified aligns with the cell's axes)\n    # then scale into fractional coordinates\n    atoms = cell.get_atoms('linear') \\\n        .transform(AffineTransform3D.scale(1/box_size)) \\\n        .with_wobble().with_occupancy()\n\n    out: ElementTree\n    if template is None:\n        out = default_template()\n    elif not isinstance(template, ElementTree):\n        with open_file(template, 'r') as temp:\n            out = et.parse(temp, None)\n    else:\n        out = deepcopy(template)\n\n    # TODO clean up this code\n    db: t.Optional[Element] = out.getroot() if out.getroot().tag == 'database' else out.find(\"./database\", None)\n    if db is None:\n        raise ValueError(\"Couldn't find 'database' tag in template.\")\n\n    struct = db.find(\".//object[@type='STRUCTURE']\", None)\n    if struct is None:\n        raise ValueError(\"Couldn't find STRUCTURE object in template.\")\n\n    params = db.find(\".//object[@type='SIMPARAMETERS']\", None)\n    if params is None:\n        raise ValueError(\"Couldn't find SIMPARAMETERS object in template.\")\n\n    microscope = db.find(\".//object[@type='MICROSCOPE']\", None)\n    if microscope is None:\n        raise ValueError(\"Couldn't find MICROSCOPE object in template.\")\n\n    scan = db.find(\".//object[@type='SCAN']\", None)\n    aberrations = db.findall(\".//object[@type='ABERRATION']\", None)\n\n    def set_attr(struct: Element, name: str, type: str, val: str):\n        node = t.cast(t.Optional[Element], struct.find(f\".//attribute[@name='{name}']\", None))\n        if node is None:\n            node = t.cast(Element, et.Element('attribute', dict(name=name, type=type), None))\n            struct.append(node)\n        else:\n            node.attrib['type'] = type\n        node.text = val  # type: ignore\n\n    def parse_xml_object(obj: Element) -> t.Dict[str, t.Any]:\n        \"\"\"Parse the attributes of a passed XML object.\"\"\"\n        params = {}\n        for attr in obj.iterchildren(None):\n            if attr.tag == 'attribute':\n                params[attr.attrib['name']] = convert_xml_value(attr.text, attr.attrib['type'])\n            elif attr.tag == 'relationship':\n                # todo give this a better API\n                if 'idrefs' in attr.attrib:\n                    params[f\"{attr.attrib['name']}ID\"] = attr.attrib['idrefs']\n        return params\n\n    # TODO how to store atoms in unexploded form\n    (n_a, n_b, n_c) = map(str, (1, 1, 1) if n_cells is None else numpy.asarray(n_cells).astype(int))\n    set_attr(struct, 'repeata', 'int16', n_a)\n    set_attr(struct, 'repeatb', 'int16', n_b)\n    set_attr(struct, 'repeatc', 'int16', n_c)\n\n    (a, b, c) = map(lambda v: f\"{v:.8g}\", box_size)\n    set_attr(struct, 'aparam', 'float', a)\n    set_attr(struct, 'bparam', 'float', b)\n    set_attr(struct, 'cparam', 'float', c)\n\n    if tilt is not None:\n        (tiltx, tilty) = tilt\n        set_attr(struct, 'tiltx', 'float', f\"{tiltx:.4g}\")\n        set_attr(struct, 'tilty', 'float', f\"{tilty:.4g}\")\n\n    if slice_thickness is not None:\n        set_attr(params, 'slicethickness', 'float', f\"{float(slice_thickness):.8g}\")\n    if tds is not None:\n        set_attr(params, 'includetds', 'bool', str(int(bool(tds))))\n    if conv_angle is not None:\n        set_attr(microscope, 'aperture', 'float', f\"{float(conv_angle):.8g}\")\n    if energy is not None:\n        set_attr(microscope, 'kv', 'float', f\"{float(energy):.8g}\")\n    if noise_sigma is not None:\n        if scan is None:\n            raise ValueError(\"New scan specification required for 'noise_sigma'.\")\n        set_attr(scan, 'noise_sigma', 'float', f\"{float(noise_sigma):.8g}\")\n\n    if defocus is not None:\n        for aberration in aberrations:\n            obj = parse_xml_object(aberration)\n            if obj['n'] == 1 and obj['m'] == 0:\n                set_attr(aberration, 'cnma', 'float', f\"{float(defocus):.8g}\")  # A, + is over\n                set_attr(aberration, 'cnmb', 'float', \"0.0\")\n                break\n        else:\n            raise ValueError(\"Couldn't find defocus aberration to modify.\")\n\n    if scan_points is not None:\n        (nx, ny) = numpy.broadcast_to(scan_points, 2,).astype(int)\n        if scan is not None:\n            set_attr(scan, 'nx', 'int16', str(nx))\n            set_attr(scan, 'ny', 'int16', str(ny))\n        else:\n            set_attr(params, 'numscanx', 'int16', str(nx))\n            set_attr(params, 'numscany', 'int16', str(ny))\n\n    if scan_extent is not None:\n        scan_extent = numpy.asarray(scan_extent, dtype=float)\n        try:\n            if scan_extent.ndim < 2:\n                if not scan_extent.shape == (4,):\n                    scan_extent = numpy.broadcast_to(scan_extent, (2,))\n                    scan_extent = numpy.stack(((0., 0.), scan_extent), axis=-1)\n            else:\n                scan_extent = numpy.broadcast_to(scan_extent, (2, 2))\n        except ValueError as e:\n            raise ValueError(f\"Invalid scan_extent '{scan_extent}'. Expected an array of shape (2,), (4,), or (2, 2).\") from e\n\n        if scan is not None:\n            names = ('x_i', 'x_f', 'y_i', 'y_f')\n            elem = scan\n        else:\n            names = ('intx', 'finx', 'inty', 'finy')\n            elem = params\n\n        for (name, val) in zip(names, scan_extent.ravel()):\n            set_attr(elem, name, 'float', f\"{float(val):.8g}\")\n\n    # remove existing atoms\n    for elem in db.findall(\"./object[@type='STRUCTUREATOM']\", None):\n        db.remove(elem)\n\n    # <u^2> -> 1d sigma\n    atoms = atoms.with_wobble((polars.col('wobble') / 3.).sqrt())\n    rows = atoms.select(('elem', 'coords', 'wobble', 'frac_occupancy')).rows()\n    for (i, (elem, (x, y, z), wobble, frac_occupancy)) in enumerate(rows):\n        e = _atom_elem(i, elem, x, y, z, wobble, frac_occupancy)\n        db.append(e)\n\n    et.indent(db, space=\"    \", level=0)  # type: ignore\n\n    with open_file_binary(f, 'w') as f:\n        doctype = b\"\"\"<!DOCTYPE database SYSTEM \"file:///System/Library/DTDs/CoreData.dtd\">\\n\"\"\"\n        out.write(f, encoding='UTF-8', xml_declaration=True, standalone=True, doctype=doctype)  # type: ignore\n        f.write(b'\\n')\n
    "},{"location":"api/io/#atomlib.io.write_qe","title":"write_qe","text":"
    write_qe(\n    atomcell: HasAtomCell,\n    f: FileOrPath,\n    pseudo: Optional[Mapping[str, str]] = None,\n)\n

    Write a structure to a Quantum Espresso pw.x file.

    PARAMETER DESCRIPTION atomcell

    Structure to write

    TYPE: HasAtomCell

    f

    File or path to write to

    TYPE: FileOrPath

    pseudo

    Mapping from atom symbol

    TYPE: Optional[Mapping[str, str]] DEFAULT: None

    Source code in atomlib/io/qe.py
    def write_qe(atomcell: HasAtomCell, f: FileOrPath, pseudo: t.Optional[t.Mapping[str, str]] = None):\n    \"\"\"\n    Write a structure to a Quantum Espresso pw.x file.\n\n    Args:\n      atomcell: Structure to write\n      f: File or path to write to\n      pseudo: Mapping from atom symbol\n    \"\"\"\n    if not isinstance(atomcell, HasAtomCell):\n        raise TypeError(\"'qe' format requires an AtomCell.\")\n\n    atoms = atomcell.wrap().get_atoms('cell_box').with_mass()\n\n    types = atoms.select(('symbol', 'mass')).unique(subset='symbol').sort('mass')\n    if pseudo is not None:\n        types = types.with_columns(_get_symbol_mapping(types, pseudo, ty=polars.Utf8).alias('pot'))\n    else:\n        types = types.with_columns((polars.col('symbol') + polars.lit('.UPF')).alias('pot'))\n        #types = types.with_columns(polars.col('symbol').apply(lambda sym: f\"{sym}.UPF\").alias('pot'))\n\n    with open_file(f, 'w') as f:\n        print(f\"\"\"\\\n&SYSTEM\n  ibrav=0,\n  nat={len(atoms)},\n  ntyp={len(types)}\n/\"\"\", file=f)\n\n        ortho = atomcell.get_transform('local', 'cell_box').to_linear().inner\n        print(\"\\nCELL_PARAMETERS angstrom\", file=f)\n        for row in ortho.T:\n            print(f\"  {row[0]:12.8f} {row[1]:12.8f} {row[2]:12.8f}\", file=f)\n\n        print(\"\\nATOMIC_SPECIES\", file=f)\n        for (symbol, mass, pot) in types.select(('symbol', 'mass', 'pot')).rows():\n            print(f\"{symbol:>4} {mass:10.3f}  {pot}\", file=f)\n\n        print(\"\\nATOMIC_POSITIONS crystal\", file=f)\n        for (symbol, (x, y, z)) in atoms.select(('symbol', 'coords')).rows():\n            print(f\"{symbol:>4} {x:.8f} {y:.8f} {z:.8f}\", file=f)\n\n        print(file=f)  # allows for easy concatenation\n
    "},{"location":"api/io/#atomlib.io.read_cif","title":"read_cif","text":"
    read_cif(\n    f: Union[FileOrPath, CIF, CIFDataBlock],\n    block: Union[int, str, None] = None,\n) -> HasAtoms\n

    Read a structure from a CIF file.

    If block is specified, read data from the given block of the CIF file (index or name).

    Source code in atomlib/io/__init__.py
    def read_cif(f: t.Union[FileOrPath, CIF, CIFDataBlock], block: t.Union[int, str, None] = None) -> HasAtoms:\n    \"\"\"\n    Read a structure from a CIF file.\n\n    If `block` is specified, read data from the given block of the CIF file (index or name).\n    \"\"\"\n\n    if isinstance(f, (CIF, CIFDataBlock)):\n        cif = f\n    else:\n        cif = CIF.from_file(f)\n\n    if isinstance(cif, CIF):\n        if len(cif) == 0:\n            raise ValueError(\"No data present in CIF file.\")\n        if block is None:\n            if len(cif) > 1:\n                logging.warning(\"Multiple blocks present in CIF file. Defaulting to reading first block.\")\n            cif = cif.data_blocks[0]\n        else:\n            cif = cif.get_block(block)\n\n    logging.debug(\"cif data: %r\", cif.data_dict)\n\n    # TODO: support atom_site_Cartn_[xyz]\n    df = cif.stack_tags('atom_site_fract_x', 'atom_site_fract_y', 'atom_site_fract_z',\n                        'atom_site_type_symbol', 'atom_site_label', 'atom_site_occupancy',\n                        'atom_site_U_iso_or_equiv', 'atom_site_B_iso_or_equiv',\n                        rename=('x', 'y', 'z', 'symbol', 'label', 'frac_occupancy', 'wobble', 'wobble_B'),\n                        required=(True, True, True, False, False, False, False, False))\n    if 'wobble_B' in df.columns:\n        if 'wobble' in df.columns:\n            raise ValueError(\"CIF file specifies both 'atom_site_U_iso_or_equiv' and 'atom_site_B_iso_or_equiv'\")\n        df = df.rename({'wobble_B': 'wobble'}) \\\n            .with_columns(polars.col('wobble') * (3./8. / numpy.pi**2))\n    if 'symbol' not in df.columns:\n        if 'label' not in df.columns:\n            raise ValueError(\"Tag 'atom_site_type_symbol' or 'atom_site_label' missing from CIF file\")\n        # infer symbol from label, insert at beginning\n        df = df.insert_column(0, get_sym(get_elem(df['label'])))\n    atoms = Atoms(df)\n\n    # parse and apply symmetry\n    sym_atoms = []\n    for sym in cif.get_symmetry():\n        sym_atoms.append(atoms.transform(sym))\n\n    #s = '\\n'.join(map(str, sym_atoms))\n    #print(f\"sym_atoms:\\n{s}\")\n    #print(f\"atoms: {atoms!s}\")\n\n    if len(sym_atoms) > 0:\n        atoms = Atoms.concat(sym_atoms)._wrap().deduplicate()\n\n    if (cell_size := cif.cell_size()) is not None:\n        cell_size = to_vec3(cell_size)\n        if (cell_angle := cif.cell_angle()) is not None:\n            # degrees to radians\n            cell_angle = to_vec3(cell_angle) * numpy.pi/180.\n        return AtomCell.from_unit_cell(atoms, cell_size, cell_angle, frame='cell_frac')\n    return Atoms(atoms)\n
    "},{"location":"api/io/#atomlib.io.write_cif","title":"write_cif","text":"
    write_cif(\n    atoms: Union[HasAtoms, CIF, CIFDataBlock], f: FileOrPath\n)\n

    Write a structure to an XSF file.

    Source code in atomlib/io/__init__.py
    def write_cif(atoms: t.Union[HasAtoms, CIF, CIFDataBlock], f: FileOrPath):\n    \"\"\"Write a structure to an XSF file.\"\"\"\n    if isinstance(atoms, (CIF, CIFDataBlock)):\n        cif = atoms\n    elif isinstance(atoms, AtomCell):\n        cif = CIF((CIFDataBlock.from_atomcell(atoms),))\n    else:\n        cif = CIF((CIFDataBlock.from_atoms(atoms),))\n\n    cif.write(f)\n
    "},{"location":"api/io/#atomlib.io.read_xyz","title":"read_xyz","text":"
    read_xyz(f: Union[FileOrPath, XYZ]) -> HasAtoms\n

    Read a structure from an XYZ file.

    Source code in atomlib/io/__init__.py
    def read_xyz(f: t.Union[FileOrPath, XYZ]) -> HasAtoms:\n    \"\"\"Read a structure from an XYZ file.\"\"\"\n    if isinstance(f, XYZ):\n        xyz = f\n    else:\n        xyz = XYZ.from_file(f)\n\n    atoms = Atoms(xyz.atoms)\n\n    if (cell_matrix := xyz.cell_matrix()) is not None:\n        cell = Cell.from_ortho(LinearTransform3D(cell_matrix), pbc=xyz.pbc())\n        return AtomCell(atoms, cell, frame='local')\n    return Atoms(atoms)\n
    "},{"location":"api/io/#atomlib.io.write_xyz","title":"write_xyz","text":"
    write_xyz(\n    atoms: Union[HasAtoms, XYZ],\n    f: FileOrPath,\n    fmt: XYZFormat = \"exyz\",\n)\n
    Source code in atomlib/io/__init__.py
    def write_xyz(atoms: t.Union[HasAtoms, XYZ], f: FileOrPath, fmt: XYZFormat = 'exyz'):\n    if not isinstance(atoms, XYZ):\n        atoms = XYZ.from_atoms(atoms)\n    atoms.write(f, fmt)\n
    "},{"location":"api/io/#atomlib.io.read_xsf","title":"read_xsf","text":"
    read_xsf(f: Union[FileOrPath, XSF]) -> HasAtoms\n

    Read a structure from a XSF file.

    Source code in atomlib/io/__init__.py
    def read_xsf(f: t.Union[FileOrPath, XSF]) -> HasAtoms:\n    \"\"\"Read a structure from a XSF file.\"\"\"\n    if isinstance(f, XSF):\n        xsf = f\n    else:\n        xsf = XSF.from_file(f)\n\n    atoms = xsf.get_atoms()\n    atoms = atoms.with_columns(get_sym(atoms['elem']))\n\n    if (primitive_cell := xsf.primitive_cell) is not None:\n        cell = Cell.from_ortho(primitive_cell, pbc=xsf.get_pbc())\n        return AtomCell(atoms, cell, frame='local')\n    return Atoms(atoms)\n
    "},{"location":"api/io/#atomlib.io.write_xsf","title":"write_xsf","text":"
    write_xsf(atoms: Union[HasAtoms, XSF], f: FileOrPath)\n

    Write a structure to an XSF file.

    Source code in atomlib/io/__init__.py
    def write_xsf(atoms: t.Union[HasAtoms, XSF], f: FileOrPath):\n    \"\"\"Write a structure to an XSF file.\"\"\"\n    if isinstance(atoms, XSF):\n        xsf = atoms\n    elif isinstance(atoms, AtomCell):\n        xsf = XSF.from_cell(atoms)\n    else:\n        xsf = XSF.from_atoms(atoms)\n\n    xsf.write(f)\n
    "},{"location":"api/io/#atomlib.io.read_cfg","title":"read_cfg","text":"
    read_cfg(f: Union[FileOrPath, CFG]) -> AtomCell\n

    Read a structure from an AtomEye CFG file.

    Source code in atomlib/io/__init__.py
    def read_cfg(f: t.Union[FileOrPath, CFG]) -> AtomCell:\n    \"\"\"Read a structure from an AtomEye CFG file.\"\"\"\n    if isinstance(f, CFG):\n        cfg = f\n    else:\n        cfg = CFG.from_file(f)\n\n    ortho = cfg.cell\n    if cfg.transform is not None:\n        ortho = cfg.transform @ ortho\n\n    if cfg.length_scale is not None:\n        ortho = ortho.scale(all=cfg.length_scale)\n\n    if cfg.eta is not None:\n        m = numpy.eye(3) + 2. * cfg.eta.inner\n        # matrix sqrt using eigenvals, eigenvecs\n        eigenvals, eigenvecs = numpy.linalg.eigh(m)\n        sqrtm = (eigenvecs * numpy.sqrt(eigenvals)) @ eigenvecs.T\n        ortho = LinearTransform3D(sqrtm) @ ortho\n\n    frame = Atoms(cfg.atoms).transform(ortho, transform_velocities=True)\n    return AtomCell.from_ortho(frame, ortho)\n
    "},{"location":"api/io/#atomlib.io.write_cfg","title":"write_cfg","text":"
    write_cfg(atoms: Union[HasAtoms, CFG], f: FileOrPath)\n

    Write a structure to an AtomEye CFG file.

    Source code in atomlib/io/__init__.py
    def write_cfg(atoms: t.Union[HasAtoms, CFG], f: FileOrPath):\n    \"\"\"Write a structure to an AtomEye CFG file.\"\"\"\n    if not isinstance(atoms, CFG):\n        atoms = CFG.from_atoms(atoms)\n    atoms.write(f)\n
    "},{"location":"api/io/#atomlib.io.read_lmp","title":"read_lmp","text":"
    read_lmp(\n    f: Union[FileOrPath, LMP],\n    type_map: Optional[Dict[int, Union[str, int]]] = None,\n) -> AtomCell\n

    Read a structure from a LAAMPS data file.

    Source code in atomlib/io/__init__.py
    def read_lmp(f: t.Union[FileOrPath, LMP], type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> AtomCell:\n    \"\"\"Read a structure from a LAAMPS data file.\"\"\"\n    if isinstance(f, LMP):\n        lmp = f\n    else:\n        lmp = LMP.from_file(f)\n\n    return lmp.get_atoms(type_map=type_map)\n
    "},{"location":"api/io/#atomlib.io.write_lmp","title":"write_lmp","text":"
    write_lmp(atoms: Union[HasAtoms, LMP], f: FileOrPath)\n

    Write a structure to a LAAMPS data file.

    Source code in atomlib/io/__init__.py
    def write_lmp(atoms: t.Union[HasAtoms, LMP], f: FileOrPath):\n    \"\"\"Write a structure to a LAAMPS data file.\"\"\"\n    if not isinstance(atoms, LMP):\n        atoms = LMP.from_atoms(atoms)\n    atoms.write(f)\n
    "},{"location":"api/io/#atomlib.io.read","title":"read","text":"
    read(path: FileOrPath, ty: FileType) -> HasAtoms\n
    read(\n    path: Union[str, Path, TextIO], ty: Literal[None] = None\n) -> HasAtoms\n
    read(\n    path: FileOrPath, ty: Optional[FileType] = None\n) -> HasAtoms\n

    Read a structure from a file.

    Currently, supported file types are 'cif', 'xyz', and 'xsf'. If no ty is specified, it is inferred from the file's extension.

    Source code in atomlib/io/__init__.py
    def read(path: FileOrPath, ty: t.Optional[FileType] = None) -> HasAtoms:\n    \"\"\"\n    Read a structure from a file.\n\n    Currently, supported file types are 'cif', 'xyz', and 'xsf'.\n    If no `ty` is specified, it is inferred from the file's extension.\n    \"\"\"\n    if ty is None:\n        if isinstance(path, (t.IO, IOBase)):\n            try:\n                name = path.name  # type: ignore\n                if name is None:\n                    raise AttributeError()\n                ext = Path(name).suffix\n            except AttributeError:\n                raise TypeError(\"read() must be passed a file-type when reading an already-open file.\") from None\n        else:\n            name = Path(path).name\n            ext = Path(path).suffix\n\n        if len(ext) == 0:\n            raise ValueError(f\"Can't infer extension for file '{name}'\")\n\n        return read(path, t.cast(FileType, ext))\n\n    ty_strip = str(ty).lstrip('.').lower()\n    try:\n        read_fn = _READ_TABLE[t.cast(FileType, ty_strip)]\n    except KeyError:\n        raise ValueError(f\"Unknown file type '{ty}'\") from None\n    if read_fn is None:\n        raise ValueError(f\"Reading is not supported for file type '{ty_strip}'\")\n    return read_fn(path)\n
    "},{"location":"api/io/#atomlib.io.write","title":"write","text":"
    write(atoms: HasAtoms, path: FileOrPath, ty: FileType)\n
    write(\n    atoms: HasAtoms,\n    path: Union[str, Path, TextIO],\n    ty: Literal[None] = None,\n)\n
    write(\n    atoms: HasAtoms,\n    path: FileOrPath,\n    ty: Optional[FileType] = None,\n)\n

    Write this structure to a file.

    A file type may be specified using ty. If no ty is specified, it is inferred from the path's extension.

    Source code in atomlib/io/__init__.py
    def write(atoms: HasAtoms, path: FileOrPath, ty: t.Optional[FileType] = None):\n    \"\"\"\n    Write this structure to a file.\n\n    A file type may be specified using `ty`.\n    If no `ty` is specified, it is inferred from the path's extension.\n    \"\"\"\n\n    if ty is None:\n        if isinstance(path, (t.IO, IOBase)):\n            try:\n                name = path.name  # type: ignore\n                if name is None:\n                    raise AttributeError()\n                ext = Path(name).suffix\n            except AttributeError:\n                raise TypeError(\"write() must be passed a file-type when reading an already-open file.\") from None\n        else:\n            name = Path(path).name\n            ext = Path(path).suffix\n\n        if len(ext) == 0:\n            raise ValueError(f\"Can't infer extension for file '{name}'\")\n\n        return write(atoms, path, t.cast(FileType, ext))\n\n    ty_strip = str(ty).lstrip('.').lower()\n    try:\n        write_fn = _WRITE_TABLE[t.cast(FileType, ty_strip)]\n    except KeyError:\n        raise ValueError(f\"Unknown file type '{ty}'\") from None\n    if write_fn is None:\n        raise ValueError(f\"Writing is not supported for file type '{ty_strip}'\")\n\n    return write_fn(atoms, path)\n
    "},{"location":"api/io/cfg/","title":"atomlib.io.cfg","text":"

    IO for AtomEye's CFG file format, as described here.

    "},{"location":"api/io/cfg/#atomlib.io.cfg.TAGS","title":"TAGS module-attribute","text":"
    TAGS: FrozenSet[str] = frozenset(\n    map(\n        lambda s: lower(), (\"Number of particles\", \"A\", \"R\")\n    )\n)\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.ARRAY_TAGS","title":"ARRAY_TAGS module-attribute","text":"
    ARRAY_TAGS: FrozenSet[str] = frozenset(\n    map(lambda s: lower(), (\"H0\", \"Transform\", \"eta\"))\n)\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG","title":"CFG dataclass","text":"Source code in atomlib/io/cfg.py
    @dataclass\nclass CFG:\n    atoms: polars.DataFrame\n\n    cell: LinearTransform3D\n    transform: t.Optional[LinearTransform3D] = None\n    eta: t.Optional[LinearTransform3D] = None\n\n    length_scale: t.Optional[float] = None\n    length_unit: t.Optional[str] = None\n    rate_scale: t.Optional[float] = None\n    rate_unit: t.Optional[str] = None\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> CFG:\n        with open_file(file, 'r') as f:\n            return CFGParser(f).parse()\n\n    @staticmethod\n    def from_atoms(atoms: HasAtoms) -> CFG:\n        if isinstance(atoms, HasAtomCell):\n            cell = atoms.get_transform('cell_box').inverse().to_linear()\n            atoms = atoms.get_atoms('cell_box')\n        else:\n            cell = LinearTransform3D.identity()\n\n        # ensure we have masses and velocities\n        atoms = atoms.with_mass().with_velocity()\n        return CFG(atoms._get_frame(), cell, length_scale=1.0, length_unit=\"Angstrom\")\n\n    def write(self, file: FileOrPath):\n        with open_file(file, 'w', newline='\\r\\n') as f:\n            f.write(f\"Number of particles = {len(self.atoms)}\\n\")\n\n            if self.length_scale is not None:\n                unit = f\" [{self.length_unit}]\" if self.length_unit is not None else \"\"\n                f.write(f\"\\nA = {self.length_scale:.8}{unit}\\n\\n\")\n\n            cell = self.cell.inner\n            for (i, j) in product(range(3), repeat=2):\n                f.write(f\"H0({i+1},{j+1}) = {cell[j,i]:.8} A\\n\")\n\n            if self.transform is not None:\n                f.write(\"\\n\")\n                transform = self.transform.inner\n                for (i, j) in product(range(3), repeat=2):\n                    f.write(f\"Transform({i+1},{j+1}) = {transform[j,i]:.8}\\n\")\n\n            if self.eta is not None:\n                f.write(\"\\n\")\n                eta = self.eta.inner\n                for i in range(3):\n                    for j in range(i, 3):\n                        f.write(f\"eta({i+1},{j+1}) = {eta[j,i]:.8}\\n\")\n\n            if self.rate_scale is not None:\n                unit = f\" [{self.rate_unit}]\" if self.rate_unit is not None else \"\"\n                f.write(f\"\\nR = {self.rate_scale:.8}{unit}\\n\")\n\n            f.write(\"\\n\")\n            for row in self.atoms.select(('mass', 'symbol', 'coords', 'velocity')).rows():\n                (mass, sym, (x, y, z), (v_x, v_y, v_z)) = row\n                f.write(f\"{mass:.4f} {sym:>2} {x:.8} {y:.8} {z:.8} {v_x:.8} {v_y:.8} {v_z:.8}\\n\")\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.atoms","title":"atoms instance-attribute","text":"
    atoms: DataFrame\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.cell","title":"cell instance-attribute","text":"
    cell: LinearTransform3D\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.transform","title":"transform class-attribute instance-attribute","text":"
    transform: Optional[LinearTransform3D] = None\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.eta","title":"eta class-attribute instance-attribute","text":"
    eta: Optional[LinearTransform3D] = None\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.length_scale","title":"length_scale class-attribute instance-attribute","text":"
    length_scale: Optional[float] = None\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.length_unit","title":"length_unit class-attribute instance-attribute","text":"
    length_unit: Optional[str] = None\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.rate_scale","title":"rate_scale class-attribute instance-attribute","text":"
    rate_scale: Optional[float] = None\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.rate_unit","title":"rate_unit class-attribute instance-attribute","text":"
    rate_unit: Optional[str] = None\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> CFG\n
    Source code in atomlib/io/cfg.py
    @staticmethod\ndef from_file(file: FileOrPath) -> CFG:\n    with open_file(file, 'r') as f:\n        return CFGParser(f).parse()\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.from_atoms","title":"from_atoms staticmethod","text":"
    from_atoms(atoms: HasAtoms) -> CFG\n
    Source code in atomlib/io/cfg.py
    @staticmethod\ndef from_atoms(atoms: HasAtoms) -> CFG:\n    if isinstance(atoms, HasAtomCell):\n        cell = atoms.get_transform('cell_box').inverse().to_linear()\n        atoms = atoms.get_atoms('cell_box')\n    else:\n        cell = LinearTransform3D.identity()\n\n    # ensure we have masses and velocities\n    atoms = atoms.with_mass().with_velocity()\n    return CFG(atoms._get_frame(), cell, length_scale=1.0, length_unit=\"Angstrom\")\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFG.write","title":"write","text":"
    write(file: FileOrPath)\n
    Source code in atomlib/io/cfg.py
    def write(self, file: FileOrPath):\n    with open_file(file, 'w', newline='\\r\\n') as f:\n        f.write(f\"Number of particles = {len(self.atoms)}\\n\")\n\n        if self.length_scale is not None:\n            unit = f\" [{self.length_unit}]\" if self.length_unit is not None else \"\"\n            f.write(f\"\\nA = {self.length_scale:.8}{unit}\\n\\n\")\n\n        cell = self.cell.inner\n        for (i, j) in product(range(3), repeat=2):\n            f.write(f\"H0({i+1},{j+1}) = {cell[j,i]:.8} A\\n\")\n\n        if self.transform is not None:\n            f.write(\"\\n\")\n            transform = self.transform.inner\n            for (i, j) in product(range(3), repeat=2):\n                f.write(f\"Transform({i+1},{j+1}) = {transform[j,i]:.8}\\n\")\n\n        if self.eta is not None:\n            f.write(\"\\n\")\n            eta = self.eta.inner\n            for i in range(3):\n                for j in range(i, 3):\n                    f.write(f\"eta({i+1},{j+1}) = {eta[j,i]:.8}\\n\")\n\n        if self.rate_scale is not None:\n            unit = f\" [{self.rate_unit}]\" if self.rate_unit is not None else \"\"\n            f.write(f\"\\nR = {self.rate_scale:.8}{unit}\\n\")\n\n        f.write(\"\\n\")\n        for row in self.atoms.select(('mass', 'symbol', 'coords', 'velocity')).rows():\n            (mass, sym, (x, y, z), (v_x, v_y, v_z)) = row\n            f.write(f\"{mass:.4f} {sym:>2} {x:.8} {y:.8} {z:.8} {v_x:.8} {v_y:.8} {v_z:.8}\\n\")\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFGParser","title":"CFGParser","text":"Source code in atomlib/io/cfg.py
    class CFGParser:\n    def __init__(self, f: TextIOBase):\n        self.buf = LineBuffer(f)\n\n    def parse(self) -> CFG:\n        (n, value_tags, array_tags) = self.parse_tags()\n        atoms = self.parse_atoms(n)\n\n        try:\n            cell = array_tags['h0']\n        except KeyError:\n            raise ValueError(\"CFG file missing required tag 'H0'\") from None\n\n        length = value_tags.get('a')\n        length_scale = map_some(lambda t: t[0], length)\n        length_unit = map_some(lambda t: t[1], length)\n\n        rate = value_tags.get('r')\n        rate_scale = map_some(lambda t: t[0], rate)\n        rate_unit = map_some(lambda t: t[1], rate)\n\n        return CFG(\n            atoms=atoms,\n            cell=LinearTransform3D(cell),\n            transform=map_some(LinearTransform3D, array_tags.get('transform')),\n            eta=map_some(LinearTransform3D, array_tags.get('eta')),\n            length_scale=length_scale,\n            length_unit=length_unit,\n            rate_scale=rate_scale,\n            rate_unit=rate_unit,\n        )\n\n    def parse_tags(self) -> t.Tuple[int, t.Dict[str, t.Tuple[float, t.Optional[str]]], t.Dict[str, numpy.ndarray]]:\n        first = True\n\n        # tag, (value, unit)\n        n: t.Optional[int] = None\n        value_tags: t.Dict[str, t.Tuple[float, t.Optional[str]]] = {}\n        array_tags: t.Dict[str, t.List[t.List[t.Optional[float]]]] = {}\n\n        while (line := self.buf.peek_line()) is not None:\n            line = line.strip()\n            if len(line) == 0 or line.startswith(\"#\"):\n                # skip comments and blank lines\n                self.buf.next_line()\n                continue\n\n            if first:\n                if not line.lower().startswith('number of particles'):\n                    raise ValueError(\"File does not start with Number of particles.\"\n                                    \" Is this an AtomEye CFG file?\")\n\n            try:\n                tag, value = line.split('=')\n            except ValueError:\n                try:\n                    float(line.split(' ', 1)[0])\n                    # started list of atoms\n                    break\n                except ValueError:\n                    raise ValueError(f\"Expected a tag-value pair at line {self.buf.line}: '{line}'\")\n\n            tag = tag.strip()\n            value = value.strip()\n            if first:\n                try:\n                    value = int(value)\n                except ValueError:\n                    raise ValueError(f\"Invalid # of elements '{value}' at line {self.buf.line}\") from None\n                n = value\n                first = False\n                self.buf.next_line()\n                continue\n\n            if tag.lower() in TAGS:\n                try:\n                    value_tags[tag.lower()] = self.parse_value_with_unit(value)\n                except ValueError:\n                    raise ValueError(f\"Invalid value '{value}' at line {self.buf.line}\") from None\n            elif (match := re.match(r'(.+)\\((\\d+),(\\d+)\\)', tag)):\n                try:\n                    (tag, i, j) = (match[1].lower(), int(match[2]), int(match[3]))\n                    if not (0 < i <= 3 and 0 < j <= 3):\n                        raise ValueError(f\"Invalid index ({i},{j}) for tag '{tag}' at line {self.buf.line}\")\n                    if tag not in array_tags:\n                        array_tags[tag] = [[None] * 3, [None] * 3, [None] * 3]\n                    try:\n                        val = self.parse_value_with_unit(value)[0]\n                        array_tags[tag][j-1][i-1] = val\n                        if tag == 'eta':\n                            array_tags[tag][i-1][j-1] = val\n                    except ValueError:\n                        raise ValueError(f\"Invalid value '{value}' at line {self.buf.line}\") from None\n                except ValueError:\n                    raise ValueError(f\"Invalid indexes in tag '{tag}' at line {self.buf.line}\") from None\n                if tag.lower() not in ARRAY_TAGS:\n                    raise ValueError(f\"Unknown array tag '{tag}'\")\n            elif tag.lower() in ARRAY_TAGS:\n                raise ValueError(f\"Missing indexes for tag '{tag}' at line {self.buf.line}\")\n            else:\n                raise ValueError(f\"Unknown tag '{tag}' at line {self.buf.line}\")\n\n            self.buf.next_line()\n\n        if n is None:\n            raise ValueError(\"Empty CFG file\")\n\n        ndarray_tags: t.Dict[str, numpy.ndarray] = {}\n\n        for (tag, value) in array_tags.items():\n            for i in range(3):\n                for j in range(3):\n                    if value[j][i] is None:\n                        raise ValueError(f\"Tag '{tag}' missing value for index ({i+1},{j+1})\")\n            ndarray_tags[tag] = numpy.array(value)\n\n        return (n, value_tags, ndarray_tags)\n\n    def parse_value_with_unit(self, value: str) -> t.Tuple[float, t.Optional[str]]:\n        segments = value.split(maxsplit=1)\n        if len(segments) == 1:\n            return (float(value), None)\n        value, unit = map(lambda s: s.strip(), segments)\n\n        if (match := re.match(r'\\[(.+)\\]', unit)):\n            unit = str(match[1])\n        else:\n            unit = unit.split(maxsplit=1)[0]\n\n        return (float(value), unit)\n\n    def parse_atoms(self, n: int) -> polars.DataFrame:\n        df = parse_whitespace_separated(self.buf, {\n            'mass': polars.Float64, 'symbol': polars.Utf8,\n            'coords': polars.Array(polars.Float64, 3),\n            'velocity': polars.Array(polars.Float64, 3),\n        })\n        df = df.with_columns(get_elem(df['symbol'])).select(\n            'elem', 'symbol', 'coords', 'velocity', 'mass'\n        )\n\n        if n != len(df):\n            raise ValueError(f\"# of atom rows doesn't match declared number ({len(df)} vs. {n})\")\n\n        return df\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFGParser.buf","title":"buf instance-attribute","text":"
    buf = LineBuffer(f)\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFGParser.parse","title":"parse","text":"
    parse() -> CFG\n
    Source code in atomlib/io/cfg.py
    def parse(self) -> CFG:\n    (n, value_tags, array_tags) = self.parse_tags()\n    atoms = self.parse_atoms(n)\n\n    try:\n        cell = array_tags['h0']\n    except KeyError:\n        raise ValueError(\"CFG file missing required tag 'H0'\") from None\n\n    length = value_tags.get('a')\n    length_scale = map_some(lambda t: t[0], length)\n    length_unit = map_some(lambda t: t[1], length)\n\n    rate = value_tags.get('r')\n    rate_scale = map_some(lambda t: t[0], rate)\n    rate_unit = map_some(lambda t: t[1], rate)\n\n    return CFG(\n        atoms=atoms,\n        cell=LinearTransform3D(cell),\n        transform=map_some(LinearTransform3D, array_tags.get('transform')),\n        eta=map_some(LinearTransform3D, array_tags.get('eta')),\n        length_scale=length_scale,\n        length_unit=length_unit,\n        rate_scale=rate_scale,\n        rate_unit=rate_unit,\n    )\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFGParser.parse_tags","title":"parse_tags","text":"
    parse_tags() -> Tuple[\n    int,\n    Dict[str, Tuple[float, Optional[str]]],\n    Dict[str, ndarray],\n]\n
    Source code in atomlib/io/cfg.py
    def parse_tags(self) -> t.Tuple[int, t.Dict[str, t.Tuple[float, t.Optional[str]]], t.Dict[str, numpy.ndarray]]:\n    first = True\n\n    # tag, (value, unit)\n    n: t.Optional[int] = None\n    value_tags: t.Dict[str, t.Tuple[float, t.Optional[str]]] = {}\n    array_tags: t.Dict[str, t.List[t.List[t.Optional[float]]]] = {}\n\n    while (line := self.buf.peek_line()) is not None:\n        line = line.strip()\n        if len(line) == 0 or line.startswith(\"#\"):\n            # skip comments and blank lines\n            self.buf.next_line()\n            continue\n\n        if first:\n            if not line.lower().startswith('number of particles'):\n                raise ValueError(\"File does not start with Number of particles.\"\n                                \" Is this an AtomEye CFG file?\")\n\n        try:\n            tag, value = line.split('=')\n        except ValueError:\n            try:\n                float(line.split(' ', 1)[0])\n                # started list of atoms\n                break\n            except ValueError:\n                raise ValueError(f\"Expected a tag-value pair at line {self.buf.line}: '{line}'\")\n\n        tag = tag.strip()\n        value = value.strip()\n        if first:\n            try:\n                value = int(value)\n            except ValueError:\n                raise ValueError(f\"Invalid # of elements '{value}' at line {self.buf.line}\") from None\n            n = value\n            first = False\n            self.buf.next_line()\n            continue\n\n        if tag.lower() in TAGS:\n            try:\n                value_tags[tag.lower()] = self.parse_value_with_unit(value)\n            except ValueError:\n                raise ValueError(f\"Invalid value '{value}' at line {self.buf.line}\") from None\n        elif (match := re.match(r'(.+)\\((\\d+),(\\d+)\\)', tag)):\n            try:\n                (tag, i, j) = (match[1].lower(), int(match[2]), int(match[3]))\n                if not (0 < i <= 3 and 0 < j <= 3):\n                    raise ValueError(f\"Invalid index ({i},{j}) for tag '{tag}' at line {self.buf.line}\")\n                if tag not in array_tags:\n                    array_tags[tag] = [[None] * 3, [None] * 3, [None] * 3]\n                try:\n                    val = self.parse_value_with_unit(value)[0]\n                    array_tags[tag][j-1][i-1] = val\n                    if tag == 'eta':\n                        array_tags[tag][i-1][j-1] = val\n                except ValueError:\n                    raise ValueError(f\"Invalid value '{value}' at line {self.buf.line}\") from None\n            except ValueError:\n                raise ValueError(f\"Invalid indexes in tag '{tag}' at line {self.buf.line}\") from None\n            if tag.lower() not in ARRAY_TAGS:\n                raise ValueError(f\"Unknown array tag '{tag}'\")\n        elif tag.lower() in ARRAY_TAGS:\n            raise ValueError(f\"Missing indexes for tag '{tag}' at line {self.buf.line}\")\n        else:\n            raise ValueError(f\"Unknown tag '{tag}' at line {self.buf.line}\")\n\n        self.buf.next_line()\n\n    if n is None:\n        raise ValueError(\"Empty CFG file\")\n\n    ndarray_tags: t.Dict[str, numpy.ndarray] = {}\n\n    for (tag, value) in array_tags.items():\n        for i in range(3):\n            for j in range(3):\n                if value[j][i] is None:\n                    raise ValueError(f\"Tag '{tag}' missing value for index ({i+1},{j+1})\")\n        ndarray_tags[tag] = numpy.array(value)\n\n    return (n, value_tags, ndarray_tags)\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFGParser.parse_value_with_unit","title":"parse_value_with_unit","text":"
    parse_value_with_unit(\n    value: str,\n) -> Tuple[float, Optional[str]]\n
    Source code in atomlib/io/cfg.py
    def parse_value_with_unit(self, value: str) -> t.Tuple[float, t.Optional[str]]:\n    segments = value.split(maxsplit=1)\n    if len(segments) == 1:\n        return (float(value), None)\n    value, unit = map(lambda s: s.strip(), segments)\n\n    if (match := re.match(r'\\[(.+)\\]', unit)):\n        unit = str(match[1])\n    else:\n        unit = unit.split(maxsplit=1)[0]\n\n    return (float(value), unit)\n
    "},{"location":"api/io/cfg/#atomlib.io.cfg.CFGParser.parse_atoms","title":"parse_atoms","text":"
    parse_atoms(n: int) -> DataFrame\n
    Source code in atomlib/io/cfg.py
    def parse_atoms(self, n: int) -> polars.DataFrame:\n    df = parse_whitespace_separated(self.buf, {\n        'mass': polars.Float64, 'symbol': polars.Utf8,\n        'coords': polars.Array(polars.Float64, 3),\n        'velocity': polars.Array(polars.Float64, 3),\n    })\n    df = df.with_columns(get_elem(df['symbol'])).select(\n        'elem', 'symbol', 'coords', 'velocity', 'mass'\n    )\n\n    if n != len(df):\n        raise ValueError(f\"# of atom rows doesn't match declared number ({len(df)} vs. {n})\")\n\n    return df\n
    "},{"location":"api/io/cif/","title":"atomlib.io.cif","text":"

    IO for the CIF1.1 file format, specified here.

    "},{"location":"api/io/cif/#atomlib.io.cif.Value","title":"Value module-attribute","text":"
    Value: TypeAlias = Union[int, float, str, None]\n
    "},{"location":"api/io/cif/#atomlib.io.cif.SYMMETRY_PARSER","title":"SYMMETRY_PARSER module-attribute","text":"
    SYMMETRY_PARSER: Parser[SymmetryVec, SymmetryVec] = Parser(\n    [\n        BinaryOrUnaryOp([\"-\"], sub, False, 5),\n        BinaryOrUnaryOp([\"+\"], add, False, 5),\n        BinaryOp([\"*\"], mul, 6),\n        BinaryOp([\"/\"], truediv, 6),\n    ],\n    parse,\n)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIF","title":"CIF dataclass","text":"Source code in atomlib/io/cif.py
    @dataclass\nclass CIF:\n    data_blocks: t.Tuple[CIFDataBlock, ...]\n\n    def __post_init__(self):\n        # ensure that all data_blocks after the first have a name\n        for data_block in self.data_blocks[1:]:\n            if data_block.name is None:\n                data_block.name = \"\"\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> CIF:\n        return CIF(tuple(CIFDataBlock.from_file(file)))\n\n    def __len__(self) -> int:\n        return self.data_blocks.__len__()\n\n    def get_block(self, block: t.Union[int, str]) -> CIFDataBlock:\n        try:\n            if isinstance(block, int):\n                return self.data_blocks[block]\n            return next(b for b in self.data_blocks if b.name == block)\n        except (IndexError, StopIteration):\n            raise ValueError(f\"Couldn't find block '{block}' in CIF file. File has {len(self)} blocks.\")\n\n    def write(self, file: FileOrPath):\n        with open_file(file, 'w') as f:\n            print(\"# generated by atomlib\", file=f, end=None)\n            for data_block in self.data_blocks:\n                print(file=f)\n                data_block._write(f)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIF.data_blocks","title":"data_blocks instance-attribute","text":"
    data_blocks: Tuple[CIFDataBlock, ...]\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIF.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> CIF\n
    Source code in atomlib/io/cif.py
    @staticmethod\ndef from_file(file: FileOrPath) -> CIF:\n    return CIF(tuple(CIFDataBlock.from_file(file)))\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIF.get_block","title":"get_block","text":"
    get_block(block: Union[int, str]) -> CIFDataBlock\n
    Source code in atomlib/io/cif.py
    def get_block(self, block: t.Union[int, str]) -> CIFDataBlock:\n    try:\n        if isinstance(block, int):\n            return self.data_blocks[block]\n        return next(b for b in self.data_blocks if b.name == block)\n    except (IndexError, StopIteration):\n        raise ValueError(f\"Couldn't find block '{block}' in CIF file. File has {len(self)} blocks.\")\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIF.write","title":"write","text":"
    write(file: FileOrPath)\n
    Source code in atomlib/io/cif.py
    def write(self, file: FileOrPath):\n    with open_file(file, 'w') as f:\n        print(\"# generated by atomlib\", file=f, end=None)\n        for data_block in self.data_blocks:\n            print(file=f)\n            data_block._write(f)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock","title":"CIFDataBlock dataclass","text":"Source code in atomlib/io/cif.py
    @dataclass\nclass CIFDataBlock:\n    name: t.Optional[str]  # None: no data_ block, empty string: unnamed \"data_\"\n\n    # data (including loops) in file order\n    data: t.Tuple[t.Union[t.Tuple[str, Value], CIFTable], ...]\n\n    # data flattened into a single dictionary. Created automatically from `data`\n    data_dict: t.Dict[str, t.Union[t.List[Value], Value]] = field(init=False)\n\n    def __post_init__(self):\n        # if we raise here, make sure the object state is fine\n        self.data_dict = None  # type: ignore\n\n        data_values = {}\n\n        def _iter_data_values():\n            for d in self.data:\n                if isinstance(d, CIFTable):\n                    yield from d.data.items()\n                else:\n                    yield d\n\n        for (k, v) in _iter_data_values():\n            if k in data_values:\n                raise ValueError(f\"Duplicate key {k}\")\n            data_values[k] = v\n\n        self.data_dict = data_values\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> t.Iterator[CIFDataBlock]:\n        with open_file(file) as f:\n            yield from CifReader(f).parse()\n\n    @staticmethod\n    def from_atoms(atoms: HasAtoms) -> CIFDataBlock:\n        data: t.List[t.Union[t.Tuple[str, Value], CIFTable]] = []\n\n        data.append(('audit_creation_method', 'Generated by atomlib'))\n\n        keys: t.Sequence[t.Tuple[str, t.Union[str, polars.Expr], t.Union[str, bool]]] = (\n            # col, expr, predicate (column or boolean)\n            ('atom_site_type_symbol', 'symbol', True),\n            ('atom_site_label', 'label', 'label'),\n            ('atom_site_occupancy', 'frac_occupancy', 'frac_occupancy'),\n            ('atom_site_Cartn_x', polars.col('coords').arr.get(0), True),\n            ('atom_site_Cartn_y', polars.col('coords').arr.get(1), True),\n            ('atom_site_Cartn_z', polars.col('coords').arr.get(2), True),\n            ('atom_site_U_iso_or_equiv', 'wobble', 'wobble'),\n        )\n        data.append(CIFTable({\n            key: atoms.select(expr).to_series().to_list() for (key, expr, pred) in keys\n            if (atoms.try_get_column(pred) is not None if isinstance(pred, str) else pred)\n        }))\n\n        return CIFDataBlock(\"\", tuple(data))\n\n    @staticmethod\n    def from_atomcell(atomcell: HasAtomCell) -> CIFDataBlock:\n        atoms = atomcell.get_atoms('cell_box')\n        ortho = atomcell.get_transform('local', 'cell_box').to_linear()\n        (cell_size, cell_angle) = ortho_to_cell(ortho)\n        cell_angle *= 180./numpy.pi  # convert to degrees\n\n        data: t.List[t.Union[t.Tuple[str, Value], CIFTable]] = []\n\n        data.append(('audit_creation_method', 'Generated by atomlib'))\n\n        # symmetry information\n        data.append(CIFTable({\n            'space_group_symop_id': [1],\n            'space_group_symop_operation_xyz': ['x,y,z'],\n        }))\n\n        # cell information\n        data.append(('cell_length_a', cell_size[0]))\n        data.append(('cell_length_b', cell_size[1]))\n        data.append(('cell_length_c', cell_size[2]))\n        data.append(('cell_angle_alpha', cell_angle[0]))\n        data.append(('cell_angle_beta', cell_angle[1]))\n        data.append(('cell_angle_gamma', cell_angle[2]))\n        data.append(('cell_volume', ortho.det()))\n\n        keys: t.Sequence[t.Tuple[str, t.Union[str, polars.Expr], t.Union[str, bool]]] = (\n            # col, expr, predicate (column or boolean)\n            ('atom_site_type_symbol', 'symbol', True),\n            ('atom_site_label', 'label', 'label'),\n            ('atom_site_occupancy', 'frac_occupancy', 'frac_occupancy'),\n            ('atom_site_fract_x', polars.col('coords').arr.get(0), True),\n            ('atom_site_fract_y', polars.col('coords').arr.get(1), True),\n            ('atom_site_fract_z', polars.col('coords').arr.get(2), True),\n            ('atom_site_U_iso_or_equiv', 'wobble', 'wobble'),\n        )\n        data.append(CIFTable({\n            key: atoms.select(expr).to_series().to_list() for (key, expr, pred) in keys\n            if (atoms.try_get_column(pred) is not None if isinstance(pred, str) else pred)\n        }))\n\n        return CIFDataBlock(\"\", tuple(data))\n\n    def write(self, file: FileOrPath):\n        with open_file(file, 'w') as f:\n            self._write(f)\n\n    def _write(self, f: TextIOBase):\n        if self.name is not None:\n            print(f\"data_{self.name}\\n\", file=f)\n\n        for data in self.data:\n            if isinstance(data, CIFTable):\n                data._write(f)\n            else:\n                (name, value) = data\n                val = _format_val(value).rstrip()\n                if val.startswith(';'):\n                    # multiline string\n                    print(f\"_{name}\\n{val}\", file=f)\n                else:\n                    print(f\"_{name: <28} {_format_val(value).rstrip()}\", file=f)\n\n    def stack_tags(self, *tags: str, dtype: t.Union[str, numpy.dtype, t.Iterable[t.Union[str, numpy.dtype]], None] = None,\n                   rename: t.Optional[t.Iterable[t.Optional[str]]] = None, required: t.Union[bool, t.Iterable[bool]] = True) -> polars.DataFrame:\n        dtypes: t.Iterable[t.Optional[numpy.dtype]]\n        if dtype is None:\n            dtypes = repeat(None)\n        elif isinstance(dtype, (numpy.dtype, str)):\n            dtypes = (numpy.dtype(dtype),) * len(tags)\n        else:\n            dtypes = tuple(map(lambda ty: numpy.dtype(ty), dtype))\n            if len(dtypes) != len(tags):\n                raise ValueError(\"dtype list of invalid length\")\n\n        if isinstance(required, bool):\n            required = repeat(required)\n\n        if rename is None:\n            rename = repeat(None)\n\n        d = {}\n        for (tag, ty, req, name) in zip(tags, dtypes, required, rename):\n            if tag not in self.data_dict:\n                if req:\n                    raise ValueError(f\"Tag '{tag}' missing from CIF file\")\n                continue\n            try:\n                arr = numpy.array(self.data_dict[tag], dtype=ty)\n                d[name or tag] = arr\n            except TypeError:\n                raise TypeError(f\"Tag '{tag}' of invalid or heterogeneous type.\")\n\n        if len(d) == 0:\n            return polars.DataFrame({})\n\n        tag_len = len(next(iter(d.values())))\n        if any(len(arr) != tag_len for arr in d.values()):\n            raise ValueError(f\"Tags of mismatching lengths: {tuple(map(len, d.values()))}\")\n\n        return polars.DataFrame(d)\n\n    def cell_size(self) -> t.Optional[t.Tuple[float, float, float]]:\n        \"\"\"Return cell size (in angstroms).\"\"\"\n        try:\n            a = float(self['cell_length_a'])  # type: ignore\n            b = float(self['cell_length_b'])  # type: ignore\n            c = float(self['cell_length_c'])  # type: ignore\n            return (a, b, c)\n        except (ValueError, TypeError, KeyError):\n            return None\n\n    def cell_angle(self) -> t.Optional[t.Tuple[float, float, float]]:\n        \"\"\"Return cell angle (in degrees).\"\"\"\n        try:\n            a = float(self['cell_angle_alpha'])  # type: ignore\n            b = float(self['cell_angle_beta'])   # type: ignore\n            g = float(self['cell_angle_gamma'])  # type: ignore\n            return (a, b, g)\n        except (ValueError, TypeError, KeyError):\n            return None\n\n    def get_symmetry(self) -> t.Iterator[AffineTransform3D]:\n        syms = self.data_dict.get('space_group_symop_operation_xyz')\n        if syms is None:\n            # old name for symmetry\n            syms = self.data_dict.get('symmetry_equiv_pos_as_xyz')\n        if syms is None:\n            syms = ()\n        if not hasattr(syms, '__iter__'):\n            syms = (syms,)\n        return map(parse_symmetry, map(str, syms))  # type: ignore\n\n    def __getitem__(self, key: str) -> t.Union[Value, t.List[Value]]:\n        return self.data_dict.__getitem__(key)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.name","title":"name instance-attribute","text":"
    name: Optional[str]\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.data","title":"data instance-attribute","text":"
    data: Tuple[Union[Tuple[str, Value], CIFTable], ...]\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.data_dict","title":"data_dict class-attribute instance-attribute","text":"
    data_dict: Dict[str, Union[List[Value], Value]] = field(\n    init=False\n)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> Iterator[CIFDataBlock]\n
    Source code in atomlib/io/cif.py
    @staticmethod\ndef from_file(file: FileOrPath) -> t.Iterator[CIFDataBlock]:\n    with open_file(file) as f:\n        yield from CifReader(f).parse()\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.from_atoms","title":"from_atoms staticmethod","text":"
    from_atoms(atoms: HasAtoms) -> CIFDataBlock\n
    Source code in atomlib/io/cif.py
    @staticmethod\ndef from_atoms(atoms: HasAtoms) -> CIFDataBlock:\n    data: t.List[t.Union[t.Tuple[str, Value], CIFTable]] = []\n\n    data.append(('audit_creation_method', 'Generated by atomlib'))\n\n    keys: t.Sequence[t.Tuple[str, t.Union[str, polars.Expr], t.Union[str, bool]]] = (\n        # col, expr, predicate (column or boolean)\n        ('atom_site_type_symbol', 'symbol', True),\n        ('atom_site_label', 'label', 'label'),\n        ('atom_site_occupancy', 'frac_occupancy', 'frac_occupancy'),\n        ('atom_site_Cartn_x', polars.col('coords').arr.get(0), True),\n        ('atom_site_Cartn_y', polars.col('coords').arr.get(1), True),\n        ('atom_site_Cartn_z', polars.col('coords').arr.get(2), True),\n        ('atom_site_U_iso_or_equiv', 'wobble', 'wobble'),\n    )\n    data.append(CIFTable({\n        key: atoms.select(expr).to_series().to_list() for (key, expr, pred) in keys\n        if (atoms.try_get_column(pred) is not None if isinstance(pred, str) else pred)\n    }))\n\n    return CIFDataBlock(\"\", tuple(data))\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.from_atomcell","title":"from_atomcell staticmethod","text":"
    from_atomcell(atomcell: HasAtomCell) -> CIFDataBlock\n
    Source code in atomlib/io/cif.py
    @staticmethod\ndef from_atomcell(atomcell: HasAtomCell) -> CIFDataBlock:\n    atoms = atomcell.get_atoms('cell_box')\n    ortho = atomcell.get_transform('local', 'cell_box').to_linear()\n    (cell_size, cell_angle) = ortho_to_cell(ortho)\n    cell_angle *= 180./numpy.pi  # convert to degrees\n\n    data: t.List[t.Union[t.Tuple[str, Value], CIFTable]] = []\n\n    data.append(('audit_creation_method', 'Generated by atomlib'))\n\n    # symmetry information\n    data.append(CIFTable({\n        'space_group_symop_id': [1],\n        'space_group_symop_operation_xyz': ['x,y,z'],\n    }))\n\n    # cell information\n    data.append(('cell_length_a', cell_size[0]))\n    data.append(('cell_length_b', cell_size[1]))\n    data.append(('cell_length_c', cell_size[2]))\n    data.append(('cell_angle_alpha', cell_angle[0]))\n    data.append(('cell_angle_beta', cell_angle[1]))\n    data.append(('cell_angle_gamma', cell_angle[2]))\n    data.append(('cell_volume', ortho.det()))\n\n    keys: t.Sequence[t.Tuple[str, t.Union[str, polars.Expr], t.Union[str, bool]]] = (\n        # col, expr, predicate (column or boolean)\n        ('atom_site_type_symbol', 'symbol', True),\n        ('atom_site_label', 'label', 'label'),\n        ('atom_site_occupancy', 'frac_occupancy', 'frac_occupancy'),\n        ('atom_site_fract_x', polars.col('coords').arr.get(0), True),\n        ('atom_site_fract_y', polars.col('coords').arr.get(1), True),\n        ('atom_site_fract_z', polars.col('coords').arr.get(2), True),\n        ('atom_site_U_iso_or_equiv', 'wobble', 'wobble'),\n    )\n    data.append(CIFTable({\n        key: atoms.select(expr).to_series().to_list() for (key, expr, pred) in keys\n        if (atoms.try_get_column(pred) is not None if isinstance(pred, str) else pred)\n    }))\n\n    return CIFDataBlock(\"\", tuple(data))\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.write","title":"write","text":"
    write(file: FileOrPath)\n
    Source code in atomlib/io/cif.py
    def write(self, file: FileOrPath):\n    with open_file(file, 'w') as f:\n        self._write(f)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.stack_tags","title":"stack_tags","text":"
    stack_tags(\n    *tags: str,\n    dtype: Union[\n        str, dtype, Iterable[Union[str, dtype]], None\n    ] = None,\n    rename: Optional[Iterable[Optional[str]]] = None,\n    required: Union[bool, Iterable[bool]] = True\n) -> DataFrame\n
    Source code in atomlib/io/cif.py
    def stack_tags(self, *tags: str, dtype: t.Union[str, numpy.dtype, t.Iterable[t.Union[str, numpy.dtype]], None] = None,\n               rename: t.Optional[t.Iterable[t.Optional[str]]] = None, required: t.Union[bool, t.Iterable[bool]] = True) -> polars.DataFrame:\n    dtypes: t.Iterable[t.Optional[numpy.dtype]]\n    if dtype is None:\n        dtypes = repeat(None)\n    elif isinstance(dtype, (numpy.dtype, str)):\n        dtypes = (numpy.dtype(dtype),) * len(tags)\n    else:\n        dtypes = tuple(map(lambda ty: numpy.dtype(ty), dtype))\n        if len(dtypes) != len(tags):\n            raise ValueError(\"dtype list of invalid length\")\n\n    if isinstance(required, bool):\n        required = repeat(required)\n\n    if rename is None:\n        rename = repeat(None)\n\n    d = {}\n    for (tag, ty, req, name) in zip(tags, dtypes, required, rename):\n        if tag not in self.data_dict:\n            if req:\n                raise ValueError(f\"Tag '{tag}' missing from CIF file\")\n            continue\n        try:\n            arr = numpy.array(self.data_dict[tag], dtype=ty)\n            d[name or tag] = arr\n        except TypeError:\n            raise TypeError(f\"Tag '{tag}' of invalid or heterogeneous type.\")\n\n    if len(d) == 0:\n        return polars.DataFrame({})\n\n    tag_len = len(next(iter(d.values())))\n    if any(len(arr) != tag_len for arr in d.values()):\n        raise ValueError(f\"Tags of mismatching lengths: {tuple(map(len, d.values()))}\")\n\n    return polars.DataFrame(d)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.cell_size","title":"cell_size","text":"
    cell_size() -> Optional[Tuple[float, float, float]]\n

    Return cell size (in angstroms).

    Source code in atomlib/io/cif.py
    def cell_size(self) -> t.Optional[t.Tuple[float, float, float]]:\n    \"\"\"Return cell size (in angstroms).\"\"\"\n    try:\n        a = float(self['cell_length_a'])  # type: ignore\n        b = float(self['cell_length_b'])  # type: ignore\n        c = float(self['cell_length_c'])  # type: ignore\n        return (a, b, c)\n    except (ValueError, TypeError, KeyError):\n        return None\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.cell_angle","title":"cell_angle","text":"
    cell_angle() -> Optional[Tuple[float, float, float]]\n

    Return cell angle (in degrees).

    Source code in atomlib/io/cif.py
    def cell_angle(self) -> t.Optional[t.Tuple[float, float, float]]:\n    \"\"\"Return cell angle (in degrees).\"\"\"\n    try:\n        a = float(self['cell_angle_alpha'])  # type: ignore\n        b = float(self['cell_angle_beta'])   # type: ignore\n        g = float(self['cell_angle_gamma'])  # type: ignore\n        return (a, b, g)\n    except (ValueError, TypeError, KeyError):\n        return None\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFDataBlock.get_symmetry","title":"get_symmetry","text":"
    get_symmetry() -> Iterator[AffineTransform3D]\n
    Source code in atomlib/io/cif.py
    def get_symmetry(self) -> t.Iterator[AffineTransform3D]:\n    syms = self.data_dict.get('space_group_symop_operation_xyz')\n    if syms is None:\n        # old name for symmetry\n        syms = self.data_dict.get('symmetry_equiv_pos_as_xyz')\n    if syms is None:\n        syms = ()\n    if not hasattr(syms, '__iter__'):\n        syms = (syms,)\n    return map(parse_symmetry, map(str, syms))  # type: ignore\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFTable","title":"CIFTable dataclass","text":"Source code in atomlib/io/cif.py
    @dataclass\nclass CIFTable:\n    data: t.Dict[str, t.List[Value]]\n\n    def _write(self, f: TextIOBase):\n        print(\"\\nloop_\", file=f)\n        for tag in self.data.keys():\n            print(f\" _{tag}\", file=f)\n\n        for row in zip(*self.data.values()):\n            print(f' {\"  \".join(map(_format_val, row))}', file=f)\n\n        print(file=f)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CIFTable.data","title":"data instance-attribute","text":"
    data: Dict[str, List[Value]]\n
    "},{"location":"api/io/cif/#atomlib.io.cif.SymmetryVec","title":"SymmetryVec","text":"Source code in atomlib/io/cif.py
    class SymmetryVec:\n    @classmethod\n    def parse(cls, s: str) -> SymmetryVec:\n        if s[0] in ('x', 'y', 'z'):\n            a = numpy.zeros((4,))\n            a[('x', 'y', 'z').index(s[0])] += 1.\n            return cls(a)\n        return cls(float(s))\n\n    def __init__(self, val: t.Union[float, NDArray[numpy.floating]]):\n       self.inner: t.Union[float, NDArray[numpy.floating]] = val\n\n    def is_scalar(self) -> bool:\n        return isinstance(self.inner, float)\n\n    def to_vec(self) -> NDArray[numpy.floating]:\n        if isinstance(self.inner, (int, float)):\n            vec = numpy.zeros((4,))\n            vec[3] = self.inner\n            return vec\n        return self.inner\n\n    def __add__(self, rhs: SymmetryVec) -> SymmetryVec:\n        if self.is_scalar() and rhs.is_scalar():\n            return SymmetryVec(self.inner + rhs.inner)\n        return SymmetryVec(rhs.to_vec() + self.to_vec())\n\n    def __neg__(self) -> SymmetryVec:\n        return SymmetryVec(-self.inner)\n\n    def __pos__(self) -> SymmetryVec:\n        return self\n\n    def __sub__(self, rhs: SymmetryVec) -> SymmetryVec:\n        if self.is_scalar() and rhs.is_scalar():\n            return SymmetryVec(self.inner - rhs.inner)\n        return SymmetryVec(rhs.to_vec() - self.to_vec())\n\n    def __mul__(self, rhs: SymmetryVec) -> SymmetryVec:\n        if not self.is_scalar() and not rhs.is_scalar():\n            raise ValueError(\"Can't multiply two symmetry directions\")\n        return SymmetryVec(rhs.inner * self.inner)\n\n    def __truediv__(self, rhs: SymmetryVec) -> SymmetryVec:\n        if not self.is_scalar() and not rhs.is_scalar():\n            raise ValueError(\"Can't divide two symmetry directions\")\n        return SymmetryVec(rhs.inner / self.inner)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.SymmetryVec.inner","title":"inner instance-attribute","text":"
    inner: Union[float, NDArray[floating]] = val\n
    "},{"location":"api/io/cif/#atomlib.io.cif.SymmetryVec.parse","title":"parse classmethod","text":"
    parse(s: str) -> SymmetryVec\n
    Source code in atomlib/io/cif.py
    @classmethod\ndef parse(cls, s: str) -> SymmetryVec:\n    if s[0] in ('x', 'y', 'z'):\n        a = numpy.zeros((4,))\n        a[('x', 'y', 'z').index(s[0])] += 1.\n        return cls(a)\n    return cls(float(s))\n
    "},{"location":"api/io/cif/#atomlib.io.cif.SymmetryVec.is_scalar","title":"is_scalar","text":"
    is_scalar() -> bool\n
    Source code in atomlib/io/cif.py
    def is_scalar(self) -> bool:\n    return isinstance(self.inner, float)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.SymmetryVec.to_vec","title":"to_vec","text":"
    to_vec() -> NDArray[floating]\n
    Source code in atomlib/io/cif.py
    def to_vec(self) -> NDArray[numpy.floating]:\n    if isinstance(self.inner, (int, float)):\n        vec = numpy.zeros((4,))\n        vec[3] = self.inner\n        return vec\n    return self.inner\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader","title":"CifReader","text":"Source code in atomlib/io/cif.py
    class CifReader:\n    def __init__(self, file: TextIOBase):\n        self.line = 0\n        self._file: TextIOBase = file\n        self._buf: t.Optional[str] = None\n        self._after_eol = True\n        self._eof = False\n\n    def parse(self) -> t.Iterator[CIFDataBlock]:\n        while True:\n            line = self.line\n            word = self.peek_word()\n            if word is None:\n                return\n            if word.lower().startswith('data_'):\n                self.next_word()\n                name = word[len('data_'):]\n            elif word.startswith('_'):\n                name = None\n            else:\n                raise ValueError(f\"While parsing line {line}: Unexpected token {word}\")\n\n            yield self.parse_datablock(name)\n\n    def after_eol(self) -> bool:\n        \"\"\"\n        Returns whether the current token (the one that will be returned\n        by the next peek() or next()) is after a newline.\n        \"\"\"\n        return self._after_eol\n\n    def peek_line(self) -> t.Optional[str]:\n        buf = self._try_fill_buf()\n        return buf\n\n    def next_line(self) -> t.Optional[str]:\n        line = self.peek_line()\n        self._buf = None\n        return line\n\n    def next_until(self, marker: str) -> t.Optional[str]:\n        \"\"\"\n        Collect words until `marker`. Because of the weirdness of CIF,\n        `marker` must occur immediately before a whitespace boundary.\n        \"\"\"\n        s = \"\"\n        buf = self._try_fill_buf()\n        if buf is None:\n            return None\n        while not (match := re.search(re.escape(marker) + r'(?=\\s|$)', buf)):\n            s += buf\n            buf = self._try_fill_buf(True)\n            if buf is None:\n                return None\n        s += buf[:match.end()]\n        self._buf = buf[match.end():]\n        if len(self._buf) == 0 or self._buf.isspace():\n            self._buf = None\n        return s\n\n    def peek_word(self) -> t.Optional[str]:\n        while True:\n            buf = self._try_fill_buf()\n            if buf is None:\n                return None\n            buf = buf.lstrip()\n            if len(buf) == 0 or buf.isspace() or buf.startswith('#'):\n                # eat comment or blank line\n                self._buf = None\n                continue\n            break\n\n        #print(f\"buf: '{buf}'\")\n        return buf.split(maxsplit=1)[0]\n\n    def next_word(self) -> t.Optional[str]:\n        w = self.peek_word()\n        if w is None:\n            return None\n        assert self._buf is not None\n        self._buf = self._buf.lstrip()[len(w)+1:].lstrip()\n        if len(self._buf) == 0 or self._buf.isspace():\n            # eat whitespace at end of line\n            self._buf = None\n            self._after_eol = True\n        else:\n            self._after_eol = False\n        return w\n\n    def _try_fill_buf(self, force: bool = False) -> t.Optional[str]:\n        if force:\n            self._buf = None\n        if self._buf is None:\n            try:\n                self._buf = next(self._file)\n                self.line += 1\n            except StopIteration:\n                pass\n        return self._buf\n\n    def parse_bare(self) -> t.Union[int, float, str]:\n        w = self.next_word()\n        if w is None:\n            raise ValueError(\"Unexpected EOF while parsing value.\")\n        if _INT_RE.fullmatch(w):\n            return int(w)  # may raise\n        if (m := _FLOAT_RE.fullmatch(w)):\n            if m[1] != '.':\n                return float(m[1])  # may raise\n        return w\n\n    def parse_datablock(self, name: t.Optional[str] = None) -> CIFDataBlock:\n        logging.debug(f\"parse datablock '{name}'\")\n        #data: t.Dict[str, t.Union[t.List[Value], Value]] = {}\n\n        data: t.List[t.Union[CIFTable, t.Tuple[str, Value]]] = []\n\n        while True:\n            word = self.peek_word()\n            if word is None:\n                break\n            if word.lower() == 'loop_':\n                self.next_word()\n                data.append(self.parse_loop())\n            elif word.startswith('_'):\n                self.next_word()\n                (k, v) = (word[1:], self.parse_value())\n                logging.debug(f\"{k} = {v}\")\n                data.append((k, v))\n            else:\n                break\n\n        return CIFDataBlock(name, tuple(data))\n\n    def eat_saveframe(self):\n        line = self.line\n        while True:\n            w = self.next_word()\n            if w is None:\n                raise ValueError(f\"EOF before end of save frame starting at line {line}\")\n            if w.lower() == 'save_':\n                break\n\n    def parse_loop(self) -> CIFTable:\n        line = self.line\n        tags = []\n        while True:\n            w = self.peek_word()\n            if w is None:\n                raise ValueError(f\"EOF before loop values at line {line}\")\n            if w.startswith('_'):\n                self.next_word()\n                tags.append(w[1:])\n            else:\n                break\n\n        vals: t.Tuple[t.List[Value], ...] = tuple([] for _ in tags)\n        i = 0\n\n        while True:\n            w = self.peek_word()\n            if w is None or w.startswith('_') or w.endswith('_'):\n                break\n            vals[i].append(self.parse_value())\n            i = (i + 1) % len(tags)\n\n        if i != 0:\n            n_vals = sum(map(len, vals))\n            raise ValueError(f\"While parsing loop at line {line}: \"\n                            f\"Got {n_vals} vals, expected a multiple of {len(tags)}\")\n\n        return CIFTable(dict(zip(tags, vals)))\n\n    def parse_value(self) -> Value:\n        logging.debug(\"parse_value\")\n        w = self.peek_word()\n        assert w is not None\n        if w in ('.', '?'):\n            self.next_word()\n            return None\n\n        if self.after_eol() and w == ';':\n            return self.parse_text_field()\n\n        if w[0] in ('\"', \"'\"):\n            return self.parse_quoted()\n\n        return self.parse_bare()\n\n    def parse_text_field(self) -> str:\n        start_line = self.line\n        line = self.next_line()\n        assert line is not None\n        s = line.lstrip().removeprefix(';').lstrip()\n        while True:\n            line = self.next_line()\n            if line is None:\n                raise ValueError(f\"While parsing text field at line {start_line}: Unexpected EOF\")\n            if line.strip() == ';':\n                break\n            s += line\n        return s.rstrip()\n\n    def parse_quoted(self) -> str:\n        line = self.line\n        w = self.peek_word()\n        assert w is not None\n        quote = w[0]\n        if quote not in ('\"', \"'\"):\n            raise ValueError(f\"While parsing string at line {line}: Invalid quote char {quote}\")\n\n        s = self.next_until(quote)\n        if s is None:\n            raise ValueError(f\"While parsing string {w}... at line {line}: Unexpected EOF\")\n        return s.lstrip()[1:-1]\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.line","title":"line instance-attribute","text":"
    line = 0\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.parse","title":"parse","text":"
    parse() -> Iterator[CIFDataBlock]\n
    Source code in atomlib/io/cif.py
    def parse(self) -> t.Iterator[CIFDataBlock]:\n    while True:\n        line = self.line\n        word = self.peek_word()\n        if word is None:\n            return\n        if word.lower().startswith('data_'):\n            self.next_word()\n            name = word[len('data_'):]\n        elif word.startswith('_'):\n            name = None\n        else:\n            raise ValueError(f\"While parsing line {line}: Unexpected token {word}\")\n\n        yield self.parse_datablock(name)\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.after_eol","title":"after_eol","text":"
    after_eol() -> bool\n

    Returns whether the current token (the one that will be returned by the next peek() or next()) is after a newline.

    Source code in atomlib/io/cif.py
    def after_eol(self) -> bool:\n    \"\"\"\n    Returns whether the current token (the one that will be returned\n    by the next peek() or next()) is after a newline.\n    \"\"\"\n    return self._after_eol\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.peek_line","title":"peek_line","text":"
    peek_line() -> Optional[str]\n
    Source code in atomlib/io/cif.py
    def peek_line(self) -> t.Optional[str]:\n    buf = self._try_fill_buf()\n    return buf\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.next_line","title":"next_line","text":"
    next_line() -> Optional[str]\n
    Source code in atomlib/io/cif.py
    def next_line(self) -> t.Optional[str]:\n    line = self.peek_line()\n    self._buf = None\n    return line\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.next_until","title":"next_until","text":"
    next_until(marker: str) -> Optional[str]\n

    Collect words until marker. Because of the weirdness of CIF, marker must occur immediately before a whitespace boundary.

    Source code in atomlib/io/cif.py
    def next_until(self, marker: str) -> t.Optional[str]:\n    \"\"\"\n    Collect words until `marker`. Because of the weirdness of CIF,\n    `marker` must occur immediately before a whitespace boundary.\n    \"\"\"\n    s = \"\"\n    buf = self._try_fill_buf()\n    if buf is None:\n        return None\n    while not (match := re.search(re.escape(marker) + r'(?=\\s|$)', buf)):\n        s += buf\n        buf = self._try_fill_buf(True)\n        if buf is None:\n            return None\n    s += buf[:match.end()]\n    self._buf = buf[match.end():]\n    if len(self._buf) == 0 or self._buf.isspace():\n        self._buf = None\n    return s\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.peek_word","title":"peek_word","text":"
    peek_word() -> Optional[str]\n
    Source code in atomlib/io/cif.py
    def peek_word(self) -> t.Optional[str]:\n    while True:\n        buf = self._try_fill_buf()\n        if buf is None:\n            return None\n        buf = buf.lstrip()\n        if len(buf) == 0 or buf.isspace() or buf.startswith('#'):\n            # eat comment or blank line\n            self._buf = None\n            continue\n        break\n\n    #print(f\"buf: '{buf}'\")\n    return buf.split(maxsplit=1)[0]\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.next_word","title":"next_word","text":"
    next_word() -> Optional[str]\n
    Source code in atomlib/io/cif.py
    def next_word(self) -> t.Optional[str]:\n    w = self.peek_word()\n    if w is None:\n        return None\n    assert self._buf is not None\n    self._buf = self._buf.lstrip()[len(w)+1:].lstrip()\n    if len(self._buf) == 0 or self._buf.isspace():\n        # eat whitespace at end of line\n        self._buf = None\n        self._after_eol = True\n    else:\n        self._after_eol = False\n    return w\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.parse_bare","title":"parse_bare","text":"
    parse_bare() -> Union[int, float, str]\n
    Source code in atomlib/io/cif.py
    def parse_bare(self) -> t.Union[int, float, str]:\n    w = self.next_word()\n    if w is None:\n        raise ValueError(\"Unexpected EOF while parsing value.\")\n    if _INT_RE.fullmatch(w):\n        return int(w)  # may raise\n    if (m := _FLOAT_RE.fullmatch(w)):\n        if m[1] != '.':\n            return float(m[1])  # may raise\n    return w\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.parse_datablock","title":"parse_datablock","text":"
    parse_datablock(name: Optional[str] = None) -> CIFDataBlock\n
    Source code in atomlib/io/cif.py
    def parse_datablock(self, name: t.Optional[str] = None) -> CIFDataBlock:\n    logging.debug(f\"parse datablock '{name}'\")\n    #data: t.Dict[str, t.Union[t.List[Value], Value]] = {}\n\n    data: t.List[t.Union[CIFTable, t.Tuple[str, Value]]] = []\n\n    while True:\n        word = self.peek_word()\n        if word is None:\n            break\n        if word.lower() == 'loop_':\n            self.next_word()\n            data.append(self.parse_loop())\n        elif word.startswith('_'):\n            self.next_word()\n            (k, v) = (word[1:], self.parse_value())\n            logging.debug(f\"{k} = {v}\")\n            data.append((k, v))\n        else:\n            break\n\n    return CIFDataBlock(name, tuple(data))\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.eat_saveframe","title":"eat_saveframe","text":"
    eat_saveframe()\n
    Source code in atomlib/io/cif.py
    def eat_saveframe(self):\n    line = self.line\n    while True:\n        w = self.next_word()\n        if w is None:\n            raise ValueError(f\"EOF before end of save frame starting at line {line}\")\n        if w.lower() == 'save_':\n            break\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.parse_loop","title":"parse_loop","text":"
    parse_loop() -> CIFTable\n
    Source code in atomlib/io/cif.py
    def parse_loop(self) -> CIFTable:\n    line = self.line\n    tags = []\n    while True:\n        w = self.peek_word()\n        if w is None:\n            raise ValueError(f\"EOF before loop values at line {line}\")\n        if w.startswith('_'):\n            self.next_word()\n            tags.append(w[1:])\n        else:\n            break\n\n    vals: t.Tuple[t.List[Value], ...] = tuple([] for _ in tags)\n    i = 0\n\n    while True:\n        w = self.peek_word()\n        if w is None or w.startswith('_') or w.endswith('_'):\n            break\n        vals[i].append(self.parse_value())\n        i = (i + 1) % len(tags)\n\n    if i != 0:\n        n_vals = sum(map(len, vals))\n        raise ValueError(f\"While parsing loop at line {line}: \"\n                        f\"Got {n_vals} vals, expected a multiple of {len(tags)}\")\n\n    return CIFTable(dict(zip(tags, vals)))\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.parse_value","title":"parse_value","text":"
    parse_value() -> Value\n
    Source code in atomlib/io/cif.py
    def parse_value(self) -> Value:\n    logging.debug(\"parse_value\")\n    w = self.peek_word()\n    assert w is not None\n    if w in ('.', '?'):\n        self.next_word()\n        return None\n\n    if self.after_eol() and w == ';':\n        return self.parse_text_field()\n\n    if w[0] in ('\"', \"'\"):\n        return self.parse_quoted()\n\n    return self.parse_bare()\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.parse_text_field","title":"parse_text_field","text":"
    parse_text_field() -> str\n
    Source code in atomlib/io/cif.py
    def parse_text_field(self) -> str:\n    start_line = self.line\n    line = self.next_line()\n    assert line is not None\n    s = line.lstrip().removeprefix(';').lstrip()\n    while True:\n        line = self.next_line()\n        if line is None:\n            raise ValueError(f\"While parsing text field at line {start_line}: Unexpected EOF\")\n        if line.strip() == ';':\n            break\n        s += line\n    return s.rstrip()\n
    "},{"location":"api/io/cif/#atomlib.io.cif.CifReader.parse_quoted","title":"parse_quoted","text":"
    parse_quoted() -> str\n
    Source code in atomlib/io/cif.py
    def parse_quoted(self) -> str:\n    line = self.line\n    w = self.peek_word()\n    assert w is not None\n    quote = w[0]\n    if quote not in ('\"', \"'\"):\n        raise ValueError(f\"While parsing string at line {line}: Invalid quote char {quote}\")\n\n    s = self.next_until(quote)\n    if s is None:\n        raise ValueError(f\"While parsing string {w}... at line {line}: Unexpected EOF\")\n    return s.lstrip()[1:-1]\n
    "},{"location":"api/io/cif/#atomlib.io.cif.parse_symmetry","title":"parse_symmetry","text":"
    parse_symmetry(s: str) -> AffineTransform3D\n
    Source code in atomlib/io/cif.py
    def parse_symmetry(s: str) -> AffineTransform3D:\n    axes = s.split(',')\n    if not len(axes) == 3:\n        raise ValueError(f\"Error parsing symmetry expression '{s}': Expected 3 values, got {len(axes)}\")\n\n    axes = [SYMMETRY_PARSER.parse(StringIO(ax)).eval(lambda v: v).to_vec() for ax in axes]\n    axes.append(numpy.array([0., 0., 0., 1.]))\n    return AffineTransform3D(numpy.stack(axes, axis=0))\n
    "},{"location":"api/io/lmp/","title":"atomlib.io.lmp","text":"

    IO for LAAMPS data files, as described here.

    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMP","title":"LMP dataclass","text":"Source code in atomlib/io/lmp.py
    @dataclass\nclass LMP:\n    comment: t.Optional[str]\n    headers: t.Dict[str, t.Any]\n    sections: t.Tuple[LMPSection, ...]\n\n    def get_cell(self) -> Cell:\n        dims = numpy.array([\n            self.headers.get(f\"{c}lo {c}hi\", (-0.5, 0.5))\n            for c in \"xyz\"\n        ])\n        origin = dims[:, 0]\n        tilts = self.headers.get(\"xy xz yz\", (0., 0., 0.))\n\n        ortho = numpy.diag(dims[:, 1] - dims[:, 0])\n        (ortho[0, 1], ortho[0, 2], ortho[1, 2]) = tilts\n\n        return Cell.from_ortho(LinearTransform3D(ortho).translate(origin))\n\n    def get_atoms(self, type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> AtomCell:\n        if type_map is not None:\n            try:\n                type_map_df = polars.DataFrame({\n                    'type': polars.Series(type_map.keys(), dtype=polars.Int32),\n                    'elem': polars.Series(list(map(get_elem, type_map.values())), dtype=polars.UInt8),\n                    'symbol': polars.Series([get_sym(v) if isinstance(v, int) else v for v in type_map.values()], dtype=polars.Utf8),\n                })\n            except ValueError as e:\n                raise ValueError(\"Invalid type map\") from e\n        else:\n            type_map_df = None\n\n        cell = self.get_cell()\n\n        def _apply_type_labels(df: polars.DataFrame, section_name: str, labels: t.Optional[polars.DataFrame] = None) -> polars.DataFrame:\n            if labels is not None:\n                #df = df.with_columns(polars.col('type').replace(d, default=polars.col('type').cast(polars.Int32, strict=False), return_dtype=polars.Int32))\n                df = df.with_columns(polars.col('type').replace_strict(labels['symbol'], labels['type'], default=polars.col('type').cast(polars.Int32, strict=False), return_dtype=polars.Int32))\n                if df['type'].is_null().any():\n                    raise ValueError(f\"While parsing section {section_name}: Unknown atom label or invalid atom type\")\n            try:\n                return df.with_columns(polars.col('type').cast(polars.Int32))\n            except polars.ComputeError:\n                raise ValueError(f\"While parsing section {section_name}: Invalid atom type(s)\")\n\n        atoms: t.Optional[polars.DataFrame] = None\n        labels: t.Optional[polars.DataFrame] = None\n        masses: t.Optional[polars.DataFrame] = None\n        velocities = None\n\n        for section in self.sections:\n            start_line = section.start_line + 1\n\n            if section.name == 'Atoms':\n                if section.style not in (None, 'atomic'):\n                    # TODO support other styles\n                    raise ValueError(f\"Only 'atomic' atom_style is supported, instead got '{section.style}'\")\n\n                atoms = parse_whitespace_separated(section.body, {\n                    'i': polars.Int64, 'type': polars.Utf8,\n                    'coords': polars.Array(polars.Float64, 3),\n                }, start_line=start_line)\n                atoms = _apply_type_labels(atoms, 'Atoms', labels)\n            elif section.name == 'Atom Type Labels':\n                labels = parse_whitespace_separated(section.body, {'type': polars.Int32, 'symbol': polars.Utf8}, start_line=start_line)\n            elif section.name == 'Masses':\n                masses = parse_whitespace_separated(section.body, {'type': polars.Utf8, 'mass': polars.Float64}, start_line=start_line)\n                masses = _apply_type_labels(masses, 'Masses', labels)\n            elif section.name == 'Velocities':\n                velocities = parse_whitespace_separated(section.body, {\n                    'i': polars.Int64, 'velocity': polars.Array(polars.Float64, 3),\n                }, start_line=start_line)\n\n        # now all 'type's should be in Int32\n\n        if atoms is None:\n            if self.headers['atoms'] > 0:\n                raise ValueError(\"Missing required section 'Atoms'\")\n            return AtomCell(Atoms.empty(), cell=cell, frame='local')\n\n        # next we need to assign element symbols\n        # first, if type_map is specified, use that:\n        #if type_map_elem is not None and type_map_sym is not None:\n        if type_map_df is not None:\n            try:\n                atoms = checked_left_join(atoms, type_map_df, on='type')\n            except CheckedJoinError as e:\n                raise ValueError(f\"Missing type_map specification for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n        elif labels is not None:\n            try:\n                labels = labels.with_columns(get_elem(labels['symbol']))\n            except ValueError as e:\n                raise ValueError(\"Failed to auto-detect elements from type labels. Please pass 'type_map' explicitly\") from e\n            try:\n                atoms = checked_left_join(atoms, labels, 'type')\n            except CheckedJoinError as e:\n                raise ValueError(f\"Missing labels for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n        # otherwise we have no way\n        else:\n            raise ValueError(\"Failed to auto-detect elements from type labels. Please pass 'type_map' explicitly\")\n\n        if velocities is not None:\n            # join velocities\n            try:\n                # TODO use join_asof here?\n                atoms = checked_left_join(atoms, velocities, 'i')\n            except CheckedJoinError as e:\n                raise ValueError(f\"Missing velocities for {len(e.missing_keys)}/{len(atoms)} atoms\")\n\n        if masses is not None:\n            # join masses\n            try:\n                atoms = checked_left_join(atoms, masses, 'type')\n            except CheckedJoinError as e:\n                raise ValueError(f\"Missing masses for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n\n        return AtomCell(atoms, cell=cell, frame='local')\n\n    @staticmethod\n    def from_atoms(atoms: HasAtoms) -> LMP:\n        if isinstance(atoms, HasAtomCell):\n            # we're basically converting everything to the ortho frame, but including the affine shift\n\n            # transform affine shift into ortho frame\n            origin = atoms.get_transform('ortho', 'local').to_linear().round_near_zero() \\\n                .transform(atoms.get_cell().affine.translation())\n\n            # get the orthogonalization transform only, without affine\n            ortho = atoms.get_transform('ortho', 'cell_box').to_linear().round_near_zero().inner\n\n            # get atoms in ortho frame, and then add the affine shift\n            frame = atoms.get_atoms('ortho').transform_atoms(AffineTransform3D.translate(origin)) \\\n                .round_near_zero().with_type()\n        else:\n            bbox = atoms.bbox_atoms()\n            ortho = numpy.diag(bbox.size)\n            origin = bbox.min\n\n            frame = atoms.get_atoms('local').with_type()\n\n        types = frame.unique(subset='type')\n        types = types.with_mass().sort('type')\n\n        now = localtime()\n        comment = f\"# Generated by atomlib on {now.isoformat(' ', 'seconds')}\"\n\n        headers = {}\n        sections = []\n\n        headers['atoms'] = len(frame)\n        headers['atom types'] = len(types)\n\n        for (s, low, diff) in zip(('x', 'y', 'z'), origin, ortho.diagonal()):\n            headers[f\"{s}lo {s}hi\"] = (low, low + diff)\n\n        headers['xy xz yz'] = (ortho[0, 1], ortho[0, 2], ortho[1, 2])\n\n        body = [\n            f\" {ty:8} {sym:>4}\\n\"\n            for (ty, sym) in types.select('type', 'symbol').rows()\n        ]\n        sections.append(LMPSection(\"Atom Type Labels\", tuple(body)))\n\n        if 'mass' in types:\n            body = [\n                f\" {ty:8} {mass:14.7f}  # {sym}\\n\"\n                for (ty, sym, mass) in types.select(('type', 'symbol', 'mass')).rows()\n            ]\n            sections.append(LMPSection(\"Masses\", tuple(body)))\n\n        body = [\n            f\" {i+1:8} {ty:4} {x:14.7f} {y:14.7f} {z:14.7f}\\n\"\n            for (i, (ty, (x, y, z))) in enumerate(frame.select(('type', 'coords')).rows())\n        ]\n        sections.append(LMPSection(\"Atoms\", tuple(body), 'atomic'))\n\n        if (velocities := frame.velocities()) is not None:\n            body = [\n                f\" {i+1:8} {v_x:14.7f} {v_y:14.7f} {v_z:14.7f}\\n\"\n                for (i, (v_x, v_y, v_z)) in enumerate(velocities)\n            ]\n            sections.append(LMPSection(\"Velocities\", tuple(body)))\n\n        return LMP(comment, headers, tuple(sections))\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> LMP:\n        with open_file(file, 'r') as f:\n            return LMPReader(f).parse()\n\n    def write(self, file: FileOrPath):\n        with open_file(file, 'w') as f:\n            print((self.comment or \"\") + '\\n', file=f)\n\n            # print headers\n            for (name, val) in self.headers.items():\n                val = _HEADER_FMT.get(name, lambda s: f\"{s:8}\")(val)\n                print(f\" {val} {name}\", file=f)\n\n            # print sections\n            for section in self.sections:\n                line = section.name\n                if section.style is not None:\n                    line += f'  # {section.style}'\n                print(f\"\\n{line}\\n\", file=f)\n\n                f.writelines(section.body)\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMP.comment","title":"comment instance-attribute","text":"
    comment: Optional[str]\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMP.headers","title":"headers instance-attribute","text":"
    headers: Dict[str, Any]\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMP.sections","title":"sections instance-attribute","text":"
    sections: Tuple[LMPSection, ...]\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMP.get_cell","title":"get_cell","text":"
    get_cell() -> Cell\n
    Source code in atomlib/io/lmp.py
    def get_cell(self) -> Cell:\n    dims = numpy.array([\n        self.headers.get(f\"{c}lo {c}hi\", (-0.5, 0.5))\n        for c in \"xyz\"\n    ])\n    origin = dims[:, 0]\n    tilts = self.headers.get(\"xy xz yz\", (0., 0., 0.))\n\n    ortho = numpy.diag(dims[:, 1] - dims[:, 0])\n    (ortho[0, 1], ortho[0, 2], ortho[1, 2]) = tilts\n\n    return Cell.from_ortho(LinearTransform3D(ortho).translate(origin))\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMP.get_atoms","title":"get_atoms","text":"
    get_atoms(\n    type_map: Optional[Dict[int, Union[str, int]]] = None\n) -> AtomCell\n
    Source code in atomlib/io/lmp.py
    def get_atoms(self, type_map: t.Optional[t.Dict[int, t.Union[str, int]]] = None) -> AtomCell:\n    if type_map is not None:\n        try:\n            type_map_df = polars.DataFrame({\n                'type': polars.Series(type_map.keys(), dtype=polars.Int32),\n                'elem': polars.Series(list(map(get_elem, type_map.values())), dtype=polars.UInt8),\n                'symbol': polars.Series([get_sym(v) if isinstance(v, int) else v for v in type_map.values()], dtype=polars.Utf8),\n            })\n        except ValueError as e:\n            raise ValueError(\"Invalid type map\") from e\n    else:\n        type_map_df = None\n\n    cell = self.get_cell()\n\n    def _apply_type_labels(df: polars.DataFrame, section_name: str, labels: t.Optional[polars.DataFrame] = None) -> polars.DataFrame:\n        if labels is not None:\n            #df = df.with_columns(polars.col('type').replace(d, default=polars.col('type').cast(polars.Int32, strict=False), return_dtype=polars.Int32))\n            df = df.with_columns(polars.col('type').replace_strict(labels['symbol'], labels['type'], default=polars.col('type').cast(polars.Int32, strict=False), return_dtype=polars.Int32))\n            if df['type'].is_null().any():\n                raise ValueError(f\"While parsing section {section_name}: Unknown atom label or invalid atom type\")\n        try:\n            return df.with_columns(polars.col('type').cast(polars.Int32))\n        except polars.ComputeError:\n            raise ValueError(f\"While parsing section {section_name}: Invalid atom type(s)\")\n\n    atoms: t.Optional[polars.DataFrame] = None\n    labels: t.Optional[polars.DataFrame] = None\n    masses: t.Optional[polars.DataFrame] = None\n    velocities = None\n\n    for section in self.sections:\n        start_line = section.start_line + 1\n\n        if section.name == 'Atoms':\n            if section.style not in (None, 'atomic'):\n                # TODO support other styles\n                raise ValueError(f\"Only 'atomic' atom_style is supported, instead got '{section.style}'\")\n\n            atoms = parse_whitespace_separated(section.body, {\n                'i': polars.Int64, 'type': polars.Utf8,\n                'coords': polars.Array(polars.Float64, 3),\n            }, start_line=start_line)\n            atoms = _apply_type_labels(atoms, 'Atoms', labels)\n        elif section.name == 'Atom Type Labels':\n            labels = parse_whitespace_separated(section.body, {'type': polars.Int32, 'symbol': polars.Utf8}, start_line=start_line)\n        elif section.name == 'Masses':\n            masses = parse_whitespace_separated(section.body, {'type': polars.Utf8, 'mass': polars.Float64}, start_line=start_line)\n            masses = _apply_type_labels(masses, 'Masses', labels)\n        elif section.name == 'Velocities':\n            velocities = parse_whitespace_separated(section.body, {\n                'i': polars.Int64, 'velocity': polars.Array(polars.Float64, 3),\n            }, start_line=start_line)\n\n    # now all 'type's should be in Int32\n\n    if atoms is None:\n        if self.headers['atoms'] > 0:\n            raise ValueError(\"Missing required section 'Atoms'\")\n        return AtomCell(Atoms.empty(), cell=cell, frame='local')\n\n    # next we need to assign element symbols\n    # first, if type_map is specified, use that:\n    #if type_map_elem is not None and type_map_sym is not None:\n    if type_map_df is not None:\n        try:\n            atoms = checked_left_join(atoms, type_map_df, on='type')\n        except CheckedJoinError as e:\n            raise ValueError(f\"Missing type_map specification for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n    elif labels is not None:\n        try:\n            labels = labels.with_columns(get_elem(labels['symbol']))\n        except ValueError as e:\n            raise ValueError(\"Failed to auto-detect elements from type labels. Please pass 'type_map' explicitly\") from e\n        try:\n            atoms = checked_left_join(atoms, labels, 'type')\n        except CheckedJoinError as e:\n            raise ValueError(f\"Missing labels for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n    # otherwise we have no way\n    else:\n        raise ValueError(\"Failed to auto-detect elements from type labels. Please pass 'type_map' explicitly\")\n\n    if velocities is not None:\n        # join velocities\n        try:\n            # TODO use join_asof here?\n            atoms = checked_left_join(atoms, velocities, 'i')\n        except CheckedJoinError as e:\n            raise ValueError(f\"Missing velocities for {len(e.missing_keys)}/{len(atoms)} atoms\")\n\n    if masses is not None:\n        # join masses\n        try:\n            atoms = checked_left_join(atoms, masses, 'type')\n        except CheckedJoinError as e:\n            raise ValueError(f\"Missing masses for atom type(s): {', '.join(map(repr, e.missing_keys))}\")\n\n    return AtomCell(atoms, cell=cell, frame='local')\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMP.from_atoms","title":"from_atoms staticmethod","text":"
    from_atoms(atoms: HasAtoms) -> LMP\n
    Source code in atomlib/io/lmp.py
    @staticmethod\ndef from_atoms(atoms: HasAtoms) -> LMP:\n    if isinstance(atoms, HasAtomCell):\n        # we're basically converting everything to the ortho frame, but including the affine shift\n\n        # transform affine shift into ortho frame\n        origin = atoms.get_transform('ortho', 'local').to_linear().round_near_zero() \\\n            .transform(atoms.get_cell().affine.translation())\n\n        # get the orthogonalization transform only, without affine\n        ortho = atoms.get_transform('ortho', 'cell_box').to_linear().round_near_zero().inner\n\n        # get atoms in ortho frame, and then add the affine shift\n        frame = atoms.get_atoms('ortho').transform_atoms(AffineTransform3D.translate(origin)) \\\n            .round_near_zero().with_type()\n    else:\n        bbox = atoms.bbox_atoms()\n        ortho = numpy.diag(bbox.size)\n        origin = bbox.min\n\n        frame = atoms.get_atoms('local').with_type()\n\n    types = frame.unique(subset='type')\n    types = types.with_mass().sort('type')\n\n    now = localtime()\n    comment = f\"# Generated by atomlib on {now.isoformat(' ', 'seconds')}\"\n\n    headers = {}\n    sections = []\n\n    headers['atoms'] = len(frame)\n    headers['atom types'] = len(types)\n\n    for (s, low, diff) in zip(('x', 'y', 'z'), origin, ortho.diagonal()):\n        headers[f\"{s}lo {s}hi\"] = (low, low + diff)\n\n    headers['xy xz yz'] = (ortho[0, 1], ortho[0, 2], ortho[1, 2])\n\n    body = [\n        f\" {ty:8} {sym:>4}\\n\"\n        for (ty, sym) in types.select('type', 'symbol').rows()\n    ]\n    sections.append(LMPSection(\"Atom Type Labels\", tuple(body)))\n\n    if 'mass' in types:\n        body = [\n            f\" {ty:8} {mass:14.7f}  # {sym}\\n\"\n            for (ty, sym, mass) in types.select(('type', 'symbol', 'mass')).rows()\n        ]\n        sections.append(LMPSection(\"Masses\", tuple(body)))\n\n    body = [\n        f\" {i+1:8} {ty:4} {x:14.7f} {y:14.7f} {z:14.7f}\\n\"\n        for (i, (ty, (x, y, z))) in enumerate(frame.select(('type', 'coords')).rows())\n    ]\n    sections.append(LMPSection(\"Atoms\", tuple(body), 'atomic'))\n\n    if (velocities := frame.velocities()) is not None:\n        body = [\n            f\" {i+1:8} {v_x:14.7f} {v_y:14.7f} {v_z:14.7f}\\n\"\n            for (i, (v_x, v_y, v_z)) in enumerate(velocities)\n        ]\n        sections.append(LMPSection(\"Velocities\", tuple(body)))\n\n    return LMP(comment, headers, tuple(sections))\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMP.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> LMP\n
    Source code in atomlib/io/lmp.py
    @staticmethod\ndef from_file(file: FileOrPath) -> LMP:\n    with open_file(file, 'r') as f:\n        return LMPReader(f).parse()\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMP.write","title":"write","text":"
    write(file: FileOrPath)\n
    Source code in atomlib/io/lmp.py
    def write(self, file: FileOrPath):\n    with open_file(file, 'w') as f:\n        print((self.comment or \"\") + '\\n', file=f)\n\n        # print headers\n        for (name, val) in self.headers.items():\n            val = _HEADER_FMT.get(name, lambda s: f\"{s:8}\")(val)\n            print(f\" {val} {name}\", file=f)\n\n        # print sections\n        for section in self.sections:\n            line = section.name\n            if section.style is not None:\n                line += f'  # {section.style}'\n            print(f\"\\n{line}\\n\", file=f)\n\n            f.writelines(section.body)\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPSection","title":"LMPSection dataclass","text":"Source code in atomlib/io/lmp.py
    @dataclass\nclass LMPSection:\n    name: str\n    body: t.Tuple[str, ...]\n    style: t.Optional[str] = None\n    start_line: int = 0\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPSection.name","title":"name instance-attribute","text":"
    name: str\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPSection.body","title":"body instance-attribute","text":"
    body: Tuple[str, ...]\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPSection.style","title":"style class-attribute instance-attribute","text":"
    style: Optional[str] = None\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPSection.start_line","title":"start_line class-attribute instance-attribute","text":"
    start_line: int = 0\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPReader","title":"LMPReader","text":"Source code in atomlib/io/lmp.py
    class LMPReader:\n    def __init__(self, f: TextIOBase):\n        self.line = 0\n        self._file: TextIOBase = f\n        self._buf: t.Optional[str] = None\n\n    def _split_comment(self, line: str) -> t.Tuple[str, t.Optional[str]]:\n        split = _COMMENT_RE.split(line, maxsplit=1)\n        return (split[0], split[1] if len(split) > 1 else None)\n\n    def parse(self) -> LMP:\n        # parse comment\n        comment = self.next_line(skip_blank=False)\n        if comment is None:\n            raise ValueError(\"Unexpected EOF (file is blank)\")\n        if comment.isspace():\n            comment = None\n        else:\n            comment = comment[:-1]\n\n        headers = self.parse_headers()\n        sections = self.parse_sections(headers)\n\n        return LMP(comment, headers, sections)\n\n    def parse_headers(self) -> t.Dict[str, t.Any]:\n        headers: t.Dict[str, t.Any] = {}\n        while True:\n            line = self.peek_line()\n            if line is None:\n                break\n            body = self._split_comment(line)[0]\n\n            if (match := _HEADER_KW_RE.search(body)) is None:\n                # probably a body\n                break\n            self.next_line()\n\n            name = match[0]\n            value = body[:match.start(0)].strip()\n\n            try:\n                if name in _HEADER_PARSE:\n                    value = _HEADER_PARSE[name](value)\n            except Exception as e:\n                raise ValueError(f\"While parsing header '{name}' at line {self.line}: Failed to parse value '{value}\") from e\n\n            #print(f\"header {name} => {value} (type {type(value)})\")\n            headers[name] = value\n\n        return headers\n\n    def parse_sections(self, headers: t.Dict[str, t.Any]) -> t.Tuple[LMPSection, ...]:\n        first = True\n\n        sections: t.List[LMPSection] = []\n\n        while True:\n            start_line = self.line\n            line = self.next_line()\n            if line is None:\n                break\n            name, comment = self._split_comment(line)\n            name = name.strip()\n\n            try:\n                n_lines_header = _SECTION_KWS[name]\n            except KeyError:\n                if first:\n                    raise ValueError(f\"While parsing line {self.line}: Unknown header or section keyword '{line}'\") from None\n                else:\n                    raise ValueError(f\"While parsing line {self.line}: Unknown section keyword '{line}'\") from None\n\n            try:\n                if n_lines_header is None:\n                    # special case for PairIJ Coeffs:\n                    n = int(headers['atom types'])\n                    n_lines = (n * (n + 1)) // 2\n                else:\n                    n_lines = int(headers[n_lines_header])\n            except KeyError:\n                raise ValueError(f\"While parsing body section '{name}' at line {self.line}: \"\n                                 f\"Missing required header '{n_lines_header or 'atom types'}'\") from None\n\n            style = comment if name in _SECTION_STYLE_KWS else None\n            if style is not None:\n                style = style.strip()\n\n            #print(f\"section '{name}' @ {self.line}, {n_lines} lines, style {style}\")\n\n            lines = self.collect_lines(n_lines)\n            if lines is None:\n                raise ValueError(f\"While parsing body section '{name}' starting at line {self.line}: \"\n                                 f\"Unexpected EOF before {n_lines} lines were read\")\n\n            sections.append(LMPSection(\n                name, tuple(lines), style, start_line\n            ))\n            first = False\n\n        return tuple(sections)\n\n    def _try_fill_buf(self, skip_blank: bool = True) -> t.Optional[str]:\n        if self._buf is None:\n            try:\n                # skip blank lines\n                while True:\n                    self._buf = next(self._file)\n                    self.line += 1\n                    if not (skip_blank and self._buf.isspace()):\n                        break\n            except StopIteration:\n                pass\n        return self._buf\n\n    def peek_line(self, skip_blank: bool = True) -> t.Optional[str]:\n        return self._try_fill_buf(skip_blank)\n\n    def next_line(self, skip_blank: bool = True) -> t.Optional[str]:\n        line = self._try_fill_buf(skip_blank)\n        self._buf = None\n        return line\n\n    def collect_lines(self, n: int) -> t.Optional[t.List[str]]:\n        assert self._buf is None\n        lines = []\n        try:\n            for _ in range(n):\n                while True:\n                    line = next(self._file)\n                    if not line.isspace():\n                        lines.append(line)\n                        break\n        except StopIteration:\n            return None\n        self.line += n\n        return lines\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPReader.line","title":"line instance-attribute","text":"
    line = 0\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPReader.parse","title":"parse","text":"
    parse() -> LMP\n
    Source code in atomlib/io/lmp.py
    def parse(self) -> LMP:\n    # parse comment\n    comment = self.next_line(skip_blank=False)\n    if comment is None:\n        raise ValueError(\"Unexpected EOF (file is blank)\")\n    if comment.isspace():\n        comment = None\n    else:\n        comment = comment[:-1]\n\n    headers = self.parse_headers()\n    sections = self.parse_sections(headers)\n\n    return LMP(comment, headers, sections)\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPReader.parse_headers","title":"parse_headers","text":"
    parse_headers() -> Dict[str, Any]\n
    Source code in atomlib/io/lmp.py
    def parse_headers(self) -> t.Dict[str, t.Any]:\n    headers: t.Dict[str, t.Any] = {}\n    while True:\n        line = self.peek_line()\n        if line is None:\n            break\n        body = self._split_comment(line)[0]\n\n        if (match := _HEADER_KW_RE.search(body)) is None:\n            # probably a body\n            break\n        self.next_line()\n\n        name = match[0]\n        value = body[:match.start(0)].strip()\n\n        try:\n            if name in _HEADER_PARSE:\n                value = _HEADER_PARSE[name](value)\n        except Exception as e:\n            raise ValueError(f\"While parsing header '{name}' at line {self.line}: Failed to parse value '{value}\") from e\n\n        #print(f\"header {name} => {value} (type {type(value)})\")\n        headers[name] = value\n\n    return headers\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPReader.parse_sections","title":"parse_sections","text":"
    parse_sections(\n    headers: Dict[str, Any]\n) -> Tuple[LMPSection, ...]\n
    Source code in atomlib/io/lmp.py
    def parse_sections(self, headers: t.Dict[str, t.Any]) -> t.Tuple[LMPSection, ...]:\n    first = True\n\n    sections: t.List[LMPSection] = []\n\n    while True:\n        start_line = self.line\n        line = self.next_line()\n        if line is None:\n            break\n        name, comment = self._split_comment(line)\n        name = name.strip()\n\n        try:\n            n_lines_header = _SECTION_KWS[name]\n        except KeyError:\n            if first:\n                raise ValueError(f\"While parsing line {self.line}: Unknown header or section keyword '{line}'\") from None\n            else:\n                raise ValueError(f\"While parsing line {self.line}: Unknown section keyword '{line}'\") from None\n\n        try:\n            if n_lines_header is None:\n                # special case for PairIJ Coeffs:\n                n = int(headers['atom types'])\n                n_lines = (n * (n + 1)) // 2\n            else:\n                n_lines = int(headers[n_lines_header])\n        except KeyError:\n            raise ValueError(f\"While parsing body section '{name}' at line {self.line}: \"\n                             f\"Missing required header '{n_lines_header or 'atom types'}'\") from None\n\n        style = comment if name in _SECTION_STYLE_KWS else None\n        if style is not None:\n            style = style.strip()\n\n        #print(f\"section '{name}' @ {self.line}, {n_lines} lines, style {style}\")\n\n        lines = self.collect_lines(n_lines)\n        if lines is None:\n            raise ValueError(f\"While parsing body section '{name}' starting at line {self.line}: \"\n                             f\"Unexpected EOF before {n_lines} lines were read\")\n\n        sections.append(LMPSection(\n            name, tuple(lines), style, start_line\n        ))\n        first = False\n\n    return tuple(sections)\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPReader.peek_line","title":"peek_line","text":"
    peek_line(skip_blank: bool = True) -> Optional[str]\n
    Source code in atomlib/io/lmp.py
    def peek_line(self, skip_blank: bool = True) -> t.Optional[str]:\n    return self._try_fill_buf(skip_blank)\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPReader.next_line","title":"next_line","text":"
    next_line(skip_blank: bool = True) -> Optional[str]\n
    Source code in atomlib/io/lmp.py
    def next_line(self, skip_blank: bool = True) -> t.Optional[str]:\n    line = self._try_fill_buf(skip_blank)\n    self._buf = None\n    return line\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.LMPReader.collect_lines","title":"collect_lines","text":"
    collect_lines(n: int) -> Optional[List[str]]\n
    Source code in atomlib/io/lmp.py
    def collect_lines(self, n: int) -> t.Optional[t.List[str]]:\n    assert self._buf is None\n    lines = []\n    try:\n        for _ in range(n):\n            while True:\n                line = next(self._file)\n                if not line.isspace():\n                    lines.append(line)\n                    break\n    except StopIteration:\n        return None\n    self.line += n\n    return lines\n
    "},{"location":"api/io/lmp/#atomlib.io.lmp.write_lmp","title":"write_lmp","text":"
    write_lmp(atoms: HasAtoms, f: FileOrPath)\n
    Source code in atomlib/io/lmp.py
    def write_lmp(atoms: HasAtoms, f: FileOrPath):\n    LMP.from_atoms(atoms).write(f)\n    return\n
    "},{"location":"api/io/mp_api/","title":"atomlib.io.mp_api","text":"

    IO for the Materials Project API, .

    "},{"location":"api/io/mp_api/#atomlib.io.mp_api.get_api_key","title":"get_api_key","text":"
    get_api_key(key: Optional[str] = None) -> str\n
    Source code in atomlib/io/mp_api.py
    def get_api_key(key: t.Optional[str] = None) -> str:\n    try:\n        return environ['MP_API_KEY']\n    except KeyError:\n        raise RuntimeError(\"No materials project API key specified. \"\n                           \"Either pass `api_key` or set `MP_API_KEY` in your environment.\") from None\n
    "},{"location":"api/io/mp_api/#atomlib.io.mp_api.get_api_endpoint","title":"get_api_endpoint","text":"
    get_api_endpoint() -> str\n
    Source code in atomlib/io/mp_api.py
    def get_api_endpoint() -> str:\n    return environ.get(\"MP_API_ENDPOINT\") or \"https://api.materialsproject.org/\"\n
    "},{"location":"api/io/mp_api/#atomlib.io.mp_api.resolve_id","title":"resolve_id","text":"
    resolve_id(id: Union[str, int]) -> str\n
    Source code in atomlib/io/mp_api.py
    def resolve_id(id: t.Union[str, int]) -> str:\n    if isinstance(id, int):\n        return str(id)\n    if id.lower().startswith('mp-'):\n        return id[3:]\n    return id\n
    "},{"location":"api/io/mp_api/#atomlib.io.mp_api.load_materials_project","title":"load_materials_project","text":"
    load_materials_project(\n    id: Union[str, int],\n    *,\n    api_key: Optional[str] = None,\n    api_endpoint: Optional[str] = None\n) -> AtomCell\n
    Source code in atomlib/io/mp_api.py
    def load_materials_project(id: t.Union[str, int], *, api_key: t.Optional[str] = None,\n                           api_endpoint: t.Optional[str] = None) -> AtomCell:\n    id = resolve_id(id)\n    api_key = api_key or get_api_key()\n    if len(api_key) != 32:\n        raise RuntimeError(\"Materials project API key must be a 32-character alphanumeric string. \"\n                           f\"Instead got '{api_key}' of length '{len(api_key)}'.\")\n    api_endpoint = (api_endpoint or get_api_endpoint()).rstrip('/')\n\n    logging.info(f\"Fetching structure mp-{id} from materials project...\")\n    response: requests.Response = requests.get(\n        api_endpoint + f'/materials/core/mp-{id}/',\n        headers={'X-Api-Key': api_key},\n        params={'_fields': 'structure'}\n    )\n    try:\n        response.raise_for_status()\n    except requests.HTTPError as e:\n        raise ValueError(f\"Failed to fetch structure 'mp-{id}'\") from e\n    try:\n        data = response.json()['data'][0]\n        structure = data['structure']\n    except (KeyError, ValueError, TypeError) as e:\n        raise ValueError(f\"Unexpected API response: {response}\") from e\n\n    ortho = LinearTransform3D(numpy.array(structure['lattice']['matrix']).T)\n    sites = structure['sites']\n\n    rows = []\n    for site in sites:\n        (x, y, z) = site['xyz']\n        for species in site['species']:\n            rows.append({\n                'symbol': site['label'],\n                'elem': get_elem(species['element']),\n                'x': x, 'y': y, 'z': z,\n                'frac': species['occu'],\n                **site['properties'],\n            })\n\n    frame = Atoms(rows, orient='row')\n    return AtomCell.from_ortho(frame, ortho)\n
    "},{"location":"api/io/mslice/","title":"atomlib.io.mslice","text":"

    IO support for the pyMultislicer XML file format.

    Writes mslice files with the help of a user-supplied template.

    "},{"location":"api/io/mslice/#atomlib.io.mslice.ElementTree","title":"ElementTree module-attribute","text":"
    ElementTree: TypeAlias = _ElementTree\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.Element","title":"Element module-attribute","text":"
    Element: TypeAlias = _Element\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.MSliceFile","title":"MSliceFile module-attribute","text":"
    MSliceFile: TypeAlias = Union[ElementTree, FileOrPath]\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.DEFAULT_TEMPLATE_PATH","title":"DEFAULT_TEMPLATE_PATH module-attribute","text":"
    DEFAULT_TEMPLATE_PATH = (\n    files(\"atomlib.data\") / \"template.mslice\"\n)\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.DEFAULT_TEMPLATE","title":"DEFAULT_TEMPLATE module-attribute","text":"
    DEFAULT_TEMPLATE: Optional[ElementTree] = None\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.default_template","title":"default_template","text":"
    default_template() -> ElementTree\n
    Source code in atomlib/io/mslice.py
    def default_template() -> ElementTree:\n    global DEFAULT_TEMPLATE\n\n    if DEFAULT_TEMPLATE is None:\n        with DEFAULT_TEMPLATE_PATH.open('r') as f:  # type: ignore\n            DEFAULT_TEMPLATE = t.cast(ElementTree, et.parse(f, None))\n\n    return deepcopy(DEFAULT_TEMPLATE)\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.convert_xml_value","title":"convert_xml_value","text":"
    convert_xml_value(val: str, ty: str)\n

    Convert an XML value val to a Python type determined by the XML type name ty.

    Source code in atomlib/io/mslice.py
    def convert_xml_value(val: str, ty: str):\n    \"\"\"Convert an XML value `val` to a Python type determined by the XML type name `ty`.\"\"\"\n    if ty == 'string':\n        ty = 'str'\n    elif ty == 'int16' or ty == 'int32':\n        val = val.split('.')[0]\n        ty = 'int'\n    elif ty == 'bool':\n        return bool(int(val))\n\n    return getattr(builtins, ty)(val)\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.parse_xml_object","title":"parse_xml_object","text":"
    parse_xml_object(obj: Element) -> Dict[str, Any]\n

    Parse the attributes of a passed XML object.

    Source code in atomlib/io/mslice.py
    def parse_xml_object(obj: Element) -> t.Dict[str, t.Any]:\n    \"\"\"Parse the attributes of a passed XML object.\"\"\"\n    params = {}\n    for attr in t.cast(t.Iterator[Element], obj.iter(None)):\n        if attr.tag == 'attribute':\n            params[attr.attrib['name']] = convert_xml_value(attr.text, attr.attrib['type'])\n        elif attr.tag == 'relationship':\n            # todo give this a better API\n            if 'idrefs' in attr.attrib:\n                params[f\"{attr.attrib['name']}ID\"] = attr.attrib['idrefs']\n    return params\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.find_xml_object","title":"find_xml_object","text":"
    find_xml_object(\n    xml: Element, typename: str\n) -> Dict[str, Any]\n

    Find and parse XML objects named typename, flattening them into a single Dict.

    Source code in atomlib/io/mslice.py
    def find_xml_object(xml: Element, typename: str) -> t.Dict[str, t.Any]:\n    \"\"\"Find and parse XML objects named `typename`, flattening them into a single Dict.\"\"\"\n    params = {}\n    for obj in xml.findall(f\".//*[@type='{typename}']\", None):\n        params.update(parse_xml_object(obj))\n    return params\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.find_xml_object_list","title":"find_xml_object_list","text":"
    find_xml_object_list(\n    xml: Element, typename: str\n) -> List[Any]\n

    Find and parse a list of XML objects named typename.

    Source code in atomlib/io/mslice.py
    def find_xml_object_list(xml: Element, typename: str) -> t.List[t.Any]:\n    \"\"\"Find and parse a list of XML objects named `typename`.\"\"\"\n    return [parse_xml_object(obj) for obj in xml.findall(f\".//*[@type='{typename}']\", None)]\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.find_xml_object_dict","title":"find_xml_object_dict","text":"
    find_xml_object_dict(\n    xml: Element, typename: str, key: str = \"id\"\n) -> Dict[str, Any]\n

    Find and parse XML objects named typename, combining them into a dict.

    Source code in atomlib/io/mslice.py
    def find_xml_object_dict(xml: Element, typename: str, key: str = \"id\") -> t.Dict[str, t.Any]:\n    \"\"\"Find and parse XML objects named `typename`, combining them into a dict.\"\"\"\n    return {\n        obj.attrib[key]: parse_xml_object(obj)\n        for obj in xml.findall(f\".//*[@type='{typename}']\", None)\n    }\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.read_mslice","title":"read_mslice","text":"
    read_mslice(path: MSliceFile) -> AtomCell\n
    Source code in atomlib/io/mslice.py
    def read_mslice(path: MSliceFile) -> AtomCell:\n    tree: ElementTree\n    if isinstance(path, ElementTree):\n        tree = path\n    else:\n        with open_file(path, 'r') as temp:\n            tree = et.parse(temp, None)\n\n    xml: Element = tree.getroot()\n\n    structure = find_xml_object(xml, \"STRUCTURE\")\n    structure_atoms = find_xml_object_list(xml, \"STRUCTUREATOM\")\n\n    n_cells = tuple(structure.get(k, 1) for k in ('repeata', 'repeatb', 'repeatc'))\n    cell_size = tuple(structure[k] for k in ('aparam', 'bparam', 'cparam'))\n\n    atoms = Atoms(\n        polars.from_dicts(structure_atoms, schema={\n            'atomicnumber': polars.UInt8,\n            'x': polars.Float64, 'y': polars.Float64, 'z': polars.Float64,\n            'wobble': polars.Float64, 'fracoccupancy': polars.Float64,\n        })\n        .rename({'atomicnumber': 'elem', 'fracoccupancy': 'frac_occupancy'}) \\\n        # 1d sigma -> <u^2>\n        .with_columns((3. * polars.col('wobble')**2).alias('wobble'))\n    )\n    cell = Cell.from_ortho(LinearTransform3D.scale(cell_size), n_cells, [True, True, False])\n\n    return AtomCell(atoms, cell, frame='cell_frac')\n
    "},{"location":"api/io/mslice/#atomlib.io.mslice.write_mslice","title":"write_mslice","text":"
    write_mslice(\n    cell: HasAtomCell,\n    f: BinaryFileOrPath,\n    template: Optional[MSliceFile] = None,\n    *,\n    slice_thickness: Optional[float] = None,\n    scan_points: Optional[ArrayLike] = None,\n    scan_extent: Optional[ArrayLike] = None,\n    noise_sigma: Optional[float] = None,\n    conv_angle: Optional[float] = None,\n    energy: Optional[float] = None,\n    defocus: Optional[float] = None,\n    tilt: Optional[Tuple[float, float]] = None,\n    tds: Optional[bool] = None,\n    n_cells: Optional[ArrayLike] = None\n)\n

    Write a structure to an mslice file. The structure must be orthogonal and aligned with the local coordinate system. It should be periodic in X and Y.

    template may be a file, path, or ElementTree containing an existing mslice file. Its structure will be modified to make the final output. If not specified, a default template will be used.

    Additional options modify simulation properties. If an option is not specified, the template's properties are used.

    Source code in atomlib/io/mslice.py
    def write_mslice(cell: HasAtomCell, f: BinaryFileOrPath, template: t.Optional[MSliceFile] = None, *,\n                 slice_thickness: t.Optional[float] = None,  # angstrom\n                 scan_points: t.Optional[ArrayLike] = None,\n                 scan_extent: t.Optional[ArrayLike] = None,\n                 noise_sigma: t.Optional[float] = None,  # angstrom\n                 conv_angle: t.Optional[float] = None,  # mrad\n                 energy: t.Optional[float] = None,  # keV\n                 defocus: t.Optional[float] = None,  # angstrom\n                 tilt: t.Optional[t.Tuple[float, float]] = None,  # (mrad, mrad)\n                 tds: t.Optional[bool] = None,\n                 n_cells: t.Optional[ArrayLike] = None):\n    \"\"\"\n    Write a structure to an mslice file. The structure must be orthogonal and aligned\n    with the local coordinate system. It should be periodic in X and Y.\n\n    ``template`` may be a file, path, or ElementTree containing an existing mslice file.\n    Its structure will be modified to make the final output. If not specified, a default\n    template will be used.\n\n    Additional options modify simulation properties. If an option is not specified, the\n    template's properties are used.\n    \"\"\"\n    #if not issubclass(type(cell), HasAtomCell):\n    #    raise TypeError(\"mslice format requires an AtomCell.\")\n\n    if not cell.is_orthogonal_in_local():\n        raise ValueError(\"mslice requires an orthogonal AtomCell.\")\n\n    if not numpy.all(cell.pbc[:2]):\n        warn(\"AtomCell may not be periodic\", UserWarning, stacklevel=2)\n\n    box_size = cell._box_size_in_local()\n\n    # get atoms in local frame (which we verified aligns with the cell's axes)\n    # then scale into fractional coordinates\n    atoms = cell.get_atoms('linear') \\\n        .transform(AffineTransform3D.scale(1/box_size)) \\\n        .with_wobble().with_occupancy()\n\n    out: ElementTree\n    if template is None:\n        out = default_template()\n    elif not isinstance(template, ElementTree):\n        with open_file(template, 'r') as temp:\n            out = et.parse(temp, None)\n    else:\n        out = deepcopy(template)\n\n    # TODO clean up this code\n    db: t.Optional[Element] = out.getroot() if out.getroot().tag == 'database' else out.find(\"./database\", None)\n    if db is None:\n        raise ValueError(\"Couldn't find 'database' tag in template.\")\n\n    struct = db.find(\".//object[@type='STRUCTURE']\", None)\n    if struct is None:\n        raise ValueError(\"Couldn't find STRUCTURE object in template.\")\n\n    params = db.find(\".//object[@type='SIMPARAMETERS']\", None)\n    if params is None:\n        raise ValueError(\"Couldn't find SIMPARAMETERS object in template.\")\n\n    microscope = db.find(\".//object[@type='MICROSCOPE']\", None)\n    if microscope is None:\n        raise ValueError(\"Couldn't find MICROSCOPE object in template.\")\n\n    scan = db.find(\".//object[@type='SCAN']\", None)\n    aberrations = db.findall(\".//object[@type='ABERRATION']\", None)\n\n    def set_attr(struct: Element, name: str, type: str, val: str):\n        node = t.cast(t.Optional[Element], struct.find(f\".//attribute[@name='{name}']\", None))\n        if node is None:\n            node = t.cast(Element, et.Element('attribute', dict(name=name, type=type), None))\n            struct.append(node)\n        else:\n            node.attrib['type'] = type\n        node.text = val  # type: ignore\n\n    def parse_xml_object(obj: Element) -> t.Dict[str, t.Any]:\n        \"\"\"Parse the attributes of a passed XML object.\"\"\"\n        params = {}\n        for attr in obj.iterchildren(None):\n            if attr.tag == 'attribute':\n                params[attr.attrib['name']] = convert_xml_value(attr.text, attr.attrib['type'])\n            elif attr.tag == 'relationship':\n                # todo give this a better API\n                if 'idrefs' in attr.attrib:\n                    params[f\"{attr.attrib['name']}ID\"] = attr.attrib['idrefs']\n        return params\n\n    # TODO how to store atoms in unexploded form\n    (n_a, n_b, n_c) = map(str, (1, 1, 1) if n_cells is None else numpy.asarray(n_cells).astype(int))\n    set_attr(struct, 'repeata', 'int16', n_a)\n    set_attr(struct, 'repeatb', 'int16', n_b)\n    set_attr(struct, 'repeatc', 'int16', n_c)\n\n    (a, b, c) = map(lambda v: f\"{v:.8g}\", box_size)\n    set_attr(struct, 'aparam', 'float', a)\n    set_attr(struct, 'bparam', 'float', b)\n    set_attr(struct, 'cparam', 'float', c)\n\n    if tilt is not None:\n        (tiltx, tilty) = tilt\n        set_attr(struct, 'tiltx', 'float', f\"{tiltx:.4g}\")\n        set_attr(struct, 'tilty', 'float', f\"{tilty:.4g}\")\n\n    if slice_thickness is not None:\n        set_attr(params, 'slicethickness', 'float', f\"{float(slice_thickness):.8g}\")\n    if tds is not None:\n        set_attr(params, 'includetds', 'bool', str(int(bool(tds))))\n    if conv_angle is not None:\n        set_attr(microscope, 'aperture', 'float', f\"{float(conv_angle):.8g}\")\n    if energy is not None:\n        set_attr(microscope, 'kv', 'float', f\"{float(energy):.8g}\")\n    if noise_sigma is not None:\n        if scan is None:\n            raise ValueError(\"New scan specification required for 'noise_sigma'.\")\n        set_attr(scan, 'noise_sigma', 'float', f\"{float(noise_sigma):.8g}\")\n\n    if defocus is not None:\n        for aberration in aberrations:\n            obj = parse_xml_object(aberration)\n            if obj['n'] == 1 and obj['m'] == 0:\n                set_attr(aberration, 'cnma', 'float', f\"{float(defocus):.8g}\")  # A, + is over\n                set_attr(aberration, 'cnmb', 'float', \"0.0\")\n                break\n        else:\n            raise ValueError(\"Couldn't find defocus aberration to modify.\")\n\n    if scan_points is not None:\n        (nx, ny) = numpy.broadcast_to(scan_points, 2,).astype(int)\n        if scan is not None:\n            set_attr(scan, 'nx', 'int16', str(nx))\n            set_attr(scan, 'ny', 'int16', str(ny))\n        else:\n            set_attr(params, 'numscanx', 'int16', str(nx))\n            set_attr(params, 'numscany', 'int16', str(ny))\n\n    if scan_extent is not None:\n        scan_extent = numpy.asarray(scan_extent, dtype=float)\n        try:\n            if scan_extent.ndim < 2:\n                if not scan_extent.shape == (4,):\n                    scan_extent = numpy.broadcast_to(scan_extent, (2,))\n                    scan_extent = numpy.stack(((0., 0.), scan_extent), axis=-1)\n            else:\n                scan_extent = numpy.broadcast_to(scan_extent, (2, 2))\n        except ValueError as e:\n            raise ValueError(f\"Invalid scan_extent '{scan_extent}'. Expected an array of shape (2,), (4,), or (2, 2).\") from e\n\n        if scan is not None:\n            names = ('x_i', 'x_f', 'y_i', 'y_f')\n            elem = scan\n        else:\n            names = ('intx', 'finx', 'inty', 'finy')\n            elem = params\n\n        for (name, val) in zip(names, scan_extent.ravel()):\n            set_attr(elem, name, 'float', f\"{float(val):.8g}\")\n\n    # remove existing atoms\n    for elem in db.findall(\"./object[@type='STRUCTUREATOM']\", None):\n        db.remove(elem)\n\n    # <u^2> -> 1d sigma\n    atoms = atoms.with_wobble((polars.col('wobble') / 3.).sqrt())\n    rows = atoms.select(('elem', 'coords', 'wobble', 'frac_occupancy')).rows()\n    for (i, (elem, (x, y, z), wobble, frac_occupancy)) in enumerate(rows):\n        e = _atom_elem(i, elem, x, y, z, wobble, frac_occupancy)\n        db.append(e)\n\n    et.indent(db, space=\"    \", level=0)  # type: ignore\n\n    with open_file_binary(f, 'w') as f:\n        doctype = b\"\"\"<!DOCTYPE database SYSTEM \"file:///System/Library/DTDs/CoreData.dtd\">\\n\"\"\"\n        out.write(f, encoding='UTF-8', xml_declaration=True, standalone=True, doctype=doctype)  # type: ignore\n        f.write(b'\\n')\n
    "},{"location":"api/io/qe/","title":"atomlib.io.qe","text":"

    IO for Quantum Espresso pw.x files.

    "},{"location":"api/io/qe/#atomlib.io.qe.write_qe","title":"write_qe","text":"
    write_qe(\n    atomcell: HasAtomCell,\n    f: FileOrPath,\n    pseudo: Optional[Mapping[str, str]] = None,\n)\n

    Write a structure to a Quantum Espresso pw.x file.

    PARAMETER DESCRIPTION atomcell

    Structure to write

    TYPE: HasAtomCell

    f

    File or path to write to

    TYPE: FileOrPath

    pseudo

    Mapping from atom symbol

    TYPE: Optional[Mapping[str, str]] DEFAULT: None

    Source code in atomlib/io/qe.py
    def write_qe(atomcell: HasAtomCell, f: FileOrPath, pseudo: t.Optional[t.Mapping[str, str]] = None):\n    \"\"\"\n    Write a structure to a Quantum Espresso pw.x file.\n\n    Args:\n      atomcell: Structure to write\n      f: File or path to write to\n      pseudo: Mapping from atom symbol\n    \"\"\"\n    if not isinstance(atomcell, HasAtomCell):\n        raise TypeError(\"'qe' format requires an AtomCell.\")\n\n    atoms = atomcell.wrap().get_atoms('cell_box').with_mass()\n\n    types = atoms.select(('symbol', 'mass')).unique(subset='symbol').sort('mass')\n    if pseudo is not None:\n        types = types.with_columns(_get_symbol_mapping(types, pseudo, ty=polars.Utf8).alias('pot'))\n    else:\n        types = types.with_columns((polars.col('symbol') + polars.lit('.UPF')).alias('pot'))\n        #types = types.with_columns(polars.col('symbol').apply(lambda sym: f\"{sym}.UPF\").alias('pot'))\n\n    with open_file(f, 'w') as f:\n        print(f\"\"\"\\\n&SYSTEM\n  ibrav=0,\n  nat={len(atoms)},\n  ntyp={len(types)}\n/\"\"\", file=f)\n\n        ortho = atomcell.get_transform('local', 'cell_box').to_linear().inner\n        print(\"\\nCELL_PARAMETERS angstrom\", file=f)\n        for row in ortho.T:\n            print(f\"  {row[0]:12.8f} {row[1]:12.8f} {row[2]:12.8f}\", file=f)\n\n        print(\"\\nATOMIC_SPECIES\", file=f)\n        for (symbol, mass, pot) in types.select(('symbol', 'mass', 'pot')).rows():\n            print(f\"{symbol:>4} {mass:10.3f}  {pot}\", file=f)\n\n        print(\"\\nATOMIC_POSITIONS crystal\", file=f)\n        for (symbol, (x, y, z)) in atoms.select(('symbol', 'coords')).rows():\n            print(f\"{symbol:>4} {x:.8f} {y:.8f} {z:.8f}\", file=f)\n\n        print(file=f)  # allows for easy concatenation\n
    "},{"location":"api/io/xsf/","title":"atomlib.io.xsf","text":"

    IO for XCrySDen's XSF format, specified here.

    "},{"location":"api/io/xsf/#atomlib.io.xsf.Periodicity","title":"Periodicity module-attribute","text":"
    Periodicity: TypeAlias = Literal[\n    \"crystal\", \"slab\", \"polymer\", \"molecule\"\n]\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF","title":"XSF dataclass","text":"Source code in atomlib/io/xsf.py
    @dataclass\nclass XSF:\n    periodicity: Periodicity = 'crystal'\n    primitive_cell: t.Optional[LinearTransform3D] = None\n    conventional_cell: t.Optional[LinearTransform3D] = None\n\n    prim_coords: t.Optional[polars.DataFrame] = None\n    conv_coords: t.Optional[polars.DataFrame] = None\n    atoms: t.Optional[polars.DataFrame] = None\n\n    def get_atoms(self) -> polars.DataFrame:\n        if self.prim_coords is not None:\n            return self.prim_coords\n        if self.atoms is not None:\n            return self.atoms\n        if self.conv_coords is not None:\n            raise NotImplementedError()  # TODO untransform conv_coords by conventional_cell?\n        raise ValueError(\"No coordinates specified in XSF file.\")\n\n    def get_pbc(self) -> NDArray[numpy.bool_]:\n        return _periodicity_to_pbc(self.periodicity)\n\n    @staticmethod\n    def from_cell(cell: HasAtomCell) -> XSF:\n        ortho = cell.get_transform('local', 'cell_box').to_linear()\n        return XSF(\n            primitive_cell=ortho,\n            conventional_cell=ortho,\n            prim_coords=cell.get_atoms('linear').inner,\n            periodicity=_pbc_to_periodicity(cell.pbc)\n        )\n\n    @staticmethod\n    def from_atoms(atoms: HasAtoms) -> XSF:\n        return XSF(\n            periodicity='molecule',\n            atoms=atoms.get_atoms('local').inner\n        )\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> XSF:\n        logging.info(f\"Loading XSF {file.name if hasattr(file, 'name') else file!r}...\")  # type: ignore\n        with open_file(file) as f:\n            return XSFParser(f).parse()\n\n    def __post_init__(self):\n        if self.prim_coords is None and self.conv_coords is None and self.atoms is None:\n            raise ValueError(\"Error: No coordinates are specified (atoms, primitive, or conventional).\")\n\n        if self.prim_coords is not None and self.conv_coords is not None:\n            logging.warning(\"Warning: Both 'primcoord' and 'convcoord' are specified. 'convcoord' will be ignored.\")\n        elif self.conv_coords is not None and self.conventional_cell is None:\n            raise ValueError(\"If 'convcoord' is specified, 'convvec' must be specified as well.\")\n\n        if self.periodicity == 'molecule':\n            if self.atoms is None:\n                raise ValueError(\"'atoms' must be specified for molecules.\")\n\n    def write(self, path: FileOrPath):\n        with open_file(path, 'w') as f:\n            print(self.periodicity.upper(), file=f)\n            if self.primitive_cell is not None:\n                print('PRIMVEC', file=f)\n                self._write_cell(f, self.primitive_cell)\n            if self.conventional_cell is not None:\n                print('CONVVEC', file=f)\n                self._write_cell(f, self.conventional_cell)\n            print(file=f)\n\n            if self.prim_coords is not None:\n                print(\"PRIMCOORD\", file=f)\n                print(f\"{len(self.prim_coords)} 1\", file=f)\n                self._write_coords(f, self.prim_coords)\n            if self.conv_coords is not None:\n                print(\"CONVCOORD\", file=f)\n                print(f\"{len(self.conv_coords)} 1\", file=f)\n                self._write_coords(f, self.conv_coords)\n            if self.atoms is not None:\n                print(\"ATOMS\", file=f)\n                self._write_coords(f, self.atoms)\n\n    def _write_cell(self, f: TextIOBase, cell: LinearTransform3D):\n        for row in cell.inner.T:\n            for val in row:\n                f.write(f\"{val:12.7f}\")\n            f.write('\\n')\n\n    def _write_coords(self, f: TextIOBase, coords: polars.DataFrame):\n        for (elem, [x, y, z]) in coords.select(['elem', 'coords']).rows():\n            print(f\"{elem:2d} {x:11.6f} {y:11.6f} {z:11.6f}\", file=f)\n        print(file=f)\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.periodicity","title":"periodicity class-attribute instance-attribute","text":"
    periodicity: Periodicity = 'crystal'\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.primitive_cell","title":"primitive_cell class-attribute instance-attribute","text":"
    primitive_cell: Optional[LinearTransform3D] = None\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.conventional_cell","title":"conventional_cell class-attribute instance-attribute","text":"
    conventional_cell: Optional[LinearTransform3D] = None\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.prim_coords","title":"prim_coords class-attribute instance-attribute","text":"
    prim_coords: Optional[DataFrame] = None\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.conv_coords","title":"conv_coords class-attribute instance-attribute","text":"
    conv_coords: Optional[DataFrame] = None\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.atoms","title":"atoms class-attribute instance-attribute","text":"
    atoms: Optional[DataFrame] = None\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.get_atoms","title":"get_atoms","text":"
    get_atoms() -> DataFrame\n
    Source code in atomlib/io/xsf.py
    def get_atoms(self) -> polars.DataFrame:\n    if self.prim_coords is not None:\n        return self.prim_coords\n    if self.atoms is not None:\n        return self.atoms\n    if self.conv_coords is not None:\n        raise NotImplementedError()  # TODO untransform conv_coords by conventional_cell?\n    raise ValueError(\"No coordinates specified in XSF file.\")\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.get_pbc","title":"get_pbc","text":"
    get_pbc() -> NDArray[bool_]\n
    Source code in atomlib/io/xsf.py
    def get_pbc(self) -> NDArray[numpy.bool_]:\n    return _periodicity_to_pbc(self.periodicity)\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.from_cell","title":"from_cell staticmethod","text":"
    from_cell(cell: HasAtomCell) -> XSF\n
    Source code in atomlib/io/xsf.py
    @staticmethod\ndef from_cell(cell: HasAtomCell) -> XSF:\n    ortho = cell.get_transform('local', 'cell_box').to_linear()\n    return XSF(\n        primitive_cell=ortho,\n        conventional_cell=ortho,\n        prim_coords=cell.get_atoms('linear').inner,\n        periodicity=_pbc_to_periodicity(cell.pbc)\n    )\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.from_atoms","title":"from_atoms staticmethod","text":"
    from_atoms(atoms: HasAtoms) -> XSF\n
    Source code in atomlib/io/xsf.py
    @staticmethod\ndef from_atoms(atoms: HasAtoms) -> XSF:\n    return XSF(\n        periodicity='molecule',\n        atoms=atoms.get_atoms('local').inner\n    )\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> XSF\n
    Source code in atomlib/io/xsf.py
    @staticmethod\ndef from_file(file: FileOrPath) -> XSF:\n    logging.info(f\"Loading XSF {file.name if hasattr(file, 'name') else file!r}...\")  # type: ignore\n    with open_file(file) as f:\n        return XSFParser(f).parse()\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSF.write","title":"write","text":"
    write(path: FileOrPath)\n
    Source code in atomlib/io/xsf.py
    def write(self, path: FileOrPath):\n    with open_file(path, 'w') as f:\n        print(self.periodicity.upper(), file=f)\n        if self.primitive_cell is not None:\n            print('PRIMVEC', file=f)\n            self._write_cell(f, self.primitive_cell)\n        if self.conventional_cell is not None:\n            print('CONVVEC', file=f)\n            self._write_cell(f, self.conventional_cell)\n        print(file=f)\n\n        if self.prim_coords is not None:\n            print(\"PRIMCOORD\", file=f)\n            print(f\"{len(self.prim_coords)} 1\", file=f)\n            self._write_coords(f, self.prim_coords)\n        if self.conv_coords is not None:\n            print(\"CONVCOORD\", file=f)\n            print(f\"{len(self.conv_coords)} 1\", file=f)\n            self._write_coords(f, self.conv_coords)\n        if self.atoms is not None:\n            print(\"ATOMS\", file=f)\n            self._write_coords(f, self.atoms)\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSFParser","title":"XSFParser","text":"Source code in atomlib/io/xsf.py
    class XSFParser:\n    def __init__(self, file: TextIOBase):\n        self._file: TextIOBase = file\n        self._peek_line: t.Optional[str] = None\n        self.lineno = 0\n\n    def skip_line(self, line: t.Optional[str]) -> bool:\n        return line is None or line.isspace() or line.lstrip().startswith('#')\n\n    def peek_line(self) -> t.Optional[str]:\n        try:\n            while self.skip_line(self._peek_line):\n                self._peek_line = next(self._file)\n                self.lineno += 1\n            return self._peek_line\n        except StopIteration:\n            return None\n\n    def next_line(self) -> t.Optional[str]:\n        line = self.peek_line()\n        self._peek_line = None\n        return line\n\n    def parse_atoms(self, expected_length: t.Optional[int] = None) -> polars.DataFrame:\n        zs = []\n        coords = []\n        words = None\n\n        while (line := self.peek_line()):\n            words = line.split()\n            if len(words) == 0:\n                continue\n            if words[0].isalpha():\n                break\n            self.next_line()\n            try:\n                z = int(words[0])\n                if z < 0 or z > 118:\n                    raise ValueError()\n            except (ValueError, TypeError):\n                raise ValueError(f\"Invalid atomic number '{words[0]}'\") from None\n\n            try:\n                coords.append(numpy.array(list(map(float, words[1:]))))\n                zs.append(z)\n            except (ValueError, TypeError):\n                raise ValueError(f\"Invalid atomic coordinates '{' '.join(words[1:])}'\") from None\n\n        if expected_length is not None:\n            if not expected_length == len(zs):\n                logging.warning(f\"Warning: List length {len(zs)} doesn't match declared length {expected_length}\")\n        elif len(zs) == 0:\n            raise ValueError(f\"Expected atom list after keyword 'ATOMS'. Got '{line or 'EOF'}' instead.\")\n\n        if len(zs) == 0:\n            return polars.DataFrame({}, schema=['elem', 'x', 'y', 'z'])  # type: ignore\n\n        coord_lens = list(map(len, coords))\n        if not all(coord_len == coord_lens[0] for coord_len in coord_lens[1:]):\n            raise ValueError(\"Mismatched atom dimensions.\")\n        if coord_lens[0] < 3:\n            raise ValueError(\"Expected at least 3 coordinates per atom.\")\n\n        coords = numpy.stack(coords, axis=0)[:, :3]\n        (x, y, z) = map(lambda a: a[:, 0], numpy.split(coords, 3, axis=1))\n\n        return polars.DataFrame({'elem': zs, 'x': x, 'y': y, 'z': z})\n\n    def parse_coords(self) -> polars.DataFrame:\n        line = self.next_line()\n        if line is None:\n            raise ValueError(\"Unexpected EOF before atom list\")\n        words = line.split()\n        try:\n            if not len(words) == 2:\n                raise ValueError()\n            (n, _) = map(int, words)\n        except (ValueError, TypeError):\n            raise ValueError(f\"Invalid atom list length: {line}\") from None\n\n        return self.parse_atoms(n)\n\n    def parse_lattice(self) -> LinearTransform3D:\n        rows = []\n        for _ in range(3):\n            line = self.next_line()\n            if line is None:\n                raise ValueError(\"Unexpected EOF in vector section.\")\n            words = line.split()\n            try:\n                if not len(words) == 3:\n                    raise ValueError()\n                row = numpy.array(list(map(float, words)))\n                rows.append(row)\n            except (ValueError, TypeError):\n                raise ValueError(f\"Invalid lattice vector: {line}\") from None\n\n        matrix = numpy.stack(rows, axis=-1)\n        return LinearTransform3D(matrix)\n\n    def eat_sandwich(self, keyword: str):\n        begin_keyword = 'begin_' + keyword\n        end_keyword = 'end_' + keyword\n        lineno = self.lineno\n\n        while (line := self.next_line()):\n            keyword = line.lstrip().split(maxsplit=1)[0].lower()\n            if keyword.lower() == begin_keyword:\n                # recurse to inner (identical) section\n                self.eat_sandwich(keyword)\n                continue\n            if keyword.lower() == end_keyword:\n                break\n        else:\n            raise ValueError(f\"Unclosed section '{keyword}' opened at line {lineno}\")\n\n    def parse(self) -> XSF:\n        data: t.Dict[str, t.Any] = {}\n        periodicity: Periodicity = 'molecule'\n\n        while (line := self.next_line()):\n            keyword = line.lstrip().split(maxsplit=1)[0].lower()\n            logging.debug(f\"Parsing keyword {keyword}\")\n\n            if keyword == 'animsteps':\n                raise ValueError(\"Animated XSF files are not supported.\")\n            elif keyword == 'atoms':\n                data['atoms'] = self.parse_atoms()\n            elif keyword in ('primcoord', 'convcoord'):\n                data[keyword] = self.parse_coords()\n            elif keyword in ('primvec', 'convvec'):\n                data[keyword] = self.parse_lattice()\n            elif keyword in ('crystal', 'slab', 'polymer', 'molecule'):\n                periodicity = keyword\n            elif keyword.startswith('begin_'):\n                self.eat_sandwich(keyword.removeprefix('begin_'))\n            elif keyword.startswith('end_'):\n                raise ValueError(f\"Unopened section close keyword '{keyword}'\")\n            else:\n                raise ValueError(f\"Unexpected keyword '{keyword.upper()}'.\")\n\n        if len(data) == 0:\n            raise ValueError(\"Unexpected EOF while parsing XSF file.\")\n\n        # most validation is performed in XSF\n        return XSF(\n            periodicity, atoms=data.get('atoms'),\n            prim_coords=data.get('primcoord'),\n            conv_coords=data.get('convcoord'),\n            primitive_cell=data.get('primvec'),\n            conventional_cell=data.get('convvec'),\n        )\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSFParser.lineno","title":"lineno instance-attribute","text":"
    lineno = 0\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSFParser.skip_line","title":"skip_line","text":"
    skip_line(line: Optional[str]) -> bool\n
    Source code in atomlib/io/xsf.py
    def skip_line(self, line: t.Optional[str]) -> bool:\n    return line is None or line.isspace() or line.lstrip().startswith('#')\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSFParser.peek_line","title":"peek_line","text":"
    peek_line() -> Optional[str]\n
    Source code in atomlib/io/xsf.py
    def peek_line(self) -> t.Optional[str]:\n    try:\n        while self.skip_line(self._peek_line):\n            self._peek_line = next(self._file)\n            self.lineno += 1\n        return self._peek_line\n    except StopIteration:\n        return None\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSFParser.next_line","title":"next_line","text":"
    next_line() -> Optional[str]\n
    Source code in atomlib/io/xsf.py
    def next_line(self) -> t.Optional[str]:\n    line = self.peek_line()\n    self._peek_line = None\n    return line\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSFParser.parse_atoms","title":"parse_atoms","text":"
    parse_atoms(\n    expected_length: Optional[int] = None,\n) -> DataFrame\n
    Source code in atomlib/io/xsf.py
    def parse_atoms(self, expected_length: t.Optional[int] = None) -> polars.DataFrame:\n    zs = []\n    coords = []\n    words = None\n\n    while (line := self.peek_line()):\n        words = line.split()\n        if len(words) == 0:\n            continue\n        if words[0].isalpha():\n            break\n        self.next_line()\n        try:\n            z = int(words[0])\n            if z < 0 or z > 118:\n                raise ValueError()\n        except (ValueError, TypeError):\n            raise ValueError(f\"Invalid atomic number '{words[0]}'\") from None\n\n        try:\n            coords.append(numpy.array(list(map(float, words[1:]))))\n            zs.append(z)\n        except (ValueError, TypeError):\n            raise ValueError(f\"Invalid atomic coordinates '{' '.join(words[1:])}'\") from None\n\n    if expected_length is not None:\n        if not expected_length == len(zs):\n            logging.warning(f\"Warning: List length {len(zs)} doesn't match declared length {expected_length}\")\n    elif len(zs) == 0:\n        raise ValueError(f\"Expected atom list after keyword 'ATOMS'. Got '{line or 'EOF'}' instead.\")\n\n    if len(zs) == 0:\n        return polars.DataFrame({}, schema=['elem', 'x', 'y', 'z'])  # type: ignore\n\n    coord_lens = list(map(len, coords))\n    if not all(coord_len == coord_lens[0] for coord_len in coord_lens[1:]):\n        raise ValueError(\"Mismatched atom dimensions.\")\n    if coord_lens[0] < 3:\n        raise ValueError(\"Expected at least 3 coordinates per atom.\")\n\n    coords = numpy.stack(coords, axis=0)[:, :3]\n    (x, y, z) = map(lambda a: a[:, 0], numpy.split(coords, 3, axis=1))\n\n    return polars.DataFrame({'elem': zs, 'x': x, 'y': y, 'z': z})\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSFParser.parse_coords","title":"parse_coords","text":"
    parse_coords() -> DataFrame\n
    Source code in atomlib/io/xsf.py
    def parse_coords(self) -> polars.DataFrame:\n    line = self.next_line()\n    if line is None:\n        raise ValueError(\"Unexpected EOF before atom list\")\n    words = line.split()\n    try:\n        if not len(words) == 2:\n            raise ValueError()\n        (n, _) = map(int, words)\n    except (ValueError, TypeError):\n        raise ValueError(f\"Invalid atom list length: {line}\") from None\n\n    return self.parse_atoms(n)\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSFParser.parse_lattice","title":"parse_lattice","text":"
    parse_lattice() -> LinearTransform3D\n
    Source code in atomlib/io/xsf.py
    def parse_lattice(self) -> LinearTransform3D:\n    rows = []\n    for _ in range(3):\n        line = self.next_line()\n        if line is None:\n            raise ValueError(\"Unexpected EOF in vector section.\")\n        words = line.split()\n        try:\n            if not len(words) == 3:\n                raise ValueError()\n            row = numpy.array(list(map(float, words)))\n            rows.append(row)\n        except (ValueError, TypeError):\n            raise ValueError(f\"Invalid lattice vector: {line}\") from None\n\n    matrix = numpy.stack(rows, axis=-1)\n    return LinearTransform3D(matrix)\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSFParser.eat_sandwich","title":"eat_sandwich","text":"
    eat_sandwich(keyword: str)\n
    Source code in atomlib/io/xsf.py
    def eat_sandwich(self, keyword: str):\n    begin_keyword = 'begin_' + keyword\n    end_keyword = 'end_' + keyword\n    lineno = self.lineno\n\n    while (line := self.next_line()):\n        keyword = line.lstrip().split(maxsplit=1)[0].lower()\n        if keyword.lower() == begin_keyword:\n            # recurse to inner (identical) section\n            self.eat_sandwich(keyword)\n            continue\n        if keyword.lower() == end_keyword:\n            break\n    else:\n        raise ValueError(f\"Unclosed section '{keyword}' opened at line {lineno}\")\n
    "},{"location":"api/io/xsf/#atomlib.io.xsf.XSFParser.parse","title":"parse","text":"
    parse() -> XSF\n
    Source code in atomlib/io/xsf.py
    def parse(self) -> XSF:\n    data: t.Dict[str, t.Any] = {}\n    periodicity: Periodicity = 'molecule'\n\n    while (line := self.next_line()):\n        keyword = line.lstrip().split(maxsplit=1)[0].lower()\n        logging.debug(f\"Parsing keyword {keyword}\")\n\n        if keyword == 'animsteps':\n            raise ValueError(\"Animated XSF files are not supported.\")\n        elif keyword == 'atoms':\n            data['atoms'] = self.parse_atoms()\n        elif keyword in ('primcoord', 'convcoord'):\n            data[keyword] = self.parse_coords()\n        elif keyword in ('primvec', 'convvec'):\n            data[keyword] = self.parse_lattice()\n        elif keyword in ('crystal', 'slab', 'polymer', 'molecule'):\n            periodicity = keyword\n        elif keyword.startswith('begin_'):\n            self.eat_sandwich(keyword.removeprefix('begin_'))\n        elif keyword.startswith('end_'):\n            raise ValueError(f\"Unopened section close keyword '{keyword}'\")\n        else:\n            raise ValueError(f\"Unexpected keyword '{keyword.upper()}'.\")\n\n    if len(data) == 0:\n        raise ValueError(\"Unexpected EOF while parsing XSF file.\")\n\n    # most validation is performed in XSF\n    return XSF(\n        periodicity, atoms=data.get('atoms'),\n        prim_coords=data.get('primcoord'),\n        conv_coords=data.get('convcoord'),\n        primitive_cell=data.get('primvec'),\n        conventional_cell=data.get('convvec'),\n    )\n
    "},{"location":"api/io/xyz/","title":"atomlib.io.xyz","text":"

    IO for the informal XYZ file format.

    Supports the extended XYZ format, described here.

    "},{"location":"api/io/xyz/#atomlib.io.xyz.XYZFormat","title":"XYZFormat module-attribute","text":"
    XYZFormat: TypeAlias = Literal['xyz', 'exyz']\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.XYZ","title":"XYZ dataclass","text":"Source code in atomlib/io/xyz.py
    @dataclass\nclass XYZ:\n    atoms: polars.DataFrame\n    comment: t.Optional[str] = None\n    params: t.Dict[str, str] = field(default_factory=dict)\n\n    @staticmethod\n    def from_atoms(atoms: HasAtoms) -> XYZ:\n        params = {}\n        if isinstance(atoms, HasAtomCell):\n            coords = atoms.get_cell().to_ortho().to_linear().inner.ravel()\n            lattice_str = \" \".join((f\"{c:.8f}\" for c in coords))\n            params['Lattice'] = lattice_str\n\n            pbc_str = \" \".join(str(int(v)) for v in atoms.get_cell().pbc)\n            params['pbc'] = pbc_str\n\n        return XYZ(\n            atoms.get_atoms('local')._get_frame(),\n            params=params\n        )\n\n    @staticmethod\n    def from_file(file: FileOrPath) -> XYZ:\n        logging.info(f\"Loading XYZ {file.name if hasattr(file, 'name') else file!r}...\")  # type: ignore\n\n        with open_file(file, 'r') as f:\n            try:\n                # TODO be more gracious about whitespace here\n                length = int(f.readline())\n            except ValueError:\n                raise ValueError(\"Error parsing XYZ file: Invalid length\") from None\n            except IOError as e:\n                raise IOError(f\"Error parsing XYZ file: {e}\") from None\n\n            comment = f.readline().rstrip('\\n')\n            # TODO handle if there's not a gap here\n\n            try:\n                params = ExtXYZParser(comment).parse()\n            except ValueError:\n                params = None\n\n            schema = _get_columns_from_params(params)\n\n            df = parse_whitespace_separated(f, schema, start_line=1)\n\n            # map atomic numbers -> symbols (on columns which are Int8)\n            df = df.with_columns(\n                get_sym(df.select(polars.col('symbol').cast(polars.Int8, strict=False)).to_series())\n                    .fill_null(df['symbol']).alias('symbol')\n            )\n            # ensure all symbols are recognizable (this will raise ValueError if not)\n            get_elem(df['symbol'])\n\n            if length < len(df):\n                warnings.warn(f\"Warning: truncating structure of length {len(df)} \"\n                            f\"to match declared length of {length}\")\n                df = df[:length]\n            elif length > len(df):\n                warnings.warn(f\"Warning: structure length {len(df)} doesn't match \"\n                            f\"declared length {length}.\\nData could be corrupted.\")\n\n            try:\n                params = ExtXYZParser(comment).parse()\n                return XYZ(df, comment, params)\n            except ValueError:\n                pass\n\n            return XYZ(df, comment)\n\n    def write(self, file: FileOrPath, fmt: XYZFormat = 'exyz'):\n        with open_file(file, 'w', newline='\\r\\n') as f:\n\n            f.write(f\"{len(self.atoms)}\\n\")\n            if len(self.params) > 0 and fmt == 'exyz':\n                f.write(\" \".join(_param_strings(self.params)))\n            else:\n                f.write(self.comment or \"\")\n            f.write(\"\\n\")\n\n            # not my best work\n            col_space = (3, 12, 12, 12)\n            f.writelines(\n                \"\".join(\n                    f\"{val:< {space}.8f}\" if isinstance(val, float) else f\"{val:<{space}}\" for (val, space) in zip(_flatten(row), col_space)\n                ).strip() + '\\n' for row in self.atoms.select(('symbol', 'coords')).rows()\n            )\n\n    def cell_matrix(self) -> t.Optional[NDArray[numpy.float64]]:\n        if (s := self.params.get('Lattice')) is None:\n            return None\n\n        try:\n            items = list(map(float, s.split()))\n            if not len(items) == 9:\n                raise ValueError(\"Invalid length\")\n            return numpy.array(items, dtype=numpy.float64).reshape((3, 3)).T\n        except ValueError:\n            warnings.warn(f\"Warning: Invalid format for key 'Lattice=\\\"{s}\\\"'\")\n        return None\n\n    def pbc(self) -> t.Optional[NDArray[numpy.bool_]]:\n        if (s := self.params.get('pbc')) is None:\n            return None\n\n        val_map = {'0': False, 'f': False, '1': True, 't': True}\n        try:\n            items = [val_map[v.lower()] for v in s.split()]\n            if not len(items) == 3:\n                raise ValueError(\"Invalid length\")\n            return numpy.array(items, dtype=numpy.bool_)\n        except ValueError:\n            warnings.warn(f\"Warning: Invalid format for key 'pbc=\\\"{s}\\\"'\")\n        return None\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.XYZ.atoms","title":"atoms instance-attribute","text":"
    atoms: DataFrame\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.XYZ.comment","title":"comment class-attribute instance-attribute","text":"
    comment: Optional[str] = None\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.XYZ.params","title":"params class-attribute instance-attribute","text":"
    params: Dict[str, str] = field(default_factory=dict)\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.XYZ.from_atoms","title":"from_atoms staticmethod","text":"
    from_atoms(atoms: HasAtoms) -> XYZ\n
    Source code in atomlib/io/xyz.py
    @staticmethod\ndef from_atoms(atoms: HasAtoms) -> XYZ:\n    params = {}\n    if isinstance(atoms, HasAtomCell):\n        coords = atoms.get_cell().to_ortho().to_linear().inner.ravel()\n        lattice_str = \" \".join((f\"{c:.8f}\" for c in coords))\n        params['Lattice'] = lattice_str\n\n        pbc_str = \" \".join(str(int(v)) for v in atoms.get_cell().pbc)\n        params['pbc'] = pbc_str\n\n    return XYZ(\n        atoms.get_atoms('local')._get_frame(),\n        params=params\n    )\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.XYZ.from_file","title":"from_file staticmethod","text":"
    from_file(file: FileOrPath) -> XYZ\n
    Source code in atomlib/io/xyz.py
    @staticmethod\ndef from_file(file: FileOrPath) -> XYZ:\n    logging.info(f\"Loading XYZ {file.name if hasattr(file, 'name') else file!r}...\")  # type: ignore\n\n    with open_file(file, 'r') as f:\n        try:\n            # TODO be more gracious about whitespace here\n            length = int(f.readline())\n        except ValueError:\n            raise ValueError(\"Error parsing XYZ file: Invalid length\") from None\n        except IOError as e:\n            raise IOError(f\"Error parsing XYZ file: {e}\") from None\n\n        comment = f.readline().rstrip('\\n')\n        # TODO handle if there's not a gap here\n\n        try:\n            params = ExtXYZParser(comment).parse()\n        except ValueError:\n            params = None\n\n        schema = _get_columns_from_params(params)\n\n        df = parse_whitespace_separated(f, schema, start_line=1)\n\n        # map atomic numbers -> symbols (on columns which are Int8)\n        df = df.with_columns(\n            get_sym(df.select(polars.col('symbol').cast(polars.Int8, strict=False)).to_series())\n                .fill_null(df['symbol']).alias('symbol')\n        )\n        # ensure all symbols are recognizable (this will raise ValueError if not)\n        get_elem(df['symbol'])\n\n        if length < len(df):\n            warnings.warn(f\"Warning: truncating structure of length {len(df)} \"\n                        f\"to match declared length of {length}\")\n            df = df[:length]\n        elif length > len(df):\n            warnings.warn(f\"Warning: structure length {len(df)} doesn't match \"\n                        f\"declared length {length}.\\nData could be corrupted.\")\n\n        try:\n            params = ExtXYZParser(comment).parse()\n            return XYZ(df, comment, params)\n        except ValueError:\n            pass\n\n        return XYZ(df, comment)\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.XYZ.write","title":"write","text":"
    write(file: FileOrPath, fmt: XYZFormat = 'exyz')\n
    Source code in atomlib/io/xyz.py
    def write(self, file: FileOrPath, fmt: XYZFormat = 'exyz'):\n    with open_file(file, 'w', newline='\\r\\n') as f:\n\n        f.write(f\"{len(self.atoms)}\\n\")\n        if len(self.params) > 0 and fmt == 'exyz':\n            f.write(\" \".join(_param_strings(self.params)))\n        else:\n            f.write(self.comment or \"\")\n        f.write(\"\\n\")\n\n        # not my best work\n        col_space = (3, 12, 12, 12)\n        f.writelines(\n            \"\".join(\n                f\"{val:< {space}.8f}\" if isinstance(val, float) else f\"{val:<{space}}\" for (val, space) in zip(_flatten(row), col_space)\n            ).strip() + '\\n' for row in self.atoms.select(('symbol', 'coords')).rows()\n        )\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.XYZ.cell_matrix","title":"cell_matrix","text":"
    cell_matrix() -> Optional[NDArray[float64]]\n
    Source code in atomlib/io/xyz.py
    def cell_matrix(self) -> t.Optional[NDArray[numpy.float64]]:\n    if (s := self.params.get('Lattice')) is None:\n        return None\n\n    try:\n        items = list(map(float, s.split()))\n        if not len(items) == 9:\n            raise ValueError(\"Invalid length\")\n        return numpy.array(items, dtype=numpy.float64).reshape((3, 3)).T\n    except ValueError:\n        warnings.warn(f\"Warning: Invalid format for key 'Lattice=\\\"{s}\\\"'\")\n    return None\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.XYZ.pbc","title":"pbc","text":"
    pbc() -> Optional[NDArray[bool_]]\n
    Source code in atomlib/io/xyz.py
    def pbc(self) -> t.Optional[NDArray[numpy.bool_]]:\n    if (s := self.params.get('pbc')) is None:\n        return None\n\n    val_map = {'0': False, 'f': False, '1': True, 't': True}\n    try:\n        items = [val_map[v.lower()] for v in s.split()]\n        if not len(items) == 3:\n            raise ValueError(\"Invalid length\")\n        return numpy.array(items, dtype=numpy.bool_)\n    except ValueError:\n        warnings.warn(f\"Warning: Invalid format for key 'pbc=\\\"{s}\\\"'\")\n    return None\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.ExtXYZParser","title":"ExtXYZParser","text":"Source code in atomlib/io/xyz.py
    class ExtXYZParser:\n    def __init__(self, comment: str):\n        self._tokens = list(filter(len, _EXT_TOKEN_RE.split(comment)))\n        self._tokens.reverse()\n\n    def peek(self) -> t.Optional[str]:\n        return None if len(self._tokens) == 0 else self._tokens[-1]\n\n    def next(self) -> str:\n        return self._tokens.pop()\n\n    def skip_wspace(self):\n        word = self.peek()\n        while word is not None and word.isspace():\n            self.next()\n            word = self.peek()\n\n    def parse(self) -> t.Dict[str, str]:\n        self.skip_wspace()\n        d = {}\n        while len(self._tokens) > 0:\n            key = self.parse_val()\n            eq = self.next()\n            if not eq == \"=\":\n                raise ValueError(f\"Expected key-value separator, instead got '{eq}'\")\n            val = self.parse_val()\n            d[key] = val\n            self.skip_wspace()\n        return d\n\n    def parse_val(self) -> str:\n        token = self.peek()\n        if token == \"=\":\n            raise ValueError(\"Expected value, instead got '='\")\n        if not token == \"\\\"\":\n            return self.next()\n\n        # quoted string\n        self.next()\n        words = []\n        while not (word := self.peek()) == \"\\\"\":\n            if word is None:\n                raise ValueError(\"EOF while parsing string value\")\n            words += self.next()\n        self.next()\n        return \"\".join(words)\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.ExtXYZParser.peek","title":"peek","text":"
    peek() -> Optional[str]\n
    Source code in atomlib/io/xyz.py
    def peek(self) -> t.Optional[str]:\n    return None if len(self._tokens) == 0 else self._tokens[-1]\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.ExtXYZParser.next","title":"next","text":"
    next() -> str\n
    Source code in atomlib/io/xyz.py
    def next(self) -> str:\n    return self._tokens.pop()\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.ExtXYZParser.skip_wspace","title":"skip_wspace","text":"
    skip_wspace()\n
    Source code in atomlib/io/xyz.py
    def skip_wspace(self):\n    word = self.peek()\n    while word is not None and word.isspace():\n        self.next()\n        word = self.peek()\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.ExtXYZParser.parse","title":"parse","text":"
    parse() -> Dict[str, str]\n
    Source code in atomlib/io/xyz.py
    def parse(self) -> t.Dict[str, str]:\n    self.skip_wspace()\n    d = {}\n    while len(self._tokens) > 0:\n        key = self.parse_val()\n        eq = self.next()\n        if not eq == \"=\":\n            raise ValueError(f\"Expected key-value separator, instead got '{eq}'\")\n        val = self.parse_val()\n        d[key] = val\n        self.skip_wspace()\n    return d\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.ExtXYZParser.parse_val","title":"parse_val","text":"
    parse_val() -> str\n
    Source code in atomlib/io/xyz.py
    def parse_val(self) -> str:\n    token = self.peek()\n    if token == \"=\":\n        raise ValueError(\"Expected value, instead got '='\")\n    if not token == \"\\\"\":\n        return self.next()\n\n    # quoted string\n    self.next()\n    words = []\n    while not (word := self.peek()) == \"\\\"\":\n        if word is None:\n            raise ValueError(\"EOF while parsing string value\")\n        words += self.next()\n    self.next()\n    return \"\".join(words)\n
    "},{"location":"api/io/xyz/#atomlib.io.xyz.batched","title":"batched","text":"
    batched(\n    iterable: Iterable[T], n: int\n) -> Iterator[Tuple[T, ...]]\n
    Source code in atomlib/io/xyz.py
    def batched(iterable: t.Iterable[T], n: int) -> t.Iterator[t.Tuple[T, ...]]:\n    if n < 1:\n        raise ValueError('n must be at least one')\n    it = iter(iterable)\n    while batch := tuple(islice(it, n)):\n        yield batch\n
    "},{"location":"using/coords/","title":"Coordinate systems","text":"

    Under construction

    "},{"location":"using/getting_started/","title":"Getting started","text":"

    atomlib is a package for creating, modifying, and controlling atomic structures. It focuses on flexibilty, correctness, and the preservation of structure metadata, while still managing to support a wide variety of file formats.

    "}]} \ No newline at end of file diff --git a/0.4/sitemap.xml.gz b/0.4/sitemap.xml.gz index 99c01400c1451d046933c957bca96fc5fb81a2d4..d7c33b061138be7b31fba75dfffbf7efe75417c0 100644 GIT binary patch delta 13 Ucmb=gXP58h;9!t;oXB1Q02dYmEC2ui delta 13 Ucmb=gXP58h;AqI=n8;oM02;jm!T
    76
    diff --git a/0.4/assets/_mkdocstrings.css b/0.4/assets/_mkdocstrings.css
    index 85449ec..b500381 100644
    --- a/0.4/assets/_mkdocstrings.css
    +++ b/0.4/assets/_mkdocstrings.css
    @@ -26,20 +26,33 @@
       float: right;
     }
     
    +/* Parameter headings must be inline, not blocks. */
    +.doc-heading-parameter {
    +  display: inline;
    +}
    +
    +/* Prefer space on the right, not the left of parameter permalinks. */
    +.doc-heading-parameter .headerlink {
    +  margin-left: 0 !important;
    +  margin-right: 0.2rem;
    +}
    +
     /* Backward-compatibility: docstring section titles in bold. */
     .doc-section-title {
       font-weight: bold;
     }
     
     /* Symbols in Navigation and ToC. */
    -:root,
    +:root, :host,
     [data-md-color-scheme="default"] {
    +  --doc-symbol-parameter-fg-color: #df50af;
       --doc-symbol-attribute-fg-color: #953800;
       --doc-symbol-function-fg-color: #8250df;
       --doc-symbol-method-fg-color: #8250df;
       --doc-symbol-class-fg-color: #0550ae;
       --doc-symbol-module-fg-color: #5cad0f;
     
    +  --doc-symbol-parameter-bg-color: #df50af1a;
       --doc-symbol-attribute-bg-color: #9538001a;
       --doc-symbol-function-bg-color: #8250df1a;
       --doc-symbol-method-bg-color: #8250df1a;
    @@ -48,12 +61,14 @@
     }
     
     [data-md-color-scheme="slate"] {
    +  --doc-symbol-parameter-fg-color: #ffa8cc;
       --doc-symbol-attribute-fg-color: #ffa657;
       --doc-symbol-function-fg-color: #d2a8ff;
       --doc-symbol-method-fg-color: #d2a8ff;
       --doc-symbol-class-fg-color: #79c0ff;
       --doc-symbol-module-fg-color: #baff79;
     
    +  --doc-symbol-parameter-bg-color: #ffa8cc1a;
       --doc-symbol-attribute-bg-color: #ffa6571a;
       --doc-symbol-function-bg-color: #d2a8ff1a;
       --doc-symbol-method-bg-color: #d2a8ff1a;
    @@ -68,6 +83,15 @@ code.doc-symbol {
       font-weight: bold;
     }
     
    +code.doc-symbol-parameter {
    +  color: var(--doc-symbol-parameter-fg-color);
    +  background-color: var(--doc-symbol-parameter-bg-color);
    +}
    +
    +code.doc-symbol-parameter::after {
    +  content: "param";
    +}
    +
     code.doc-symbol-attribute {
       color: var(--doc-symbol-attribute-fg-color);
       background-color: var(--doc-symbol-attribute-bg-color);
    diff --git a/0.4/index.html b/0.4/index.html
    index c1c16c5..84a8637 100644
    --- a/0.4/index.html
    +++ b/0.4/index.html
    @@ -1212,64 +1212,64 @@ 

    Currently supported file formats

    CIF .cif✅✅✅✅ CIF1 & CIF2. Isotropic B-factor only
    XCrysDen .xsf✅✅✅✅
    AtomEye CFG .cfg✅✅✅✅ Currently basic format only
    Basic XYZ .xyz✅✅✅✅
    Ext. XYZ .exyz✅✅✅✅ Arbitrary properties not implemented
    Special XYZ .sxyz❌❌❌❌ To be implemented
    LAMMPS Data .lmp✅✅✅✅
    Quantum Espresso .qe❌✅❌✅ pw.x format
    pyMultislicer .mslice✅✅✅✅ Currently XML format only