"""This Python module defines various data object types, which are produced and processed within OVITO's data pipeline system.
It also provides the :py:class:`DataCollection` class as a container for such data objects as well as several utility classes for
computing neighbor lists and iterating over the bonds connected to a particle.

Data containers:

  * :py:class:`DataObject` - base of all data object types in OVITO
  * :py:class:`DataCollection` - a general container for data objects representing an entire dataset
  * :py:class:`PropertyContainer` - manages a set of uniform :py:class:`Property` arrays
  * :py:class:`Particles` - a specialized :py:class:`PropertyContainer` for particles
  * :py:class:`Bonds` - specialized :py:class:`PropertyContainer` for bonds
  * :py:class:`VoxelGrid` - specialized :py:class:`PropertyContainer` for 2d and 3d volumetric grids
  * :py:class:`DataTable` - specialized :py:class:`PropertyContainer` for tabulated data
  * :py:class:`Lines` - set of 3d line segments
  * :py:class:`Vectors` - set of vector glyphs

Data objects:

  * :py:class:`Property` - uniform array of property values
  * :py:class:`SimulationCell` - simulation box geometry and boundary conditions
  * :py:class:`SurfaceMesh` - polyhedral mesh representing the boundaries of spatial regions
  * :py:class:`TriangleMesh` - general mesh structure made of vertices and triangular faces
  * :py:class:`DislocationNetwork` - set of discrete dislocation lines with Burgers vector information

Auxiliary data objects:

  * :py:class:`ElementType` - base class for type descriptors used in *typed properties*
  * :py:class:`ParticleType` - describes a single particle or atom type
  * :py:class:`BondType` - describes a single bond type

Utility classes:

  * :py:class:`CutoffNeighborFinder` - finds neighboring particles within a cutoff distance
  * :py:class:`NearestNeighborFinder` - finds *N* nearest neighbor particles
  * :py:class:`BondsEnumerator` - lets you efficiently iterate over the bonds connected to a particle"""
__all__ = ['DataCollection', 'DataObject', 'TriangleMesh', 'AttributeDataObject', 'Lines', 'Vectors', 'TrajectoryLines', 'SimulationCell', 'Property', 'PropertyContainer', 'DataTable', 'ElementType', 'SurfaceMesh', 'SurfaceMeshTopology', 'DelaunayTessellation', 'VoxelGrid', 'ParticleType', 'BondType', 'BondsEnumerator', 'CutoffNeighborFinder', 'NearestNeighborFinder', 'PTMNeighborFinder', 'Particles', 'Bonds', 'TrajectoryLines', 'DislocationNetwork', 'DislocationSegment']
from __future__ import annotations
from typing import Tuple, Optional, Any, Union, Iterator, Mapping, MutableMapping, Sequence, MutableSequence, TypeVar, Callable, List, overload, Type
import ovito.vis
import ovito.pipeline
import numpy
from ovito import ArrayLike
from numpy.typing import NDArray, DTypeLike
import numbers
import enum
import os
from dataclasses import dataclass
DataObjectType = TypeVar('DataObjectType', bound='DataObject')

@dataclass(kw_only=True)
class DataObject:
    """Abstract base class for all data object types in OVITO.

A :py:class:`DataObject` represents a fragment of data processed in or by a data pipeline.
See the :py:mod:`ovito.data` module for a list of different concrete data object types in OVITO.
Data objects are typically contained in a :py:class:`DataCollection`,
which represents a whole *data set*. Furthermore, data objects can be nested into a hierarchy.
For example, the :py:class:`Bonds` data object is part of the parent :py:class:`Particles` data object.

Data objects by themselves are non-visual objects. Visualizing the information stored in a data object in images
is the responsibility of so-called visual elements.
A data object may be associated with a :py:class:`DataVis` element by assigning it to the data object's :py:attr:`vis` field.
Each type of visual element exposes a set of parameters that allow you to configure the appearance of the
data visualization in rendered images and animations."""
    identifier: str = ''
    "identifier() -> str\n\nThe unique identifier string of the data object.\n\nThis identifier serves as a lookup key in object dictionaries, such as the :py:attr:`DataCollection.tables` collection.\nGenerally, the identifier also serves as a way to reference the object within the :py:class:`DataCollection`, for example,\nwhen specifying which data object a modifier should operate on.\n\nThe identifier string must not contain slashes ('/') or colons (':').\n\nData objects generated by modifiers typically receive an automatically assigned identifier,\nas described in the documentation of the respective modifier. When implementing a custom\nmodifier function, it is your responsibility to assign meaningful\nidentifiers to any new data objects your function creates. This ensures that subsequent modifiers\ncan correctly reference and look up these objects."
    vis: Optional[ovito.vis.DataVis] = None
    'vis() -> Optional[ovito.vis.DataVis]\n\nThe :py:class:`DataVis` element currently associated with this data object. This object is responsible for\nvisually rendering the stored data. If set to ``None``, the data object remains non-visual and does not appear in\nrendered images or viewports. Additionally, note that the same :py:class:`DataVis` element may be assigned to\nmultiple data objects to synchronize their visual appearance.\n\nSee the :py:mod:`ovito.vis` module for a list of visual element types.'

    def make_mutable(self, subobj: DataObjectType) -> DataObjectType:
        """Ensures exclusive ownership of the given sub-object by performing a copy-on-write operation if necessary.

This method checks whether *subobj*, a child of the calling :py:class:`DataObject`, is shared with
any other parent object. If *subobj* is referenced by multiple parent objects, a copy is created
to ensure that modifications do not affect other owners. However, if *subobj* is exclusively owned by
this :py:class:`DataObject`, no copy is made, and the original instance is returned.

By ensuring exclusive ownership before modification, this method prevents unintended side effects caused
by modifying a shared object. The returned object is guaranteed to be safe for modification
without affecting any other references.

See :ref:`underscore_notation` for a discussion of object ownership and common use cases for this method.

:param subobj: An existing sub-object of this parent data object, for which exclusive ownership is requested.
:return: A copy of *subobj* if it was shared with another parent; otherwise, the original object."""
        ...

    class Ref:
        """This data structure describes a reference to some :py:class:`DataObject` to be retrieved from a :py:class:`DataCollection`.
In other words, the :py:class:`~DataObject.Ref` class does not hold an actual data object but all the information needed to locate it in the output of a
:py:class:`Pipeline`.

:param cls: The class type of the object being referenced.
:param path: The :py:attr:`~DataObject.identifier` of the object being referenced.

Given a :py:class:`~DataObject.Ref` and some :py:class:`DataCollection`, you can look up the actual data object
with the :py:meth:`DataCollection.get` method::

    ref = DataObject.Ref(VoxelGrid, 'density')
    data = pipeline.compute()
    voxel_grid = data.get(ref)
    assert voxel_grid is data.grids['density']

A :py:class:`~DataObject.Ref` instance is what the :py:class:`ovito.traits.DataObjectReference` parameter trait uses to let the
user select a data object from a data collection.

Examples for valid :py:class:`~DataObject.Ref` definitions::

    DataObject.Ref()                              # Null reference
    DataObject.Ref(Particles)                     # References the Particles object
    DataObject.Ref(Bonds)                         # References the Bonds object
    DataObject.Ref(DataTable, 'coordination-rdf') # References the DataTable 'coordination-rdf'
    DataObject.Ref(PropertyContainer, 'isosurface/vertices') # References the vertices of a SurfaceMesh
    DataObject.Ref(AttributeDataObject, 'ClusterAnalysis.cluster_count') # References a global attribute"""

        def __init__(self, cls: Optional[Type[ovito.data.DataObject]]=None, path: str='', title: str='') -> None:
            ...

        @property
        def cls(self) -> Optional[Type[ovito.data.DataObject]]:
            """The concrete Python class type of the referenced data object, e.g. :py:class:`Particles` or :py:class:`DataTable`."""
            ...

        @property
        def path(self) -> str:
            """The unique :py:attr:`~DataObject.identifier` of the referenced data object. This identifier is used by the :py:meth:`~DataCollection.get` method to locate the object in a data collection.
If the referenced object is nested in a hierarchy of data objects, the path contains multiple identifiers separated by slashes, e.g.
``'particles/bonds/Bond Types/1'``.
If only a single instance of the object type specified by :py:attr:`cls` exists in the data collection, the path can be omitted.
This is the case, for example, for the :py:class:`Particles` and :py:class:`Bonds` objects."""
            ...

        @property
        def title(self) -> str:
            ...

        def __bool__(self) -> bool:
            ...

class DataObjectsDict(Mapping[str, DataObjectType]):

    def create(self, identifier: str, *, vis_params: Optional[dict[str, Any]]=None, **kwargs: Any) -> DataObjectType:
        ...

class DataCollection(DataObject):
    """Base: :py:class:`ovito.data.DataObject`

A :py:class:`DataCollection` is a container class that holds together individual *data objects*, each representing
different fragments of a dataset. For example, a dataset loaded from a simulation data file may consist
of particles, simulation cell information, and additional auxiliary data such as the current time step
number of the snapshots, etc. All this information is contained in one :py:class:`DataCollection`, which
exposes the individual pieces of information as sub-objects, for example, via the :py:attr:`DataCollection.particles`,
:py:attr:`DataCollection.cell`, and :py:attr:`DataCollection.attributes` fields.

Data collections are the elementary entities that get processed within a data :py:class:`Pipeline`.
Each modifier receives a data collection from the preceding modifier, alters it in some way, and passes it
on to the next modifier. The output data collection of the last modifier in the pipeline is returned by the `Pipeline.compute()` method.

A data collection essentially consists of a bunch of `DataObjects`,
which are all stored in the :py:attr:`DataCollection.objects` list. Typically, you don't access the data objects list
directly but rather use one of the special accessor fields provided by the :py:class:`DataCollection` class,
which give more convenient access to data objects of a particular kind. For example, the :py:attr:`DataCollection.surfaces`
dictionary provides key-based access to all the :py:class:`SurfaceMesh` instances currently in the data collection."""

    def __init__(self, **kwargs: Any) -> None:
        self.dislocations: Optional[DislocationNetwork]
        'Returns the :py:class:`DislocationNetwork` data object; or ``None`` if there\nis no object of this type in the collection. Typically, the :py:class:`DislocationNetwork` is created by a pipeline\ncontaining the :py:class:`DislocationAnalysisModifier`.'
        self.dislocations_: Optional[DislocationNetwork]
    objects: MutableSequence[DataObject]
    "objects() -> collections.abc.MutableSequence[ovito.data.DataObject]\n\nList of all top-level `DataObjects` in this data collection. You can add or remove data objects from this list as needed. \n\nTypically, however, you don't need to work with this list directly, because the :py:class:`DataCollection` class provides several convenience accessor attributes for the different flavors of data objects in OVITO. For example, :py:attr:`DataCollection.particles` returns the :py:class:`Particles` object (by looking it up in the :py:attr:`objects` list for you). Dictionary-like views such as :py:attr:`DataCollection.tables` and :py:attr:`DataCollection.surfaces` provide key-based access to particular kinds of data objects in the collection. \n\nTo add new objects to the data collection, you can append them to the :py:attr:`objects` list or, more conveniently, use creation functions such as :py:meth:`create_particles`, :py:meth:`create_cell`, or `tables.create()`, which are provided by the :py:class:`DataCollection` class."
    objects_: MutableSequence[DataObject]

    @property
    def attributes(self) -> MutableMapping[str, Any]:
        """This field contains a dictionary view with all the *global attributes* currently associated with this data collection.
Global attributes are key-value pairs that represent small tokens of information, typically simple value types such as ``int``, ``float`` or ``str``.
Every attribute has a unique identifier such as ``'Timestep'`` or ``'ConstructSurfaceMesh.surface_area'``. This identifier serves as lookup key in the :py:attr:`attributes` dictionary.
Identifiers starting with ``'.'`` are hidden in the GUI. Attributes are dynamically generated by modifiers in a data pipeline or come from the data source.
For example, if the input simulation file contains timestep information, the timestep number is made available by the :py:attr:`FileSource` as the
``'Timestep'`` attribute. It can be retrieved from pipeline's output data collection:

    >>> pipeline = import_file('snapshot_140000.dump')
    >>> pipeline.compute().attributes['Timestep']
    140000

Some modifiers report their calculation results by adding new attributes to the data collection. See each modifier's
reference documentation for the list of attributes it generates. For example, the number of clusters identified by the
:py:class:`ClusterAnalysisModifier` is available in the pipeline output as an attribute named
``ClusterAnalysis.cluster_count``::

    pipeline.modifiers.append(ClusterAnalysisModifier(cutoff = 3.1))
    data = pipeline.compute()
    nclusters = data.attributes["ClusterAnalysis.cluster_count"]

The :py:func:`ovito.io.export_file` function can be used to output dynamically computed attributes to a text file, possibly as functions of time::

    export_file(pipeline, "data.txt", "txt/attr",
        columns = ["Timestep", "ClusterAnalysis.cluster_count"],
        multiple_frames = True)

If you are writing your own modifier function, you let it add new attributes to a data collection.
In the following example, the :py:class:`CommonNeighborAnalysisModifier` first inserted into the
pipeline generates the ``'CommonNeighborAnalysis.counts.FCC'`` attribute to report the number of atoms that
have an FCC-like coordination. To compute an atomic *fraction* from that, we need to divide the count by the total number of
atoms in the system. To this end, we append a user-defined modifier function
to the pipeline, which computes the fraction and outputs the value as a new attribute named ``'fcc_fraction'``.

```python
  pipeline.modifiers.append(CommonNeighborAnalysisModifier())
              
  def compute_fcc_fraction(frame, data):
      n_fcc = data.attributes['CommonNeighborAnalysis.counts.FCC']
      data.attributes['fcc_fraction'] = n_fcc / data.particles.count
  
  pipeline.modifiers.append(compute_fcc_fraction)
  print(pipeline.compute().attributes['fcc_fraction'])
```"""
        ...

    @property
    def cell(self) -> SimulationCell:
        """Returns the :py:class:`SimulationCell` data object describing the cell vectors and periodic boundary
condition flags. It may be ``None``.

If you intend to modify the :py:class:`SimulationCell` data object within this data collection, use the :py:attr:`cell_`
attribute instead to explicitly request a mutable version of the cell object. See topic :ref:`underscore_notation` for more information.
Use :py:attr:`cell` for read access and :py:attr:`cell_` for write access, e.g. ::

    print(data.cell.volume)
    data.cell_.pbc = (True, True, False)

To create a :py:class:`SimulationCell` in a data collection that might not have a simulation cell yet, use the
:py:meth:`create_cell` method or simply assign a new instance of the :py:class:`SimulationCell` class to the :py:attr:`cell` attribute."""
        ...

    @property
    def cell_(self) -> SimulationCell:
        ...

    @property
    def particles(self) -> Particles:
        """Returns the :py:class:`Particles` object, which manages all per-particle properties.
It may be ``None`` if the data collection contains no particle model at all.

If you intend to modify the contents of the :py:class:`Particles` object in some way, use the :py:attr:`particles_`
attribute instead to explicitly request a mutable version of the particles object. See topic :ref:`underscore_notation` for more information.
Use :py:attr:`particles` for read access and :py:attr:`particles_` for write access, e.g. ::

    print(data.particles.positions[0])
    data.particles_.positions_[0] += (0.0, 0.0, 2.0)

To create a new :py:class:`Particles` object in a data collection that might not have particles yet, use the
:py:meth:`create_particles` method or simply assign a new instance of the :py:class:`Particles` class to the :py:attr:`particles` attribute."""
        ...

    @property
    def particles_(self) -> Particles:
        ...

    @property
    def grids(self) -> DataObjectsDict[VoxelGrid]:
        """Returns a dictionary view providing key-based access to all `VoxelGrids` in
this data collection. Each :py:class:`VoxelGrid` has a unique :py:attr:`identifier` key,
which allows you to look it up in this dictionary. To find out which voxel grids exist in the data collection and what
their identifiers are, use

```python
  print(data.grids)
```

Then retrieve the desired :py:class:`VoxelGrid` from the collection using its identifier key, e.g.

```python
  charge_density_grid = data.grids['charge-density']
  print(charge_density_grid.shape)
```

The view provides the convenience method :py:meth:`grids.create`, which
inserts a newly created :py:class:`VoxelGrid` into the data collection. The method expects the unique :py:attr:`identifier`
of the new grid as first argument. All other keyword arguments are forwarded to the constructor
to initialize the member fields of the :py:class:`VoxelGrid` class:

```python
  grid = data.grids.create(
      identifier="grid",
      title="Field",
      shape=(10,10,10),
      domain=data.cell)
```

If there is already an existing grid with the same :py:attr:`identifier` in the collection, the :py:meth:`create`
method modifies and returns that existing grid instead of creating another one."""
        ...

    @property
    def grids_(self) -> DataObjectsDict[VoxelGrid]:
        ...

    @property
    def surfaces(self) -> DataObjectsDict[SurfaceMesh]:
        """Returns a dictionary view providing key-based access to all :py:class:`SurfaceMesh` objects in
this data collection. Each :py:class:`SurfaceMesh` has a unique :py:attr:`identifier` key,
which can be used to look it up in the dictionary.
See the documentation of the modifier producing the surface mesh to find out what the right key is, or use

```python
  print(data.surfaces)
```

to see which identifier keys exist. Then retrieve the desired :py:class:`SurfaceMesh` object from the collection using its identifier
key, e.g.

```python
  surface = data.surfaces['surface']
  print(surface.vertices['Position'])
```

The view provides the convenience method :py:meth:`surfaces.create`, which
inserts a newly created :py:class:`SurfaceMesh` into the data collection. The method expects the unique :py:attr:`identifier`
of the new surface mesh as first argument. All other keyword arguments are forwarded to the constructor
to initialize the member fields of the :py:class:`SurfaceMesh` class:

```python
  mesh = data.surfaces.create(
      identifier="surface",
      title="A surface mesh",
      domain=data.cell)
```

If there is already an existing mesh with the same :py:attr:`identifier` in the collection, the :py:meth:`create`
method modifies and returns that existing mesh instead of creating another one."""
        ...

    @property
    def surfaces_(self) -> DataObjectsDict[SurfaceMesh]:
        ...

    @property
    def tables(self) -> DataObjectsDict[DataTable]:
        """A dictionary view of all :py:class:`DataTable` objects in
this data collection. Each :py:class:`DataTable` has a unique :py:attr:`identifier` key,
which allows it to be looked up in this dictionary. Use

```python
  print(data.tables)
```

to find out which table identifiers are present in the data collection.
Then use the identifier to retrieve the desired :py:class:`DataTable` from the dictionary, e.g.

```python
  rdf = data.tables['coordination-rdf']
  print(rdf.xy())
```

The view provides the convenience method :py:meth:`tables.create`, which
inserts a newly created :py:class:`DataTable` into the data collection. The method expects the unique :py:attr:`identifier`
of the new data table as first argument. All other keyword arguments are forwarded to the constructor
to initialize the member fields of the :py:class:`DataTable` class:

```python
  # Code example showing how to compute a histogram of the particles' x-coordinates within some interval.
  x_interval = (0.0, 100.0)
  x_coords = data.particles.positions[:,0]
  histogram = numpy.histogram(x_coords, bins=50, range=x_interval)[0]
  
  # Output the histogram as a new DataTable, which makes it appear in OVITO's data inspector panel:
  table = data.tables.create(
      identifier='binning',
      title='Binned particle counts',
      plot_mode=DataTable.PlotMode.Histogram,
      interval=x_interval,
      axis_label_x='Position X',
      count=len(histogram))
  table.y = table.create_property('Particle count', data=histogram)
```

If there is already an existing table with the same :py:attr:`identifier` in the collection, the :py:meth:`create`
method modifies and returns that existing table instead of creating another one."""
        ...

    @property
    def tables_(self) -> DataObjectsDict[DataTable]:
        ...

    @property
    def triangle_meshes(self) -> DataObjectsDict[TriangleMesh]:
        """This is a dictionary view providing key-based access to all :py:class:`TriangleMesh` objects currently stored in
this data collection. Each :py:class:`TriangleMesh` has a unique :py:attr:`identifier` key,
which can be used to look it up in the dictionary."""
        ...

    @property
    def triangle_meshes_(self) -> DataObjectsDict[TriangleMesh]:
        ...

    @property
    def lines(self) -> DataObjectsDict[Lines]:
        """A dictionary view providing key-based access to all :py:class:`Lines` objects in
this data collection. Each :py:class:`Lines` object has a unique :py:attr:`identifier` key,
which can be used to look it up in the dictionary. You can use

```python
  print(data.lines)
```

to see which identifiers exist. Then retrieve the desired :py:class:`Lines` object from the collection using its identifier
key, e.g.

```python
  lines = data.lines["trajectories"]
  print(lines["Position"])
```

The :py:class:`Lines` object with the identifier ``"trajectories"``, for example, is the one that gets
created by the :py:class:`GenerateTrajectoryLinesModifier`.

If you would like to create a new :py:class:`Lines` object, in a user-defined modifier for instance,
the dictionary view provides the method :py:meth:`lines.create`, which
creates a new :py:class:`Lines` and adds it to the data collection. The method expects the unique :py:attr:`identifier`
of the new lines object as first argument. All other keyword arguments are forwarded to the class constructor
to initialize the member fields of the :py:class:`Lines` object:

```python
  lines = data.lines.create(identifier="mylines")
```

If there is already an existing :py:class:`Lines` object with the same :py:attr:`identifier` in the collection, the :py:meth:`create`
method returns that object instead of creating another one and makes sure it can be safely modified."""
        ...

    @property
    def lines_(self) -> DataObjectsDict[Lines]:
        ...

    @property
    def vectors(self) -> DataObjectsDict[Vectors]:
        """A dictionary view providing key-based access to all :py:class:`Vectors` objects in
this data collection. Each :py:class:`Vectors` object has a unique :py:attr:`identifier` key,
which can be used to look it up in the dictionary. You can use

```python
  print(data.vectors)
```

to see which identifiers exist. Then retrieve the desired :py:class:`Vectors` object from the
collection using its identifier key, e.g.

```python
  vectors = data.vectors["vectors"]
  print(vectors["Position"])
  print(vectors["Direction"])
```

If you would like to create a new :py:class:`Vectors` object, in a user-defined modifier for instance,
the dictionary view provides the method :py:meth:`vectors.create`, which
creates a new :py:class:`Vectors` object and adds it to the data collection.
The method expects the unique :py:attr:`identifier`
of the new vectors object as first argument. All other keyword arguments are forwarded to the class constructor
to initialize the member fields of the :py:class:`Vectors` object:

```python
  vectors = data.vectors.create(identifier="myVectors")
```

If there is already an existing :py:class:`Vectors` object with the same :py:attr:`identifier`
in the collection, the :py:meth:`create` method returns that object instead of creating another
one and makes sure it can be safely modified."""
        ...

    @property
    def vectors_(self) -> DataObjectsDict[Vectors]:
        ...

    def apply(self, modifier: Union[ovito.pipeline.Modifier, ovito.pipeline.ModifierInterface, Callable], frame: Optional[int]=None) -> None:
        """This method applies a :py:class:`Modifier` function to the data stored in this collection to modify it in place.

:param modifier: The :py:class:`Modifier` instance that should alter the contents of this data collection in place.
:param frame: Optional animation frame number to be passed to the modifier function, which may use it for time-dependent modifications.

The method allows modifying a data collection with one of OVITO's modifiers directly without the need to build up a complete
:py:class:`Pipeline` first. In contrast to a data pipeline, the :py:meth:`!apply()` method
executes the modifier function immediately and alters the data in place. In other words, the original data in this :py:class:`DataCollection`
gets replaced by the output produced by the invoked modifier function. It is possible to first create a copy of
the original data using the :py:meth:`.clone` method if needed. The following code example
demonstrates how to use :py:meth:`!apply()` to successively modify a dataset:

```python
  from ovito.io import import_file
  from ovito.modifiers import *
  
  data = import_file("input/simulation.dump").compute()
  data.apply(RadialDistributionFunctionModifier(cutoff=2.9))
  data.apply(ExpressionSelectionModifier(expression="Coordination<9"))
  data.apply(DeleteSelectedModifier())
```

Note that it is typically possible to achieve the same result by first populating a :py:class:`Pipeline` with the modifiers and then calling its
:py:meth:`compute` method at the very end:

```python
  pipeline = import_file("input/simulation.dump")
  pipeline.modifiers.append(RadialDistributionFunctionModifier(cutoff=2.9))
  pipeline.modifiers.append(ExpressionSelectionModifier(expression="Coordination<9"))
  pipeline.modifiers.append(DeleteSelectedModifier())
  data = pipeline.compute()
```

An important use case of the :py:meth:`!apply()` method is in the implementation of a user-defined modifier function,
making it possible to invoke other modifiers as sub-routines:

```python
  # A user-defined modifier function that calls the built-in ColorCodingModifier
  # as a sub-routine to assign a color to each atom based on some property
  # created within the function itself:
  def modify(frame: int, data: DataCollection):
      data.particles_.create_property('idx', data=numpy.arange(data.particles.count))
      data.apply(ColorCodingModifier(property='idx'), frame)
  
  # Set up a data pipeline that uses the user-defined modifier function:
  pipeline = import_file("input/simulation.dump")
  pipeline.modifiers.append(modify)
  data = pipeline.compute()
```"""
        ...

    def clone(self) -> DataCollection:
        """Returns a shallow copy of this :py:class:`DataCollection` containing the same data objects as the original.

The method can be used to retain a copy of the original data before modifying a data collection in place,
for example, using the :py:meth:`.apply` method:

```python
  original = data.clone()
  data.apply(ExpressionSelectionModifier(expression="Position.Z < 0"))
  data.apply(DeleteSelectedModifier())
  print("Number of atoms before:", original.particles.count)
  print("Number of atoms after:", data.particles.count)
```

Note that the :py:meth:`!clone()` method performs an inexpensive shallow copy, meaning that the newly created collection
still shares the data objects with the original collection.

Keep in mind that data objects shared by two or more data collections are implicitly protected against modifications
to avoid unexpected side effects. Thus, in order to subsequently modify the objects in either the original or the
copy of the data collection, you have to use the underscore notation or the :py:meth:`DataObject.make_mutable` method
to make a deep copy of the particular data object(s) you want to modify. For example:

```python
  copy = data.clone()
  # Data objects are shared by original and copy:
  assert(copy.cell is data.cell)
  
  # In order to modify the SimulationCell in the dataset copy, we must request
  # a mutable version of the SimulationCell using the 'cell_' accessor:
  copy.cell_.pbc = (False, False, False)
  
  # As a result, the cell object in the second data collection has been replaced
  # with a deep copy and the two data collections no longer share the same
  # simulation cell object:
  assert(copy.cell is not data.cell)
```"""
        ...

    def create_cell(self, matrix: ArrayLike, pbc: Tuple[bool, bool, bool]) -> SimulationCell:
        """This convenience method conditionally creates a new :py:class:`SimulationCell` object and stores it in this data collection.
If a simulation cell already existed in the collection (:py:attr:`.cell` is not ``None``), then that cell object is
replaced with a modifiable copy if necessary and the matrix and PBC flags are set to the given values.
The attached :py:class:`SimulationCellVis` element is maintained in this case.

:param matrix: A 3x4 array to initialize the cell matrix with. It specifies the three cell vectors and the origin.
:param pbc: A tuple of three Booleans specifying the cell's :py:attr:`~SimulationCell.pbc` flags.
:param vis_params: Optional dictionary to initialize attributes of the attached :py:class:`SimulationCellVis` element (only used if the cell object is newly created by the method).

The logic of this method is roughly equivalent to the following code::

    def create_cell(data: DataCollection, matrix, pbc, vis_params=None) -> SimulationCell:
        if data.cell is None:
            data.cell = SimulationCell(pbc=pbc)
            data.cell[...] = matrix
            data.cell.vis.line_width = <...> # Some value that scales with the cell's size
            if vis_params:
                for name, value in vis_params.items(): setattr(data.cell.vis, name, value)
        else:
            data.cell_[...] = matrix
            data.cell_.pbc = pbc
        return data.cell_"""
        ...

    def create_particles(self, **kwargs: Any) -> Particles:
        """This convenience method conditionally creates a new :py:class:`Particles` container object and stores it in this data collection.
If the data collection already contains an existing particles object (:py:attr:`.particles` is not ``None``), then that particles object is
replaced with a modifiable copy if necessary. The associated :py:class:`ParticlesVis` element is preserved.

:param params: Key-value pairs passed to the method as keyword arguments are used to set attributes of the :py:class:`Particles` object (even if the particles object already existed).
:param vis_params: Optional dictionary to initialize attributes of the attached :py:class:`ParticlesVis` element (only used if the particles object is newly created by the method).

The logic of this method is roughly equivalent to the following code::

    def create_particles(data: DataCollection, vis_params=None, **params) -> Particles:
        if data.particles is None:
            data.particles = Particles()
            if vis_params:
                for name, value in vis_params.items(): setattr(data.particles.vis, name, value)
        for name, value in params.items(): setattr(data.particles_, name, value)
        return data.particles_

Usage example:

```python
  coords = [(-0.06,  1.83,  0.81), # xyz coordinates of the 3 particle system to create
            ( 1.79, -0.88, -0.11),
            (-1.73, -0.77, -0.61)]
  particles = data.create_particles(count=len(coords), vis_params={'radius': 1.4})
  particles.create_property('Position', data=coords)
```"""
        ...

    def get(self, ref: DataObject.Ref, require: bool=True, path: bool=False) -> Optional[DataObject]:
        """Resolves a :py:class:`DataObject.Ref` reference by retrieving the referenced data object from this :py:class:`DataCollection`.

:param ref: The reference to the data object to be retrieved.
:param require: If ``True``, the method raises a `KeyError` if the referenced object does not exist in the data collection. If ``False``, the method returns ``None`` in the not-found case.
:param path: If ``True``, the method returns a list of the data objects in the nested object hierarchy leading to the referenced object from the root of the :py:class:`DataCollection`.
:returns: The referenced :py:class:`DataObject`, or ``None`` if *require=False* and the data object could not be found or if *ref* is a null reference.

An :ref:`"underscore version" <underscore_notation>` of this method is available, which should be used whenever you intend to modify the returned
data object. :py:meth:`!get_()` implicitly calls :py:meth:`make_mutable` to ensure the data object can be modified
without unexpected side effects."""
        ...

    def get_(self, ref: DataObject.Ref, require: bool=True, path: bool=False) -> Optional[DataObject]:
        ...

@dataclass(kw_only=True)
class ElementType(DataObject):
    """Base: :py:class:`ovito.data.DataObject`

This class describes a single "type", for example a particle or bond type, that is part of a typed property array.

The :py:class:`ElementType` class is the generic base class used for all type descriptors in OVITO. It stores general attributes such as a type's
unique numeric :py:attr:`.id`, its human-readable :py:attr:`.name`, and its display :py:attr:`color`.
Note that, for certain typed properties, OVITO uses more specific sub-classes such as :py:class:`ParticleType` and :py:class:`BondType`,
which can store additional attributes such as :py:attr:`~ParticleType.radius` and :py:attr:`~ParticleType.mass`.

The :py:class:`ElementType` instances associated with a typed property are found in the
`Property.types` list.

You can use the `Property.type_by_id()` and `Property.type_by_name()`
methods to look up a certain :py:class:`ElementType` based on a numeric identifier or name string."""
    color: ovito.vis.Color = (1.0, 1.0, 1.0)
    'color() -> tuple[float, float, float]\n\nThe color used when rendering elements of this type. This is a RGB tuple with components in the range 0.0 -- 1.0.\n\nDefault: ``(1.0, 1.0, 1.0)``'
    enabled: bool = True
    'enabled() -> bool\n\nControls whether this type is currently *active* or *inactive*. This flag currently has a meaning only in the context of atomic structure identification. Some analysis modifiers manage a list of the structure types they can identify (e.g. FCC, BCC, etc.). The identification of individual structure types can be turned on or off by the user by changing their :py:attr:`enabled` flag. See `StructureIdentificationModifier.structures` for further information. \n\nDefault: ``True``'
    id: int = 0
    'id() -> int\n\nThe unique numeric identifier of the type (typically some positive ``int``). The identifier is and must be unique among all element types in the :py:attr:`types` list of a typed :py:class:`Property`. Thus, if you create a new element type, make sure you give it a unique id before inserting it into the :py:attr:`types` list of a typed property. \n\nDefault: ``0``'
    name: str = ''
    "name() -> str\n\nThe name of this type, e.g. the chemical element symbol of an atom type. This string may be empty, in which case its numeric :py:attr:`id` is the only way of referring to this type. \n\nDefault: ``''``"

@dataclass(kw_only=True)
class ParticleType(ElementType):
    """Base: :py:class:`ovito.data.ElementType`

This data object describes one particle or atom type. In atomistic simulations, each chemical element is typically represented by an instance of
the :py:class:`ParticleType` class. The attributes of this class control how the particles of that type get visualized in terms of e.g. color, particle radius, shape, etc.

The :py:class:`ParticleType` class inherits several general attribute fields from its base class :py:class:`ElementType`, e.g. the :py:attr:`~ElementType.color`, :py:attr:`~ElementType.name`
and :py:attr:`~ElementType.id` fields. It adds specific fields for particles: :py:attr:`radius` and :py:attr:`shape`. Furthermore, the class has additional
fields controlling the visual appearance of particles with user-defined shapes.

The :py:class:`ParticleType` instances all live in the `Property.types` list of the ``'Particle Type'`` standard :py:class:`Property` array,
which is accessible as `Particles.particle_types`.
The association of particles with particle types is established through the unique type IDs.
The following code shows how to first list all unique particle types defined for a structure and then print each particle's type ID by iterating over the data array:

```python
  # Access the property with the name 'Particle Type':
  type_property = data.particles.particle_types
  
  # Print list of particle types (their numeric IDs and names)
  for t in type_property.types:
      print(f'ID {t.id} -> {t.name}')
  
  # Print the numeric type ID of each particle:
  for tid in type_property[...]:
      print(tid)
```

A common task is to look up the :py:class:`ParticleType` that corresponds to a given numeric type ID. For this, the :py:class:`Property` class provides the :py:meth:`type_by_id` method:

```python
  # Look up the particle type with unique ID 2:
  t = type_property.type_by_id(2)
  print(t.name, t.color, t.radius)
  
  # Iterate over all particles and print their type's name:
  for tid in type_property[...]:
      print(type_property.type_by_id(tid).name)
```

Another common operation is to look up a particle type by name, for example the type representing a certain chemical element.
For this type of look-up the :py:meth:`type_by_name` method is available, which assumes that types have
unique `names`:

```python
  print(type_property.type_by_name('Si').id)   # Print numeric ID of type 'Si'
```"""
    backface_culling: bool = True
    "backface_culling() -> bool\n\nActivates back-face culling for the user-defined particle shape mesh\nto speed up rendering. If turned on, polygonal sides of the shape mesh facing away from the viewer will not be rendered.\nYou can turn this option off if the particle's shape is not closed and two-sided rendering is required.\nThis option only has an effect if a user-defined shape has been assigned to the particle type using the :py:meth:`.load_shape`\nmethod.\n\nDefault: ``True``"
    highlight_edges: bool = False
    'highlight_edges() -> bool\n\nActivates the highlighting of the polygonal edges of the user-defined particle shape during rendering.\nThis option only has an effect if a user-defined shape has been assigned to the particle type using the\n:py:meth:`.load_shape` method.\n\nDefault: ``False``'
    mass: float = 0.0
    'mass() -> float\n\nThe mass of this particle type.\n\nDefault: ``0.0``'
    radius: float = 0.0
    'radius() -> float\n\nThis attribute controls the display radius of all particles of this type.\n\nWhen set to zero, particles of this type will be rendered using the standard size specified\nby the `ParticlesVis.radius` parameter.\nFurthermore, precedence is given to any per-particle sizes assigned to the ``Radius`` particle property if that property has been defined.\n\nDefault: ``0.0``\n\nThe following example script demonstrates how to set the display radii of two particle types loaded from\na simulation file, which can be accessed through the `Property.types` list\nof the ``Particle Type`` particle property.\n\n```python\n  pipeline = import_file("input/simulation.dump")\n  pipeline.add_to_scene()\n\n  def setup_particle_types(frame, data):\n      types = data.particles_.particle_types_\n      types.type_by_id_(1).name = "Cu"\n      types.type_by_id_(1).radius = 1.35\n      types.type_by_id_(2).name = "Zr"\n      types.type_by_id_(2).radius = 1.55\n  pipeline.modifiers.append(setup_particle_types)\n```'
    shape: ovito.vis.ParticlesVis.Shape = ovito.vis.ParticlesVis.Shape.Unspecified
    'shape() -> ParticlesVis.Shape\n\nSelects the geometric shape used when rendering particles of this type. Supported modes are:\n\n   * ``ParticlesVis.Shape.Unspecified`` (default)\n   * ``ParticlesVis.Shape.Sphere``\n   * ``ParticlesVis.Shape.Box``\n   * ``ParticlesVis.Shape.Circle``\n   * ``ParticlesVis.Shape.Square``\n   * ``ParticlesVis.Shape.Cylinder``\n   * ``ParticlesVis.Shape.Spherocylinder``\n   * ``ParticlesVis.Shape.Mesh``\n\nBy default, the standard particle shape that is set in the `ParticlesVis`\nvisual element is used to render particles of this type. Parameter values other than ``Unspecified`` allow you to\ncontrol the rendering shape on a per-type basis.\nMode ``Sphere`` includes ellipsoid and superquadric particle shapes, which are enabled by the presence of\nthe ``Aspherical Shape`` and ``Superquadric Roundness`` particle properties.\n\nThe :py:meth:`load_shape` method lets you specify a user-defined :py:attr:`mesh` geometry for this particle type.\nCalling this method automatically switches the shape parameter to mode ``Mesh``.\n\nSetting the shapes of particle types permanently, i.e., for all frames of a loaded simulation trajectory, typically\nrequires a user-defined modifier function. This function is inserted into the :py:class:`Pipeline` to make the\nnecessary changes to the :py:class:`ParticleType` objects associated with the :py:class:`Property` named ``Particle Type``:\n\n```python\n  from ovito.io import import_file\n  from ovito.vis import *\n\n  # Load a simulation file containing numeric particle types 1, 2, 3, ...\n  pipeline = import_file("input/nylon.data")\n  pipeline.add_to_scene()\n\n  # Set the default particle shape in the ParticlesVis visual element, \n  # which will be used by all particle types for which we do not specify a different shape below.\n  pipeline.compute().particles.vis.shape = ParticlesVis.Shape.Box\n  pipeline.compute().particles.vis.radius = 1.0\n\n  # A user-defined modifier function that configures the shapes of particle types 1 and 2:\n  def setup_particle_types(frame, data): \n      # Write access to property \'Particle Type\':\n      types = data.particles_.particle_types_  \n      # Write access to numeric ParticleTypes, which are sub-objects of the Property object:\n      types.type_by_id_(1).radius = 0.5\n      types.type_by_id_(1).shape = ParticlesVis.Shape.Cylinder\n      types.type_by_id_(2).radius = 1.2\n      types.type_by_id_(2).shape = ParticlesVis.Shape.Sphere\n  pipeline.modifiers.append(setup_particle_types)\n\n  # Render a picture of the 3d scene:\n  vp = Viewport(camera_dir = (-2,1,-1))\n  vp.zoom_all()\n  vp.render_image(filename=\'output/particles.png\', size=(320,240), renderer=TachyonRenderer())\n```'
    use_mesh_color: bool = False
    'use_mesh_color() -> bool\n\nUse the intrinsic mesh color(s) instead of the particle color when rendering particles of this type.\nThis option only has an effect if a user-defined shape :py:attr:`mesh` has been assigned to this particle type, e.g., by calling\nthe :py:meth:`.load_shape` method.\n\nDefault: ``False``'
    vdw_radius: float = 0.0
    'vdw_radius() -> float\n\nThe van der Waals radius of the particle type. This value is used by the :py:class:`CreateBondsModifier`\nto decide which pairs of particles are close enough to be connected by a bond. In contrast to the :py:attr:`radius` parameter,\nthe van der Waals radius does not affect the visual appearance of the particles of this type.\n\nDefault: ``0.0``'
    mesh: Optional[TriangleMesh] = None
    "mesh() -> Optional[ovito.data.TriangleMesh]\n\nThe :py:class:`TriangleMesh` object to be used as custom shape for rendering particles of this type.\nYou can either programmatically create a :py:class:`TriangleMesh` from a list of vertices and faces and assign it to\nthis field, or let the :py:meth:`load_shape` method read the shape mesh from a separate geometry file. Also some file readers (e.g. GSD and Aspherix) may generate the shape mesh automatically based on information found in the simulation file.\n\nThe :py:class:`ParticlesVis` element, which is responsible for visualizing a particle system, will render an instance\nof the mesh at each particle site, uniformly scaled by the particle's :py:attr:`radius`, translated by the coordinates taken from the ``Position`` particle\nproperty, and rotated by the quaternion transformation taken from the ``Orientation`` particle property.\n\nNote: This mesh will be ignored by the :py:class:`ParticlesVis` element unless the type's :py:attr:`shape` is set to\n``ParticlesVis.Shape.Mesh``.\n\nDefault: ``None``"

    def load_defaults(self) -> None:
        """Given the type's chemical :py:attr:`name`, which must have been set before invoking this method, initializes the type's :py:attr:`color`, :py:attr:`radius`, :py:attr:`vdw_radius` and :py:attr:`mass` fields with default values from OVITO's internal database of chemical elements."""
        ...

    def load_shape(self, filepath: str) -> None:
        """Assigns a user-defined shape to the particle type. Particles of this type will subsequently be rendered using the polyhedral :py:attr:`mesh` loaded from the given file. The method will automatically detect the format of the geometry file and supports standard file formats such as OBJ, STL and VTK that contain triangle meshes, see this table. 

The shape loaded from the geometry file will be scaled with the :py:attr:`radius` value set for this particle type or the per-particle value stored in the ``Radius`` particle property if present. The shape of each particle will be rendered such that its origin is located at the coordinates of the particle (``Position`` property). 

The following example script demonstrates how to load a user-defined shape for the first particle type (index 0) loaded from a LAMMPS dump file, which can be accessed through the `Property.types` list of the ``Particle Type`` particle property. 

```python
  pipeline = import_file("input/simulation.dump")
  pipeline.add_to_scene()
  
  types = pipeline.source.data.particles_.particle_types_
  types.type_by_id_(1).load_shape("input/tetrahedron.vtk")
  types.type_by_id_(1).highlight_edges = True
```"""
        ...

@dataclass(kw_only=True)
class BondType(ElementType):
    """Base: :py:class:`ovito.data.ElementType`

Represents a bond type. This class inherits all its fields from the :py:class:`ElementType` base class. 

You can enumerate the list of defined bond types by accessing the `bond_types` bond property object: 

```python
  bond_type_property = data.particles.bonds.bond_types
  for t in bond_type_property.types:
      print(t.id, t.name, t.color, t.radius)
```"""
    radius: float = 0.0
    'radius() -> float\n\nThis attribute controls the display radius of all bonds of this type.\n\nWhen set to zero, bonds of this type will be rendered using the standard width specified\nby the `BondsVis.radius` parameter.\nFurthermore, precedence is given to any per-bond widths assigned to the ``Width`` bond property if that property exists.\n\nDefault: ``0.0``'

class Property(DataObject, NDArray[Any]):
    """Base: :py:class:`ovito.data.DataObject`

A storage array for the values of one uniform property of particles, bonds, voxel grid cells, etc.
For example, the "*Position*" property of particles is represented by one :py:class:`Property` object storing
the xyz coordinates of all the particles.

:py:class:`Property` objects are always managed by a specific sub-type of the :py:class:`PropertyContainer` class, for example
:py:class:`Particles`, :py:class:`Bonds`, :py:class:`VoxelGrid`, or :py:class:`DataTable`. These container classes
allow to add and remove properties to the data elements they represent. The properties within the :py:class:`PropertyContainer` are accessed by name.
Here, for example, the particle property holding the particle coordinates::

    positions = data.particles['Position']

This :py:class:`Property` object behaves almost like a regular NumPy array. For example, you can access the value for the *i*-th element using array indexing::

    print('XYZ coordinates of first particle:', positions[0])
    print('z-coordinate of sixth particle:', positions[5,2])
    print(positions.shape)   # --> (data.particles.count, 3)

Since the "*Position*" standard property has three components, this :py:class:`Property` object is an array of shape (*N*,3).
Properties can be either vectorial or scalar, and all elements of a property share the same data type.

If you want to set or modify the values stored in a property array, make sure you are working with a modifiable version of the :py:class:`Property` object
by employing the :pyunderscore notation, e.g.::

    modifiable_positions = data.particles_['Position_']
    modifiable_positions[0] += (2.0, 0.0, 0.5)

.. rubric:: Data types

The values of a property are stored using one of the following data types. The :py:attr:`data_type` attribute of a
:py:class:`Property` object tells you which one is in use, while its ``dtype`` attribute reports the equivalent NumPy data type:

.. list-table::
    :widths: 20 30 50
    :header-rows: 1

    * - :py:attr:`data_type` value
      - ``dtype`` value
      - Description
    * - ``Property.DataType.Int8``
      - ``numpy.int8``
      - 8-bit signed integer. Used by properties representing boolean flags, e.g. "*Selection*".
    * - ``Property.DataType.Int32``
      - ``numpy.int32``
      - 32-bit signed integer. Used by typed properties, e.g. "*Particle Type*".
    * - ``Property.DataType.Int64``
      - ``numpy.int64``
      - 64-bit signed integer. Used by properties storing unique numeric identifiers, e.g. "*Particle Identifier*".
    * - ``Property.DataType.Float32``
      - ``numpy.float32``
      - Single-precision floating-point number.
    * - ``Property.DataType.Float64``
      - ``numpy.float64``
      - Double-precision floating-point number. The default type for continuous physical quantities, e.g. "*Position*".
    * - ``Property.DataType.String``
      - ``numpy.dtypes.StringDType()``
      - Variable-length text string. Note the restrictions that apply to this data type.

For predefined standard properties, the data type is fixed and determined by OVITO.
For user-defined properties, you select it with the *dtype* parameter of :py:meth:`PropertyContainer.create_property`,
or you let OVITO infer it from the *data* array passed to that method::

    data.particles_.create_property('Local Energy', dtype=float)

.. rubric:: String properties

Properties of data type ``str`` store one text string per element. Several of OVITO's file readers produce such properties --
for example the "*Atom Name*" property read from PDB, MOL2, CIF, Gromacs, and MedeA SCI files. You can also create them
yourself with :py:meth:`PropertyContainer.create_property`::

    names = data.particles_.create_property('Atom Name', dtype=str, data=['C1', 'C2', 'O1'])

Because the strings have variable length, a string property is *not* backed by a contiguous binary buffer and therefore
cannot be exposed as a NumPy array. Any operation that would require an array view raises an error.
The array-like attributes that need no buffer remain available: ``shape``, ``ndim`` and ``dtype``, as does ``len()``.

Instead of an array view, string properties provide indexed element access. Indexing with an integer yields a single
:py:class:`python:str`, whereas a slice or an ellipsis yields a list of strings::

    >>> names = data.particles['Atom Name']
    >>> names[0]
    'C1'
    >>> names[...]
    ['C1', 'C2', 'O1']
    >>> names.shape, names.ndim, names.dtype
    ((3,), 1, StringDType())

Individual values are modified by assigning to an integer index, again using the
:pyunderscore notation to obtain a modifiable property::

    data.particles_['Atom Name_'][0] = 'C1a'

Sliced assignment and NumPy-style advanced indexing (boolean masks or index arrays) are not supported for string properties.
Element access furthermore requires the property to be scalar: a string property with more than one vector component
can be created and stored, but its values can neither be read nor written from Python.

.. rubric:: Typed properties

In OVITO, the standard particle property "*Particle Type*" contains the types of particles encoded as integer values, e.g.::

    >>> data = pipeline.compute()
    >>> type_property = data.particles['Particle Type']
    >>> print(type_property[...])
    [2 1 3 ..., 2 1 2]

The property array stores numeric *type identifiers* denoting each particle's chemical type (e.g. 1=Cu, 2=Ni, 3=Fe, etc.).
The mapping of unique numeric IDs to human-readable type *names* is found in the :py:attr:`types` list, which is
attached to the :py:class:`Property` array, making it a so-called *typed property*. This list contains one :py:class:`ParticleType` descriptor per unique numerical type,
specifying its human-readable :py:attr:`~ElementType.name` as well as other attributes such as display :py:attr:`~ElementType.color`,
:py:attr:`~ParticleType.radius` and :py:attr:`~ParticleType.mass`::

    >>> for t in type_property.types:
    ...     print(t.id, t.name, t.color, t.radius)
    ...
    1 Cu [0.188 0.313 0.972] 0.74
    2 Ni [0.564 0.564 0.564] 0.77
    3 Fe [1 0.050 0.050] 0.74

Numeric type IDs typically start at 1 and form a consecutive sequence as in the example above. But they don't have to.
The descriptors may be listed in any order and their numeric IDs may be arbitrary integers.
Thus, in general, it is not valid to directly use a numeric type ID as an index into the :py:attr:`types` list.
Instead, the :py:meth:`.type_by_id` method should be used to look up the :py:class:`ParticleType` descriptor for a given numeric ID::

    >>> for i,t in enumerate(type_property): # loop over the type IDs of particles
    ...     print(f"Atom {i} is of type {type_property.type_by_id(t).name}")
    ...
    Atom 0 is of type Ni
    Atom 1 is of type Cu
    Atom 2 is of type Fe
    Atom 3 is of type Cu

Similarly, a :py:meth:`.type_by_name` method exists that allows you to look up a :py:attr:`ParticleType` from the :py:attr:`types` list by name.
For example, to count the number of Fe atoms in a system, we first need to determine the numeric ID of the type "Fe" and then count the
number of occurrences of the value in the :py:class:`Property` array::

    >>> Fe_type_id = type_property.type_by_name('Fe').id  # Determine numeric ID of the 'Fe' type.
    >>> numpy.count_nonzero(type_property == Fe_type_id)  # Count particles having that type ID.
    957

Note that the data model supports multiple type classifications per particle. For example, while the "*Particle Type*" standard particle property,
stores the chemical types of atoms (e.g. C, H, Fe, ...), the "*Structure Type*" property stores the structural lattice types computed for each atom
(e.g. FCC, BCC, ...). In other words, multiple *typed properties* can co-exist to define several orthogonal classifications, and each typed property maintains its separate list
of type descriptors in the associated :py:attr:`types` list.

New types can be added to a typed property either using :py:meth:`.add_type_id` or :py:meth:`.add_type_name`. Use the former method if it is important that
the new type gets a particular numeric ID (which must not collide with existing types in the :py:attr:`types` list). Use the latter method if
you don't care about the numeric ID and let the method automatically assign a unique ID to the new type."""

    class DataType(enum.Enum):
        Int8 = enum.auto()
        Int32 = enum.auto()
        Int64 = enum.auto()
        Float32 = enum.auto()
        Float64 = enum.auto()
        String = enum.auto()

    @property
    def component_count(self) -> int:
        """The number of vector components if this is a vector property; or 1 if this is a scalar property. 

This attribute is read-only. For predefined standard properties, OVITO automatically determines the vector component count. For user-defined properties you can specify the component count during creation with the :py:meth:`PropertyContainer.create_property` method."""
        ...

    @property
    def component_names(self) -> Sequence[str]:
        """The list of component names if this is a vectorial property. For example, the ``Position`` particle property has three components: ``['X', 'Y', 'Z']``. 

The number of elements in this list must always be equal to :py:attr:`component_count` or zero, in which case the property components are referenced by numeric index (1, 2, 3, ...).

This list is read-only. For predefined standard properties, OVITO automatically initializes the component names. For user-defined properties you may specify the component names during creation of the property with the :py:meth:`PropertyContainer.create_property` method."""
        ...

    @property
    def data_type(self) -> Property.DataType:
        """The data type of the values stored in this property array. The returned value is one of the constants of the ``Property.DataType`` enumeration, for example ``Property.DataType.Float64``. 

This attribute is read-only. The data type of a property is selected when the property is created, see the *dtype* parameter of :py:meth:`PropertyContainer.create_property`."""
        ...

    @property
    def name(self) -> str:
        """The name of the property -- a non-empty string. 

The name may contain spaces, digits, or special characters, but no dots, because ``.`` is used by OVITO as a delimiter for vector :py:attr:`component_names`."""
        ...

    @property
    def types(self) -> MutableSequence[ElementType]:
        """The list of :py:class:`ElementType` descriptors associated with this property if it is a typed property. 

A *typed property*, such as the :ref:`"Particle Type" property for particles <manual:scene_objects.particle_types>`, stores each particle's type information, for example its chemical type, in the form of a numeric *type ID* in a uniform data array. The :py:attr:`types` list represents a look-up table containing a descriptor for each unique numeric type, which maps the numeric ID to a corresponding human-readable type name, a chemical symbol for example. 

The :py:attr:`types` list consists of instances of the :py:class:`ElementType` class or one of its sub-classes, describing the individual types. For each unique type, :py:attr:`ElementType.id` specifies the numeric identifier and :py:attr:`ElementType.name` the human-readable name. Furthermore, :py:attr:`ElementType.color` specifies the color used for rendering elements of this type. 

Some *typed properties* in OVITO use a sub-class of :py:class:`ElementType` to associate additional information with each unique type. For example, the "`Particle Type`" property for :py:class:`Particles` uses :py:class:`ParticleType` descriptors. This descriptor class defines additional fields such as :py:attr:`ParticleType.radius` and :py:attr:`ParticleType.mass` for each type. 

If you want to manipulate the descriptors in the :py:attr:`types` list one by one, you should iterate over the :ref:`"underscore" version <underscore_notation>` of the list, :py:attr:`types_`, to automatically make all the type descriptors mutable. This shortcut implicitly invokes :py:meth:`make_mutable` on each :py:class:`ElementType`, making sure it can be modified without unexpected side effects: 

```python
  # Get a mutable reference to the "Particle Type" property array.
  type_property = data.particles_.particle_types_
  
  # Iterate over all defined ParticleType descriptors and modify their radius.
  for t in type_property.types_:
      t.radius *= 0.5
```"""
        ...

    @property
    def types_(self) -> MutableSequence[ElementType]:
        ...

    def type_by_id(self, id: int, require: bool=True) -> ElementType:
        """Looks up a :py:class:`ElementType` from this property's :py:attr:`types` list based on its unique numeric ID.
Depending on the parameter *require*, raises a ``KeyError`` or returns ``None`` if no type with the numeric ID exists.


Usage example:

```python
  # Iterate over the numeric per-particle types stored in the 'Structure Type'
  # particle property array and print the corresponding human-readable type names:
  for index, type_id in enumerate(data.particles.structure_types):
      print("Atom {} is a {} atom".format(
          index, data.particles.structure_types.type_by_id(type_id).name))
```

An :ref:`"underscore version" <underscore_notation>` of the method is available, which should be used whenever you intend to modify the returned type object. :py:meth:`!type_by_id_()` implicitly calls :py:meth:`make_mutable` on the :py:class:`ElementType` to make sure it can be changed without unexpected side effects:

```python
  # Give some names to the numeric atom types from a LAMMPS simulation:
  data.particles_.particle_types_.type_by_id_(1).name = 'C'
  data.particles_.particle_types_.type_by_id_(2).name = 'H'
```"""
        ...

    def type_by_id_(self, id: int, require: bool=True) -> ElementType:
        ...

    def type_by_name(self, name: str, require: bool=True) -> ElementType:
        """Looks up an :py:class:`ElementType` from this property's :py:attr:`types` list based on its :py:attr:`~ElementType.name`.
If multiple types exist with the same name, the first one in the list is returned. 
Depending on the parameter *require*, raises a ``KeyError`` or returns ``None`` if there isn't a type with that name. 


Usage example:

```python
  # Look up the numeric ID of atom type Si and count how many times it appears in the 'Particle Type' array
  id_Si = data.particles.particle_types.type_by_name('Si').id
  Si_atom_count = numpy.count_nonzero(data.particles.particle_types == id_Si)
```

An :ref:`"underscore version" <underscore_notation>` of the method is available, which should be used whenever you intend to modify the returned type object. :py:meth:`!type_by_name_()` implicitly calls :py:meth:`make_mutable` on the :py:class:`ElementType` to make sure it can be changed without unexpected side effects:

```python
  # Rename a structure type created by the PTM modifier:
  data.particles_.structure_types_.type_by_name_('Hexagonal diamond').name = 'Wurtzite'
```"""
        ...

    def type_by_name_(self, name: str, require: bool=True) -> ElementType:
        ...

    def add_type_id(self, id: int, container: PropertyContainer, name: str='') -> ElementType:
        """Creates a new numeric :py:class:`ElementType` with the given numeric *id* and an optional human-readable *name* and adds it to this property's :py:attr:`types` list. If the list already contains an existing element type with the same numeric id, that existing type will be returned (without updating its name).

Additionally, you must specify the :py:class:`PropertyContainer` containing this property object as second parameter, because it determines the kind of :py:class:`ElementType` to create. For example, when calling :py:meth:`add_type_id` on the property "`Particle Type`" of a :py:class:`Particles` container, this method will create a new :py:class:`ParticleType` object -- a specific sub-class of the more general :py:class:`ElementType` class. Furthermore, if *name* matches one of the standard type names predefined for that particle property, e.g., a chemical symbol in case of the "`Particle Type`" property, the type's display color, radius, and mass will be pre-configured (as if :py:meth:`load_defaults` was called). 

```python
  type_property = data.particles_.create_property("Particle Type")
  type_1 = type_property.add_type_id(1, data.particles, name="A")
  type_2 = type_property.add_type_id(2, data.particles, name="B")
  
  # Configure visual appearance of the two ParticleTypes
  type_1.radius = 0.9; type_1.color = (1.0, 0.0, 0.0)
  type_2.radius = 1.2; type_2.color = (0.0, 0.0, 1.0)
  
  # Randomly assign types "A" (1) or "B" (2) to the particles
  type_property[...] = numpy.random.randint(low=1, high=1+2, size=data.particles.count)
```"""
        ...

    def add_type_name(self, name: str, container: PropertyContainer) -> ElementType:
        """Creates a new :py:class:`ElementType` with the given human-readable :py:attr:`name` and adds it to this property's :py:attr:`types` list. A unique numeric :py:attr:`id` will be automatically assigned to the type (starting at 1). If the list already contains an existing element type of the same name, that existing type will be returned. 

Additionally, you must specify the :py:class:`PropertyContainer` containing this property object as second parameter, because it determines the kind of :py:class:`ElementType` to create. For example, when calling :py:meth:`add_type_name` on the property "`Particle Type`" of a :py:class:`Particles` container, this method will create a new :py:class:`ParticleType` object -- a specific sub-class of the more general :py:class:`ElementType` class. Furthermore, if *name* matches one of the standard type names predefined for that particle property, e.g., a chemical symbol in case of the "`Particle Type`" property, the type's display color, radius, and mass will be pre-configured (as if :py:meth:`load_defaults` was called). 

```python
  type_property = data.particles_.create_property("Particle Type")
  type_Au = type_property.add_type_name("Au", data.particles)
  type_Ag = type_property.add_type_name("Ag", data.particles)
  
  # Randomly assign types "Au" or "Ag" to the particles
  type_property[...] = numpy.random.choice([type_Au.id, type_Ag.id], size=data.particles.count)
```"""
        ...

    def remove_type_id(self, id: int) -> None:
        """Removes the numeric :py:class:`ElementType` with the given numeric *id* from this property's :py:attr:`types` list. If the list does not contain an element type with the specified id, this method raises a ``KeyError``."""
        ...

@dataclass(kw_only=True)
class PropertyContainer(DataObject, Mapping[str, Property]):
    """Base: :py:class:`ovito.data.DataObject`

A dictionary-like object storing a set of :py:class:`Property` objects.

It implements the ``collections.abc.Mapping`` interface. That means it can be used
like a standard read-only Python ``dict`` object to access the properties by name, e.g.:

```python
  data = pipeline.compute()
  
  positions = data.particles['Position']
  has_selection = 'Selection' in data.particles
  name_list = data.particles.keys()
```

New properties are typically added to a container with a call to :py:meth:`.create_property` as described here.
To remove an existing property from a container, you can use Python's ``del`` statement:

```python
  del data.particles_['Selection']
```

OVITO has several concrete implementations of the abstract :py:class:`PropertyContainer` interface:

    * :py:class:`Particles`
    * :py:class:`Bonds`
    * :py:class:`VoxelGrid`
    * :py:class:`DataTable`
    * :py:class:`Lines`
    * :py:class:`Vectors`
    * :py:attr:`SurfaceMesh.vertices`
    * :py:attr:`SurfaceMesh.faces`
    * :py:attr:`SurfaceMesh.regions`"""
    count: int = 0
    'count() -> int\n\nThe number of data elements in this container, e.g. the number of particles. This value is always equal to the lengths of all :py:class:`Property` arrays managed by this container.'
    title: str = ''
    'title() -> str\n\nThe title of the data object under which it appears in the user interface of OVITO.'

    def __len__(self) -> int:
        ...

    def __iter__(self) -> Iterator[str]:
        ...

    def __getitem__(self, key: str) -> Union[Property, numpy.ndarray]:
        ...

    def __delitem__(self, key: str) -> None:
        ...

    def append(self, new_row: Mapping[str, numbers.Number | Sequence[numbers.Number] | ArrayLike], strict: bool=True) -> int:
        """Appends a new row of data to the end of the :py:class:`PropertyContainer` by incrementing the :py:attr:`count` by one.
The data of the new element is provided as a dictionary of key-value pairs, where the keys are the property names in the container and the values are the corresponding property values.
If the container type does not support appending elements, a ``RuntimeError`` is raised.

:param new_row: A dictionary mapping property names to their values for the new row.
:param strict: If set to ``True`` (default), a strict checking of the keys in *new_row* is performed.
:returns: The zero-based index of the newly added row.

When the `strict` parameter is set to ``True`` (default), the dictionary keys must match the container's property names exactly.
In other words, you have to specify a value for every existing property in the container (and *only* those properties).
If any keys are missing or extra, a ``KeyError`` is raised.

When `strict` is set to ``False``:

    * Missing keys are ignored, and the corresponding property values are initialized to 0.
    * For extra keys, new properties are added to the container and initialized to 0 for all existing elements.
    * The *dtype* and number of vector components of new properties are inferred from the provided values.

Example usage of the :py:meth:`append` method:

```python
  print(sorted(data.particles.keys()))
  # >>> ['Force', 'Particle Identifier', 'Particle Type', 'Position', 'peatom']
  print(data.particles.count)
  # >>> 7071
  data.particles_.append({"Force": (1.2, -1.2, 3.1),
                          "Particle Identifier": 7072,
                          "Particle Type": 2,
                          "Position": (31.2, 33.0, 29.8),
                          "peatom": -4.32})
  print(data.particles.count)
  # -> 7072
  data.particles_.append({"Charge": -0.33, "Gradient 2D": (1.3, 1.2)}, strict=False)
  print(sorted(data.particles.keys()))
  # >>> ['Charge', 'Force', 'Gradient 2D', 'Particle Identifier', 'Particle Type', 'Position', 'peatom']
```"""
        ...

    def create_property(self, name: str, dtype: Optional[DTypeLike]=None, components: Optional[Union[int, Sequence[str]]]=None, data: Optional[ArrayLike]=None) -> Property:
        """Adds a new property with the given *name* to the container and optionally initializes its element-wise values with *data*.
If a property with the given *name* already exists in the container, that existing property is returned
(after replacing its contents with *data* if provided).

:param name: Name of the property to create.
:param data: Optional array with initial values for the new property.
                The size of the array must match the element :py:attr:`count` of the container
                and the shape must be consistent with the number of components of the property to be created.
:param dtype: Data type of the user-defined property. Must be either :py:class:`python:int`, :py:class:`python:float`, :py:attr:`numpy:numpy.int8`, :py:attr:`numpy:numpy.int32`, :py:attr:`numpy:numpy.int64`, :py:attr:`numpy:numpy.float32`, :py:attr:`numpy:numpy.float64`, or :py:class:`python:str`.
:param components: Number of vector components of the user-defined property (1 if not specified) or a list of strings specifying the property's :py:attr:`component_names`.
:returns: The new :py:class:`Property` object.

You can create *standard* and *user-defined* properties in a container.
A *standard* property with a prescribed data layout is automatically created if *name* matches one of the
predefined names for the container type:

  * :py:class:`Particles`: List of standard particle properties
  * :py:class:`Bonds`: List of standard bond properties
  * `SurfaceMesh.vertices`: List of standard mesh vertex properties
  * `SurfaceMesh.faces`: List of standard mesh face properties
  * `SurfaceMesh.regions`: List of standard mesh region properties
  * :py:class:`Vectors`: List of standard vector properties
  * :py:class:`Lines`: List of standard line properties

The length of the provided *data* array must match the number of elements in the container, which is given by :py:attr:`PropertyContainer.count`.
If the property to be created is a vectorial property (having :math:`M>1` components), the initial data
array should be of shape :math:`(N,M)` if provided:

```python
  colors = numpy.random.random_sample(size=(data.particles.count, 3))
  data.particles_.create_property('Color', data=colors)
```

In general, however, *data* may be any value that is broadcastable to the array dimensions of the standard property (e.g. a uniform value).

If you don't specify the function argument *data*, OVITO will automatically initialize the property elements with sensible default
values (0 in most cases). Subsequently, you can set the property values for all or some of the elements:

```python
  prop = data.particles_.create_property('Color')
  prop[...] = numpy.random.random_sample(size=prop.shape)
```

To create a *user-defined* property, specify a non-standard property name:

```python
  values = numpy.arange(0, data.particles.count, dtype=int)
  data.particles_.create_property('My Integer Property', data=values)
```

In this case, the data type and the number of vector components of the new property are inferred from
the provided *data* array. Specifying a one-dimensional array creates a scalar property whereas
a two-dimensional array creates a vectorial property.

If a property of the same name already exists in the container, it gets replaced with a modifiable copy if necessary.
Its contents get overwritten with the *data* array while maintaining all other attributes of the existing property, e.g. its :py:attr:`types` list.

When creating a user-defined property, the *dtype* and *components* parameters may also be specified explicitly
if you are going to fill in the property values in a second step:

```python
  prop = data.particles_.create_property('My Vector Property', dtype=float, components=3)
  prop[...] = numpy.random.random_sample(size = prop.shape)
```

If desired, the components of the user-defined vector property can be given names, e.g. ``components=('X', 'Y', 'Z')``.

Note: If you're creating new empty :py:class:`PropertyContainer`, its initial element :py:attr:`count` is 0.
In this state, and only then, the :py:meth:`create_property` method allows you to initialize the count while adding the very first property
by providing a data array of the desired length:

```python
  # Creating an empty Particles container to begin with:
  particles = Particles()
  
  # Create 10 particles with random xyz coordinates:
  xyz = numpy.random.random_sample(size=(10,3))
  particles.create_property('Position', data=xyz)
  
  assert particles.count == len(xyz)
```

All subsequently added properties must have a matching length."""
        ...

    def delete_elements(self, mask: ArrayLike) -> None:
        """Deletes a subset of the elements from this container. The elements to be deleted must be specified in terms
of a 1-dimensional mask array having the same length as the container (see :py:attr:`count`).
The method will delete those elements whose corresponding mask value is non-zero, i.e., the ``i``-th element
will be deleted if ``mask[i]!=0``.

For example, to delete all currently selected particles, i.e., the subset of particles whose ``Selection`` property
is non-zero, one would simply write:

```python
  data.particles_.delete_elements(data.particles['Selection'])
```

The effect of this statement is the same as for applying the :py:class:`DeleteSelectedModifier` to the particles list."""
        ...

    def delete_indices(self, indices: ArrayLike) -> None:
        """Deletes a subset of the elements from this container. The elements to be deleted must be specified in terms
of a sequence of indices, all in the range 0 to :py:attr:`count`-1. The method accepts any type of iterable object,
including sequence types, arrays, and generators.

For example, to delete every other particle, one could use Python's :py:class:`python:range` function to generate
all even indices up to the length of the particle container:

```python
  data.particles_.delete_indices(range(0, data.particles.count, 2))
```"""
        ...

    def get(self, name: str, require: bool=False) -> Union[Property, numpy.ndarray, None]:
        """Retrieves a property array from this container. 

:param name: The name of the property to retrieve.
:param require: If ``True``, the method raises a `KeyError` in case the property does not exist in the container. If ``False``, the method returns ``None`` in the not-found case.
:returns: The requested :py:class:`Property`, or ``None`` if *require=False* and the property is not present in the container."""
        ...

    def get_(self, name: str, require: bool=False) -> Union[Property, numpy.ndarray, None]:
        ...

@dataclass(kw_only=True)
class SimulationCell(DataObject, NDArray[numpy.float64]):
    """Base: :py:class:`ovito.data.DataObject`

This object stores the geometric shape and boundary conditions of the simulation box.
Typically there is exactly one :py:class:`SimulationCell` object in a :py:class:`DataCollection`, which is accessible through 
the :py:attr:`~DataCollection.cell` field:

```python
  data = pipeline.compute()
  print(data.cell[...])   # Use [...] to cast SimulationCell object to a NumPy array
```

.. rubric:: The cell matrix

The geometry of the simulation cell is encoded as a 3x4 matrix :math:`\\mathbf{M}`. 
The first three columns :math:`\\mathbf{a}`, :math:`\\mathbf{b}`, :math:`\\mathbf{c}` of the matrix are the vectors spanning the three-dimensional parallelepiped
in Cartesian space. The fourth column specifies the Cartesian coordinates of the cell's origin :math:`\\mathbf{o}` within the global
simulation coordinate system:

.. math::

   \\mathbf{M} = \\begin{pmatrix}
      a_x & b_x & c_x & o_x \\\\
      a_y & b_y & c_y & o_y \\\\
      a_z & b_z & c_z & o_z \\\\
      \\end{pmatrix}

The cell matrix is represented by a two-dimensional NumPy array of shape (3,4) using row-major storage order:

```python
  a = data.cell[:,0]
  b = data.cell[:,1]
  c = data.cell[:,2]
  o = data.cell[:,3]
```

The :py:attr:`is2D` flag of the simulation cell indicates whether the system is two-dimensional. 
The cell matrix of a 2d system also has the 3x4 shape, but the cell vector :math:`\\mathbf{c}` and the 
last row of the cell matrix are ignored by many computations in OVITO if the system is marked as 2d.

.. rubric:: Periodic boundary conditions

The :py:attr:`pbc` field stores a tuple of three Boolean flags that indicate for each 
cell vector whether the system is periodic in that direction or not. OVITO uses that information
in various computations. If the system is two-dimensional, the value of the third pbc flag
is ignored.

.. rubric:: Modifying the simulation cell

When you modify the entries of the cell matrix, make sure you use the :pyunderscore notation 
to request a modifiable version of the :py:class:`SimulationCell` object: 

```python
  # Make cell twice as large along the Y direction by scaling the second cell vector:
  data.cell_[:,1] *= 2.0
```

Reset the simulation cell to an orthogonal box :math:`(L_x, L_y, L_z)` centered at the origin:

```python
  lx = 20.0; ly = 10.0; lz = 8.0
  data.cell_[:,0] = (lx, 0, 0)
  data.cell_[:,1] = (0, ly, 0)
  data.cell_[:,2] = (0, 0, lz)
  data.cell_[:,3] = numpy.dot((-0.5, -0.5, -0.5), data.cell[:3,:3])
  data.cell_.pbc = (True, True, True)
```

.. rubric:: Conversion between Cartesian and reduced coordinates

Given a point in 3d space, :math:`\\mathbf{p}=(x, y, z)`, expressed in coordinates of the Cartesian simulation system, you can compute
the corresponding reduced cell coordinates by extending the point to a quadruplet :math:`(x, y, z, 1)` and multiplying it 
with the :py:attr:`.inverse` cell matrix :math:`\\mathbf{M}^*`:

```python
  p_cartesian = (x, y, z)
  p_reduced   = cell.inverse @ numpy.append(p_cartesian, 1.0)  # @-operator is shorthand for numpy.matmul()
```

This effectively performs an affine transformation. The reverse transformation back to Cartesian coordinates in the global simulation system 
works in the same way. The following operation converts a 3d point from reduced cell coordinates to simulation coordinates:

```python
  p_reduced   = (xs, ys, zs)
  p_cartesian = cell @ numpy.append(p_reduced, 1.0)  # @-operator is shorthand for numpy.matmul()
```

Transforming vectors (as opposed to points) between Cartesian and reduced cell coordinates works somewhat differently, because vectors 
are not affected by the translation of the simulation cell, i.e., when the cell's origin does not coincide with the origin of the global simulation coordinate system.
A vector :math:`\\mathbf{v}=(x, y, z)` should thus be amended with a zero, :math:`(x, y, z, 0)`, before applying the 3x4 transformation matrix
to ignore the translational component:

```python
  v_cartesian     = (vx, vy, vz)
  v_reduced       = cell.inverse @ numpy.append(v_cartesian, 0.0)
  v_cartesian_out = cell @ numpy.append(v_reduced, 0.0)
  assert numpy.allclose(v_cartesian_out, v_cartesian)
```

The operations described above transform individual 3d points or vectors. In case you have to transform an entire array of points or vectors,
for example the list of atomic positions, it is most efficient to apply the transformation to all elements of the array at once.
Here is how you can do the affine transformation back and forth between Cartesian and reduced coordinates for an array:

```python
  cartesian_positions     = data.particles.positions
  reduced_positions       = (cell.inverse[0:3,0:3] @ cartesian_positions.T).T + cell.inverse[0:3,3]
  cartesian_positions_out = (cell[0:3,0:3] @ reduced_positions.T).T + cell[0:3,3]
  assert numpy.allclose(cartesian_positions_out, cartesian_positions)
```

When transforming an array of vectors, leave out the translation term and perform just the linear transformation (3x3 matrix-vector multiplication).

.. rubric:: Visual representation

Each :py:class:`SimulationCell` object has an attached :py:class:`SimulationCellVis` element,
which controls the visual appearance of the wireframe box in rendered images. It can be accessed via the
:py:attr:`~DataObject.vis` attribute inherited from the :py:class:`DataObject` base class:

```python
  data = pipeline.compute()
  # Change display color of simulation cell to red:
  data.cell.vis.rendering_color = (1.0, 0.0, 0.0)
  # Or turn off the display of the cell completely:
  data.cell.vis.enabled = False
```"""

    def __new__(cls, **kwargs: Any) -> SimulationCell:
        """Create and return a new object.  See help(type) for accurate signature."""
        ...
    is2D: bool = False
    'is2D() -> bool\n\nSpecifies whether the system is two-dimensional (instead of three-dimensional). For two-dimensional systems, the third :py:attr:`pbc` flag and the cell vector :math:`\\mathbf{c}` are typically ignored. \n\nDefault: ``False``'
    pbc: Tuple[bool, bool, bool] = (False, False, False)
    "A tuple of three Boolean flags specifying whether periodic boundary conditions are enabled along the\ncell's three spatial directions.\n\nDefault: ``(False, False, False)``"
    vis: ovito.vis.SimulationCellVis = ovito.vis.SimulationCellVis()
    'vis() -> Optional[ovito.vis.DataVis]\n\nThe :py:class:`DataVis` element currently associated with this data object. This object is responsible for\nvisually rendering the stored data. If set to ``None``, the data object remains non-visual and does not appear in\nrendered images or viewports. Additionally, note that the same :py:class:`DataVis` element may be assigned to\nmultiple data objects to synchronize their visual appearance.\n\nSee the :py:mod:`ovito.vis` module for a list of visual element types.'

    @property
    def volume(self) -> float:
        """Read-only property computing the volume of the three-dimensional simulation cell.
The returned value is equal to the absolute determinant of the 3x3 submatrix formed by the three cell vectors, i.e. the scalar triple product :math:`V=|(\\mathbf{a} \\times \\mathbf{b}) \\cdot \\mathbf{c}|`: 

```python
  assert cell.volume == abs(numpy.linalg.det(cell[0:3,0:3]))
```"""
        ...

    @property
    def volume2D(self) -> float:
        """Read-only property computing the area of the two-dimensional simulation cell (see :py:attr:`is2D`).
The returned value is equal to the magnitude of the cross-product of the first two cell vectors, i.e. :math:`V_{\\mathrm{2d}} = |\\mathbf{a} \\times \\mathbf{b}|`:

```python
  assert cell.volume2D == numpy.linalg.norm(numpy.cross(cell[:,0], cell[:,1]))
```"""
        ...

    @property
    def inverse(self) -> numpy.ndarray:
        """Read-only property returning the reciprocal cell matrix :math:`\\mathbf{M}^*` - an array of shape (3,4):

.. math:: \\mathbf{M}^* = \\begin{bmatrix} \\mathbf{a}^* & \\mathbf{b}^* & \\mathbf{c}^* & \\mathbf{o}^* \\end{bmatrix}

with the real-space cell volume :math:`V = (\\mathbf{a} \\times \\mathbf{b}) \\cdot \\mathbf{c}` and reciprocal cell vectors given by

.. math::

   \\mathbf{a}^* = \\frac{\\mathbf{b} \\times \\mathbf{c}}{V} \\qquad
   \\mathbf{b}^* = \\frac{\\mathbf{c} \\times \\mathbf{a}}{V} \\qquad
   \\mathbf{c}^* = \\frac{\\mathbf{a} \\times \\mathbf{b}}{V} \\qquad
   \\mathbf{o}^* = -\\begin{pmatrix} \\mathbf{a}^* \\; \\mathbf{b}^* \\; \\mathbf{c}^* \\end{pmatrix} \\mathbf{o} \\mathrm{.}"""
        ...

    def __getitem__(self, idx: Any) -> NDArray[numpy.float64]:
        ...

    def delta_vector(self, ra: ArrayLike, rb: ArrayLike, return_pbcvec: bool=False) -> NDArray[numpy.float64]:
        """Computes the vector connecting two points :math:`r_a` and :math:`r_b` in a periodic simulation cell by applying the minimum image convention.

The method starts by computing the 3d vector :math:`{\\Delta} = r_b - r_a` for two points :math:`r_a` and :math:`r_b`, which may be located in different images
of the periodic simulation cell. The `minimum image convention <https://en.wikipedia.org/wiki/Periodic_boundary_conditions>`_
is then applied to obtain the new vector :math:`{\\Delta'} = r_b' - r_a`, where the original point :math:`r_b` has been replaced by the periodic image
:math:`r_b'` that is closest to :math:`r_a`, making the vector :math:`{\\Delta'}` as short as possible (in reduced coordinate space).
:math:`r_b'` is obtained by translating :math:`r_b` an integer number of times along each of the three cell directions:
:math:`r_b' = r_b - H*n`, with :math:`H` being the 3x3 cell matrix and :math:`n` being a vector of three integers that are chosen by the
method such that :math:`r_b'` is as close to :math:`r_a` as possible.

Note that the periodic image convention is applied only along those cell directions for which
periodic boundary conditions are enabled (see :py:attr:`pbc` property). For other directions
no shifting is performed, i.e., the corresponding components of :math:`n = (n_x,n_y,n_z)` will always be zero.

The method is able to compute the results for either an individual pair of input points or for two *arrays* of input points. In the latter case,
i.e. if the input parameters *ra* and *rb* are both 2-D arrays of shape *Nx3*, the method returns a 2-D array containing
*N* output vectors. This allows applying the minimum image convention to a large number of point pairs in one function call.

The option *return_pbcvec* lets the method return the vector :math:`n` introduced above as an additional output.
The components of this vector specify the number of times the image point :math:`r_b'` needs to be shifted along each of the three cell directions
in order to bring it onto the original input point :math:`r_b`. In other words, it specifies the number of times the
computed vector :math:`{\\Delta} = r_b - r_a` crosses a periodic boundary of the cell (either in positive or negative direction).
For example, the PBC shift vector :math:`n = (1,0,-2)` would indicate that, in order to get from input point :math:`r_a` to input point :math:`r_b`, one has to cross the
cell boundaries once in the positive x-direction and twice in the negative z-direction. If *return_pbcvec* is true,
the method returns the tuple (:math:`{\\Delta'}`, :math:`n`); otherwise it returns just :math:`{\\Delta'}`.
Note that the vector :math:`n` computed by this method can be used, for instance, to correctly initialize the `Bonds.pbc_vectors`
property for newly created bonds that cross a periodic cell boundary.

:param ra: The Cartesian xyz coordinates of the first input point(s). Either a 1-D array of length 3 or a 2-D array of shape (*N*,3).
:param rb: The Cartesian xyz coordinates of the second input point(s). Must have the same shape as *ra*.
:param return_pbcvec: If true, also returns the vector :math:`n`, which specifies how often the vector :math:`(r_b' - r_a)` crosses the periodic cell boundaries.
:returns: The vector :math:`{\\Delta'}` and, optionally, the vector :math:`n`.

Note that there exists also a convenience method `Particles.delta_vector()`,
which should be used in situations where :math:`r_a` and :math:`r_b` are the coordinates of two particles in the simulation cell."""
        ...

    def wrap_point(self, pos: ArrayLike) -> NDArray[numpy.float64]:
        """Maps a point into the primary image of the periodic simulation cell.

This function returns the coordinates of *pos* wrapped into the primary simulation cell image. *pos* must be a 3-D Cartesian point or an array of such points, which will be processed all at once by the method. Points that are already inside the simulation cell are returned as-is. The wrapping only takes place along those simulation cell directions which have the :py:attr:`pbc` flag set.

To find the shortest distance or vector between two points while accounting for periodic boundary conditions, use the :py:meth:`delta_vector` method instead.

:param pos: The Cartesian xyz coordinates of the input point(s). This can be either a 1-D array of length 3, or a 2-D array of shape (*N*, 3).
:return: The wrapped coordinates - returned as an array with the same shape as the input"""
        ...

@dataclass(kw_only=True)
class Particles(PropertyContainer):
    """Base: :py:class:`ovito.data.PropertyContainer`

This object stores a system of particles and their properties. Additional things which
are typically associated with molecular systems, e.g. :py:attr:`.bonds`, :py:attr:`.angles`, etc. are stored
in corresponding sub-objects.

A :py:class:`Particles` object is usually part of a :py:class:`DataCollection` where it can
be found via the :py:attr:`DataCollection.particles` property.

The total number of particles is specified by the :py:attr:`~PropertyContainer.count` attribute, which
the :py:class:`Particles` class inherits from its :py:class:`PropertyContainer` base class.

Particles are usually associated with a set of properties, e.g. position, type, velocity. Each of the properties is represented by a
separate :py:class:`Property` data object, which is basically an array of values, one for each particle in the system.
A particle property is identified by its unique name and can be looked up via the dictionary interface of the
:py:class:`PropertyContainer` base class. OVITO predefines a set of *standard properties*,
which have a fixed data layout, meaning, and role:

=================================================== ========== ===================================================
Standard property name                              Data type  Component names
=================================================== ========== ===================================================
:guilabel:`Angular Momentum`                        float64    X, Y, Z
:guilabel:`Angular Velocity`                        float64    X, Y, Z
:guilabel:`Aspherical Shape`                        float32    X, Y, Z
:guilabel:`Centrosymmetry`                          float64
:guilabel:`Charge`                                  float64
:guilabel:`Cluster`                                 int64
:guilabel:`Color`                                   float32    R, G, B
:guilabel:`Coordination`                            int32
:guilabel:`Deformation Gradient`                    float64    XX, YX, ZX, XY, YY, ZY, XZ, YZ, ZZ
:guilabel:`Dipole Magnitude`                        float64
:guilabel:`Dipole Orientation`                      float64    X, Y, Z
:guilabel:`Displacement Magnitude`                  float64
:guilabel:`Displacement`                            float64    X, Y, Z
:guilabel:`DNA Strand`                              int32
:guilabel:`Elastic Deformation Gradient`            float64    XX, YX, ZX, XY, YY, ZY, XZ, YZ, ZZ
:guilabel:`Elastic Strain`                          float64    XX, YY, ZZ, XY, XZ, YZ
:guilabel:`Force`                                   float64    X, Y, Z
:guilabel:`Kinetic Energy`                          float64
:guilabel:`Mass`                                    float64
:guilabel:`Molecule Identifier`                     int64
:guilabel:`Molecule Type`                           int32
:guilabel:`Nucleobase`                              int32
:guilabel:`Nucleotide Axis`                         float64    X, Y, Z
:guilabel:`Nucleotide Normal`                       float64    X, Y, Z
:guilabel:`Orientation`                             float32    X, Y, Z, W
:guilabel:`Particle Identifier`                     int64
:guilabel:`Particle Type`                           int32
:guilabel:`Periodic Image`                          int32      X, Y, Z
:guilabel:`Position`                                float64    X, Y, Z
:guilabel:`Potential Energy`                        float64
:guilabel:`Radius`                                  float32
:guilabel:`Rotation`                                float64    X, Y, Z, W
:guilabel:`Selection`                               int8
:guilabel:`Spin`                                    float64
:guilabel:`Strain Tensor`                           float64    XX, YY, ZZ, XY, XZ, YZ
:guilabel:`Stress Tensor`                           float64    XX, YY, ZZ, XY, XZ, YZ
:guilabel:`Stretch Tensor`                          float64    XX, YY, ZZ, XY, XZ, YZ
:guilabel:`Structure Type`                          int32
:guilabel:`Superquadric Roundness`                  float32    Phi, Theta
:guilabel:`Torque`                                  float64    X, Y, Z
:guilabel:`Total Energy`                            float64
:guilabel:`Transparency`                            float32
:guilabel:`Vector Color`                            float32    R, G, B
:guilabel:`Vector Transparency`                     float32
:guilabel:`Velocity Magnitude`                      float64
:guilabel:`Velocity`                                float64    X, Y, Z
=================================================== ========== ===================================================

For some of the most important properties, this container class provides quick access getters such as :py:attr:`.positions`,
:py:attr:`.identifiers`, or :py:attr:`.particle_types` to look them up::

    coords = data.particles.positions

User-defined particle properties having non-standard names, and standard properties for which no quick access getter exists, can be looked up
by literal name::

    mol_ids = data.particles['Molecule Identifier']

For more information on how to add or modify particle properties, please see the :py:class:`PropertyContainer`
and :py:class:`Property` classes."""
    vis: ovito.vis.ParticlesVis = ovito.vis.ParticlesVis()
    'vis() -> Optional[ovito.vis.DataVis]\n\nThe :py:class:`DataVis` element currently associated with this data object. This object is responsible for\nvisually rendering the stored data. If set to ``None``, the data object remains non-visual and does not appear in\nrendered images or viewports. Additionally, note that the same :py:class:`DataVis` element may be assigned to\nmultiple data objects to synchronize their visual appearance.\n\nSee the :py:mod:`ovito.vis` module for a list of visual element types.'

    @property
    def angles(self) -> Angles:
        """A :py:class:`PropertyContainer` storing the list of angles defined for the molecular model (may be ``None``)."""
        ...

    @property
    def angles_(self) -> Angles:
        ...

    @property
    def bonds(self) -> Bonds:
        """The :py:class:`Bonds` data object storing the list of bonds and their properties (may be ``None``)."""
        ...

    @property
    def bonds_(self) -> Bonds:
        ...

    @property
    def impropers(self) -> Impropers:
        """A :py:class:`PropertyContainer` storing the list of impropers defined for the molecular model (may be ``None``)."""
        ...

    @property
    def impropers_(self) -> Impropers:
        ...

    @property
    def dihedrals(self) -> Dihedrals:
        """A :py:class:`PropertyContainer` storing the list of dihedrals defined for the molecular model (may be ``None``)."""
        ...

    @property
    def dihedrals_(self) -> Dihedrals:
        ...

    @property
    def colors(self) -> Property:
        """The :py:class:`Property` data array for the ``Color`` standard particle property; or ``None`` if that property is undefined."""
        ...

    @property
    def colors_(self) -> Property:
        ...

    @property
    def forces(self) -> Property:
        """The :py:class:`Property` data array for the ``Force`` standard particle property; or ``None`` if that property is undefined."""
        ...

    @property
    def forces_(self) -> Property:
        ...

    @property
    def identifiers(self) -> Property:
        """Returns a :py:class:`Property` data array containing the values of the ``Particle Identifier`` standard particle property; or ``None`` if that particle property does not exist.

The property array stores the numerical IDs that are typically used by simulation codes to uniquely identify individual particles.

Some of OVITO's simulation file readers provide the option to sort the list of particles by ID during
import to obtain a stable ordering. Generally, however, the storage order of particles is arbitrary and can vary between frames
of a trajectory. The :py:meth:`.remap_indices` method can be useful in this situation.

Given some list of zero-based particle *indices*, determining the corresponding unique identifiers requires just a simple NumPy indexing expression:

```python
  query_indices = [0, 7, 3, 2] # <-- zero-based particle indices for which we want to look up IDs
  ids = data.particles.identifiers[query_indices]
  assert len(ids) == len(query_indices)
```

The reverse lookup, i.e., finding the indices at which particles with certain IDs are stored in the list, requires some more work.
That's because particles may be stored in arbitrary order, i.e., the sequence of unique IDs is generally not sorted nor contiguous.
A rather slow approach is to :obj:`search <numpy:numpy.argwhere>` through the entire array of IDs to locate the one we are looking for:

```python
  query_id = 37 # <-- a unique particle ID we are looking for
  index = numpy.argwhere(data.particles.identifiers == query_id)[0,0]
  assert data.particles.identifiers[index] == query_id
```

We can speed things up with some extra effort, which pays off when there is a need to look up several particle IDs.
To this end, we first :obj:`sort <numpy:numpy.argsort>` the list of IDs, then perform a more efficient :obj:`sorted search <numpy:numpy.searchsorted>`,
and finally map the found indices back to the original particle ordering:

```python
  query_ids = [2, 37, 8] # <-- some unique particle IDs we want to look up all at once
  ordering = numpy.argsort(data.particles.identifiers)
  indices = ordering[numpy.searchsorted(data.particles.identifiers, query_ids, sorter=ordering)]
  assert numpy.array_equal(data.particles.identifiers[indices], query_ids)
```

The above ``assert`` statements are for illustration purposes only."""
        ...

    @property
    def identifiers_(self) -> Property:
        ...

    @property
    def masses(self) -> Property:
        """The :py:class:`Property` data array for the ``Mass`` standard particle property; or ``None`` if that property is undefined."""
        ...

    @property
    def masses_(self) -> Property:
        ...

    @property
    def particle_types(self) -> Property:
        """The :py:class:`Property` data array for the ``Particle Type`` standard particle property; or ``None`` if that property is undefined."""
        ...

    @property
    def particle_types_(self) -> Property:
        ...

    @property
    def positions(self) -> Property:
        """Returns the :py:class:`Property` data array storing the particle coordinates, i.e. the values of the ``Position`` standard particle property.
Accessing this field is equivalent to a name-based lookup in the :py:class:`PropertyContainer`::

    assert data.particles.positions is (data.particles['Position'] if 'Position' in data.particles else None)

Under special circumstances the ``Position`` particle property might not be defined (yet). Then the value is ``None``."""
        ...

    @property
    def positions_(self) -> Property:
        ...

    @property
    def selection(self) -> Property:
        """The :py:class:`Property` data array for the ``Selection`` standard particle property; or ``None`` if that property is undefined."""
        ...

    @property
    def selection_(self) -> Property:
        ...

    @property
    def structure_types(self) -> Property:
        """The :py:class:`Property` data array for the ``Structure Type`` standard particle property; or ``None`` if that property is undefined."""
        ...

    @property
    def structure_types_(self) -> Property:
        ...

    @property
    def velocities(self) -> Property:
        """The :py:class:`Property` data array for the ``Velocity`` standard particle property; or ``None`` if that property is undefined."""
        ...

    @property
    def velocities_(self) -> Property:
        ...

    def add_particle(self, position: ArrayLike) -> int:
        """Adds a new particle to the model. The particle :py:attr:`count` will be incremented by one.
The method assigns *position* to the ``Position`` property of the new particle. The values of all other properties
are initialized to zero.

:param position: The xyz coordinates for the new particle.
:returns: The 0-based index of the newly created particle, which is :py:attr:`(Particles.count-1) <ovito.data.PropertyContainer.count>`."""
        ...

    def delta_vector(self, a: ArrayLike, b: ArrayLike, cell: SimulationCell, return_pbcvec: bool=False) -> NDArray[numpy.float64]:
        """Computes the vector connecting two particles *a* and *b* in a periodic simulation cell by applying the minimum image convention.

This is a convenience wrapper for the `SimulationCell.delta_vector()` method,
which computes the vector between two arbitrary spatial locations :math:`r_a` and :math:`r_b` taking into account periodic
boundary conditions. The version of the method described here takes two particle indices *a* and *b* as input, computing the shortest vector
:math:`{\\Delta} = (r_b - r_a)` between them using the `minimum image convention <https://en.wikipedia.org/wiki/Periodic_boundary_conditions>`__.
Please see the `SimulationCell.delta_vector()` method for further information.

:param a: Zero-based index of the first input particle. This may also be an array of particle indices.
:param b: Zero-based index of the second input particle. This may also be an array of particle indices with the same length as *a*.
:param cell: The periodic domain. Typically, `DataCollection.cell` is used as argument here.
:param return_pbcvec: If true, lets the method also return the vector :math:`n`, which specifies how often the computed particle-to-particle vector crosses the cell's face.
:returns: The delta vector and, optionally, the vector :math:`n`."""
        ...

    def create_bonds(self, **kwargs: Any) -> Bonds:
        """This convenience method conditionally creates and associates a :py:class:`Bonds` object with this :py:class:`Particles` parent object.
If there is already an existing bonds object (:py:attr:`.bonds` is not ``None``), then that bonds object is
replaced with a modifiable copy if necessary. The attached :py:class:`BondsVis` element is preserved.

:param params: Key-value pairs passed to the method as keyword arguments are used to set attributes of the :py:class:`Bonds` object (even if the bonds object already existed).
:param vis_params: Optional dictionary to initialize attributes of the attached :py:class:`BondsVis` element (only used if the bonds object is newly created by the method).

The logic of this method is roughly equivalent to the following code::

    def create_bonds(particles: Particles, vis_params=None, **params) -> Bonds:
        if particles.bonds is None:
            particles.bonds = Bonds()
            if vis_params:
                for name, value in vis_params.items(): setattr(particles.bonds.vis, name, value)
        for name, value in params.items(): setattr(particles.bonds_, name, value)
        return particles.bonds_

Usage example:

```python
  pairs = [(0, 1), (1, 2), (2, 0)] # Pairs of particle indices to connect by bonds
  bonds = data.particles_.create_bonds(count=len(pairs), vis_params={'width': 0.6})
  bonds.create_property('Topology', data=pairs)
```"""
        ...

    def create_angles(self, **kwargs: Any) -> Angles:
        ...

    def create_dihedrals(self, **kwargs: Any) -> Dihedrals:
        ...

    def create_impropers(self, **kwargs: Any) -> Impropers:
        ...

    def remap_indices(self, particles: Particles, indices: Optional[Sequence[int]]=None) -> Union[NDArray[numpy.int64], slice]:
        """In case the storage order of atoms or particles changes during the course of a simulation,
this method can determine the mapping of particles from one snapshot of the trajectory to another.
It uses the unique :py:attr:`identifiers` of the particles to do that.

Given two data collections *A* and *B* containing the same set of particles but in different order,
:py:meth:`remap_indices` determines for each particle in *B* the zero-based index at which
the same particle is found in *A*. For instance::

    >>> A = pipeline.compute(frame=0)
    >>> B = pipeline.compute(frame=1)
    >>> A.particles.identifiers[...]
    [8 101  5 30 99]
    >>> B.particles.identifiers[...]
    [5 101 30 99  8]
    >>> A.particles.remap_indices(B.particles)
    [2 1 3 4 0]

The index mapping generated by :py:meth:`remap_indices` allows you to retrieve property values of particles in *A*
in the same order in which they appear in *B*, making it easy to perform computations involving property values at
different trajectory timesteps, e.g.::

    mapping = A.particles.remap_indices(B.particles)
    displacements = B.particles.positions - A.particles.positions[mapping]

:py:meth:`remap_indices` compares the unique :py:attr:`identifiers` stored in the ``Particle Identifier``
property arrays of both snapshots to compute the index permutation map. If this property is not defined, which may be the
case if the imported trajectory file did not contain atom IDs, the :py:meth:`remap_indices` method
simply assumes that both snapshots use the same constant storage order and returns the identity mapping -
as a Python :py:class:`slice` object for optimal performance when being used for NumPy indexing.
A slice object is also returned in case the ordering of particle IDs turns out to be the same in both snapshots and no
remapping is necessary.

The default behavior of the method is to look up all particles of *B* in *A*. But the index mapping can also be
established just for a subset of particles from *B* by supplying the optional parameter *indices*.
The method expects an array of zero-based indices specifying which particles from snapshot *B* should be looked up in
snapshot *A*. The returned mapping will have the same length as *indices*. Example::

    # The numeric ID of atom type 'H':
    hydrogen_type = B.particles.particle_types.type_by_name('H').id
    # Determine the indices of all H atoms in data collection B:
    hydrogen_indices = numpy.flatnonzero(B.particles.particle_types == hydrogen_type)

    # Determine the corresponding indices of the same atoms in data collection A:
    mapping = A.particles.remap_indices(B.particles, hydrogen_indices)

    # In snapshot A the same particles are all H atoms too:
    assert numpy.all(A.particles.particle_types[mapping] == hydrogen_type)"""
        ...

@dataclass(kw_only=True)
class Bonds(PropertyContainer):
    """Base: :py:class:`ovito.data.PropertyContainer`

Stores the list of bonds and their properties. A :py:class:`Bonds` object is always part of a parent :py:class:`Particles` object.
You can access it as follows:

```python
  data = pipeline.compute()
  print("Number of bonds:", data.particles.bonds.count)
```

The :py:class:`Bonds` class inherits the :py:attr:`~PropertyContainer.count` attribute from its :py:class:`PropertyContainer` base class. This attribute returns the number of bonds.

Bond properties

Bonds can be associated with arbitrary *bond properties*, which are managed in the :py:class:`Bonds` container
as a set of :py:class:`Property` data arrays. Each bond property has a unique name by which it can be looked up:

```python
  print("Bond property names:")
  print(data.particles.bonds.keys())
  if 'Length' in data.particles.bonds:
      length_prop = data.particles.bonds['Length']
      assert(len(length_prop) == data.particles.bonds.count)
```

New bond properties can be added using the :py:meth:`PropertyContainer.create_property` method.

Bond topology

The ``Topology`` bond property, which is always present,
defines the connectivity between particles in the form of a *N* x 2 array of indices into the :py:class:`Particles` array.
In other words, each bond is defined by a pair of particle indices.

```python
  for a,b in data.particles.bonds.topology:
      print("Bond from particle %i to particle %i" % (a,b))
```

Note that the bonds of a system are not stored in any particular order.
If you need to enumerate all bonds connected to a certain particle, you can use the :py:class:`BondsEnumerator` utility class for that.

Bonds visualization

The :py:class:`Bonds` data object has a :py:class:`BondsVis` element attached to it,
which controls the visual appearance of the bonds in rendered images. It can be accessed through the :py:attr:`~DataObject.vis`
attribute:

```python
  data.particles.bonds.vis.enabled = True
  data.particles.bonds.vis.flat_shading = True
  data.particles.bonds.vis.width = 0.3
```

Computing bond vectors

Since each bond is defined by two indices into the particles array, we can use these indices to determine the corresponding spatial
bond *vectors* connecting the particles. They can be computed from the positions of the particles:

```python
  topology = data.particles.bonds.topology
  positions = data.particles.positions
  bond_vectors = positions[topology[:,1]] - positions[topology[:,0]]
```

Here, the first and the second column of the bonds topology array are used to index into the particle positions array.
The subtraction of the two indexed arrays yields the list of bond vectors. Each vector in this list points
from the first particle to the second particle of the corresponding bond.

Finally, we may have to correct for the effect of periodic boundary conditions when a bond
connects two particles on opposite sides of the box. OVITO keeps track of such cases by means of
the special ``Periodic Image`` bond property. It stores a shift vector for each bond, specifying the directions in which the bond
crosses periodic boundaries. We make use of this information to correct the bond vectors computed above.
This is done by adding the product of the cell matrix and the shift vectors from the ``Periodic Image`` bond property:

```python
  bond_vectors += numpy.dot(data.cell[:3,:3], data.particles.bonds.pbc_vectors.T).T
```

The shift vectors array is transposed here to facilitate the transformation
of the entire array of vectors with a single 3x3 cell matrix.
To summarize: In the two code snippets above, we have performed
the following calculation of the *unwrapped* vector :math:`\\mathbf{v}` for every bond (*a*, *b*) in parallel:

   :math:`\\mathbf{v} = \\mathbf{x}_b - \\mathbf{x}_a + \\mathbf{H} \\cdot (n_x, n_y, n_z)^{T}`,

with :math:`\\mathbf{H}` denoting the simulation cell matrix and :math:`(n_x, n_y, n_z)` the bond's PBC shift vector.

Standard bond properties

The following standard properties are defined for bonds:

================================ ======================== ========== =======================
Property name                    Python access            Data type  Component names
================================ ======================== ========== =======================
:guilabel:`Bond Type`            :py:attr:`bond_types`    int32
:guilabel:`Color`                :py:attr:`colors`        float32    R, G, B
:guilabel:`Length`                                        float64
:guilabel:`Particle Identifiers`                          int64      A, B\\
:guilabel:`Periodic Image`       :py:attr:`pbc_vectors`   int32      X, Y, Z
:guilabel:`Selection`            :py:attr:`selection`     int8
:guilabel:`Topology`             :py:attr:`topology`      int64      1, 2\\
:guilabel:`Transparency`                                  float32
:guilabel:`Width`                                         float32
================================ ======================== ========== ======================="""
    vis: ovito.vis.BondsVis = ovito.vis.BondsVis()
    'vis() -> Optional[ovito.vis.DataVis]\n\nThe :py:class:`DataVis` element currently associated with this data object. This object is responsible for\nvisually rendering the stored data. If set to ``None``, the data object remains non-visual and does not appear in\nrendered images or viewports. Additionally, note that the same :py:class:`DataVis` element may be assigned to\nmultiple data objects to synchronize their visual appearance.\n\nSee the :py:mod:`ovito.vis` module for a list of visual element types.'

    @property
    def bond_types(self) -> Property:
        """The :py:class:`Property` data array for the ``Bond Type`` standard bond property; or ``None`` if that property is undefined."""
        ...

    @property
    def bond_types_(self) -> Property:
        ...

    @property
    def colors(self) -> Property:
        """The :py:class:`Property` data array for the ``Color`` standard bond property; or ``None`` if that property is undefined."""
        ...

    @property
    def colors_(self) -> Property:
        ...

    @property
    def pbc_vectors(self) -> Property:
        """The :py:class:`Property` data array for the ``Periodic Image`` standard bond property; or ``None`` if that property is undefined."""
        ...

    @property
    def pbc_vectors_(self) -> Property:
        ...

    @property
    def selection(self) -> Property:
        """The :py:class:`Property` data array for the ``Selection`` standard bond property; or ``None`` if that property is undefined."""
        ...

    @property
    def selection_(self) -> Property:
        ...

    @property
    def topology(self) -> Property:
        """The :py:class:`Property` data array for the ``Topology`` standard bond property; or ``None`` if that property is undefined."""
        ...

    @property
    def topology_(self) -> Property:
        ...

    def add_bond(self, a: int, b: int, type: Optional[int]=None, pbcvec: Optional[ArrayLike]=None) -> int:
        """Creates a new bond between two particles *a* and *b*, both parameters being indices into the particles list.

:param a: Index of first particle connected by the new bond. Particle indices start at 0.
:param b: Index of second particle connected by the new bond.
:param type: Optional type ID to be assigned to the new bond. This value will be stored to the :py:attr:`bond_types` array.
:param pbcvec: Three integers specifying the bond's crossings of periodic cell boundaries. The information will be stored in the :py:attr:`pbc_vectors` array.
:returns: The 0-based index of the newly created bond, which is :py:attr:`(Bonds.count-1) <ovito.data.PropertyContainer.count>`.

The method does *not* check if there already is an existing bond connecting the same pair of particles.

The method does *not* check if the particle indices *a* and *b* do exist. Thus, it is your responsibility to ensure that both indices
are in the range 0 to :py:attr:`(Particles.count-1) <ovito.data.PropertyContainer.count>`.

In case the :py:class:`SimulationCell` has periodic boundary conditions enabled, and the two particles connected by the bond are located in different periodic images,
make sure you provide the *pbcvec* argument. It is required so that OVITO does not draw the bond as a direct line from particle *a* to particle *b* but as a line passing through
the periodic cell faces. You can use the `Particles.delta_vector()` function to compute
*pbcvec* or use the ``pbc_shift`` vector returned by the :py:class:`CutoffNeighborFinder` utility."""
        ...

@dataclass(kw_only=True)
class Angles(PropertyContainer):
    """Base: :py:class:`ovito.data.DataObject`

A dictionary-like object storing a set of :py:class:`Property` objects.

It implements the ``collections.abc.Mapping`` interface. That means it can be used
like a standard read-only Python ``dict`` object to access the properties by name, e.g.:

```python
  data = pipeline.compute()
  
  positions = data.particles['Position']
  has_selection = 'Selection' in data.particles
  name_list = data.particles.keys()
```

New properties are typically added to a container with a call to :py:meth:`.create_property` as described here.
To remove an existing property from a container, you can use Python's ``del`` statement:

```python
  del data.particles_['Selection']
```

OVITO has several concrete implementations of the abstract :py:class:`PropertyContainer` interface:

    * :py:class:`Particles`
    * :py:class:`Bonds`
    * :py:class:`VoxelGrid`
    * :py:class:`DataTable`
    * :py:class:`Lines`
    * :py:class:`Vectors`
    * :py:attr:`SurfaceMesh.vertices`
    * :py:attr:`SurfaceMesh.faces`
    * :py:attr:`SurfaceMesh.regions`"""
    pass

@dataclass(kw_only=True)
class Dihedrals(PropertyContainer):
    """Base: :py:class:`ovito.data.DataObject`

A dictionary-like object storing a set of :py:class:`Property` objects.

It implements the ``collections.abc.Mapping`` interface. That means it can be used
like a standard read-only Python ``dict`` object to access the properties by name, e.g.:

```python
  data = pipeline.compute()
  
  positions = data.particles['Position']
  has_selection = 'Selection' in data.particles
  name_list = data.particles.keys()
```

New properties are typically added to a container with a call to :py:meth:`.create_property` as described here.
To remove an existing property from a container, you can use Python's ``del`` statement:

```python
  del data.particles_['Selection']
```

OVITO has several concrete implementations of the abstract :py:class:`PropertyContainer` interface:

    * :py:class:`Particles`
    * :py:class:`Bonds`
    * :py:class:`VoxelGrid`
    * :py:class:`DataTable`
    * :py:class:`Lines`
    * :py:class:`Vectors`
    * :py:attr:`SurfaceMesh.vertices`
    * :py:attr:`SurfaceMesh.faces`
    * :py:attr:`SurfaceMesh.regions`"""
    pass

@dataclass(kw_only=True)
class Impropers(PropertyContainer):
    """Base: :py:class:`ovito.data.DataObject`

A dictionary-like object storing a set of :py:class:`Property` objects.

It implements the ``collections.abc.Mapping`` interface. That means it can be used
like a standard read-only Python ``dict`` object to access the properties by name, e.g.:

```python
  data = pipeline.compute()
  
  positions = data.particles['Position']
  has_selection = 'Selection' in data.particles
  name_list = data.particles.keys()
```

New properties are typically added to a container with a call to :py:meth:`.create_property` as described here.
To remove an existing property from a container, you can use Python's ``del`` statement:

```python
  del data.particles_['Selection']
```

OVITO has several concrete implementations of the abstract :py:class:`PropertyContainer` interface:

    * :py:class:`Particles`
    * :py:class:`Bonds`
    * :py:class:`VoxelGrid`
    * :py:class:`DataTable`
    * :py:class:`Lines`
    * :py:class:`Vectors`
    * :py:attr:`SurfaceMesh.vertices`
    * :py:attr:`SurfaceMesh.faces`
    * :py:attr:`SurfaceMesh.regions`"""
    pass

class SurfaceMeshTopology(DataObject):
    """Base: :py:class:`ovito.data.DataObject`

This data structure holds the connectivity information of a :py:class:`SurfaceMesh`. It is accessible through the :py:attr:`SurfaceMesh.topology` field.
The surface mesh topology consists of *vertices*, *faces* and *halfedges*.

All these topological entities of the mesh are identified by numeric indices ranging from
0 to (:py:attr:`vertex_count`-1), (:py:attr:`face_count`-1), and (:py:attr:`edge_count`-1), respectively.
The vertices and faces of the mesh may be associated with auxiliary properties, which are stored separately from
the topology in the :py:attr:`SurfaceMesh.vertices` and :py:attr:`SurfaceMesh.faces` property containers.
In particular, the spatial coordinates of the mesh vertices are stored as ``Position`` property array in :py:attr:`SurfaceMesh.vertices`.

.. image:: ../introduction/graphics/halfedge_structure.png
   :width: 50 %
   :align: right

A *halfedge* is a directed edge :math:`a \\to b` connecting two vertices :math:`a` and :math:`b` -- depicted as a half-arrow in the figure.
A face is implicitly defined by a circular sequence of halfedges that bound the face. Typically, halfedges come in pairs.
The halfedge :math:`a \\to b` and its *opposite* halfedge, :math:`b \\to a`, form a pair that links two neighboring faces together.
Thus, halfedge pairs are essential for forming a connected, two-dimensional surface manifold. The surface is said to be *closed*,
i.e., it has no open boundaries if all halfedges of the mesh are associated with corresponding opposite halfedges (see :py:attr:`is_closed`).

For each vertex the topology object maintains a linked-list of directed halfedges leaving that vertex.
It can be accessed through the :py:meth:`first_vertex_edge` and :py:meth:`next_vertex_edge` methods.

For each face the topology object maintains a circular linked-list of directed halfedges bounding that face (in counter-clockwise winding order).
It can be accessed through the :py:meth:`first_face_edge` and :py:meth:`next_face_edge`/:py:meth:`prev_face_edge` methods."""

    @property
    def vertex_count(self) -> int:
        """Number of vertices in the :py:class:`SurfaceMesh`. This is always equal to the :py:attr:`~PropertyContainer.count` of the :py:attr:`SurfaceMesh.vertices` property container. 

This property is read-only. Use :py:meth:`SurfaceMesh.create_vertices` to add new vertices to the mesh."""
        ...

    @property
    def face_count(self) -> int:
        """Number of faces in the :py:class:`SurfaceMesh`. This is always equal to the :py:attr:`~PropertyContainer.count` of the :py:attr:`SurfaceMesh.faces` property container. 

This property is read-only. Use :py:meth:`SurfaceMesh.create_face` or :py:meth:`SurfaceMesh.create_faces` to add new faces to the mesh."""
        ...

    @property
    def edge_count(self) -> int:
        """Total number of halfedges in the :py:class:`SurfaceMesh`. This property is read-only. Halfedges are created automatically by :py:meth:`SurfaceMesh.create_face` or :py:meth:`SurfaceMesh.create_faces` when adding new faces to the mesh topology."""
        ...

    @property
    def is_closed(self) -> bool:
        """This is a read-only property indicating whether the surface mesh is fully closed. In a closed mesh, all faces are connected to exactly one adjacent face along each of their halfedges. That means the mesh presents a two-dimensional surface manifold without borders. Furthermore, a closed mesh divides space into an "interior" and an "exterior" region."""
        ...

    @overload
    def opposite_face(self, face: int) -> int: # type: ignore
        """Returns the face on the opposite side of the two-sided manifold, or -1 if the manifold is one-sided. The returned face shares the same vertices with *face* but in reverse order. Note that ``opposite_face(opposite_face(face))==face``."""
        ...

    @overload
    def opposite_face(self, face: Sequence[int]) -> NDArray[numpy.int_]:
        """Returns the face on the opposite side of the two-sided manifold, or -1 if the manifold is one-sided. The returned face shares the same vertices with *face* but in reverse order. Note that ``opposite_face(opposite_face(face))==face``."""
        ...

    @overload
    def has_opposite_face(self, face: int) -> bool: # type: ignore
        """Returns whether *face* is part of a two-sided manifold. A face that is part of a two-sided manifold has a 'partner' face with opposite orientation, which can be retrieved through the :py:meth:`opposite_face` method."""
        ...

    @overload
    def has_opposite_face(self, face: Sequence[int]) -> NDArray[numpy.bool_]:
        """Returns whether *face* is part of a two-sided manifold. A face that is part of a two-sided manifold has a 'partner' face with opposite orientation, which can be retrieved through the :py:meth:`opposite_face` method."""
        ...

    @overload
    def first_vertex_edge(self, vertex: int) -> int: # type: ignore
        """Returns the head halfedge from the linked list of halfedges leaving *vertex*. Subsequent halfedges from the linked list can be retrieved with :py:meth:`next_vertex_edge`. If no halfedges are connected to *vertex*, the method returns -1."""
        ...

    @overload
    def first_vertex_edge(self, vertex: Sequence[int]) -> NDArray[numpy.int_]:
        """Returns the head halfedge from the linked list of halfedges leaving *vertex*. Subsequent halfedges from the linked list can be retrieved with :py:meth:`next_vertex_edge`. If no halfedges are connected to *vertex*, the method returns -1."""
        ...

    @overload
    def next_vertex_edge(self, edge: int) -> int: # type: ignore
        """Returns another halfedge leaving from the same vertex as *edge*. Together with :py:meth:`first_vertex_edge` this method allows you to iterate over all halfedges connected to some vertex. When the end of the vertex' edge list has been reached, the method returns -1. 

The following example demonstrates how to visit all outgoing halfedges of some vertex and count them:

```python
  def count_edges(mesh: SurfaceMesh, vertex: int) -> int:
      count = 0
      edge = mesh.topology.first_vertex_edge(vertex)
      while edge != -1:
          assert mesh.topology.first_edge_vertex(edge) == vertex
          count += 1
          edge = mesh.topology.next_vertex_edge(edge)
      return count
  
  # The function defined above is equivalent to SurfaceMeshTopology.count_vertex_edges():
  assert count_edges(mesh, 0) == mesh.topology.count_vertex_edges(0)
```"""
        ...

    @overload
    def next_vertex_edge(self, edge: Sequence[int]) -> NDArray[numpy.int_]:
        """Returns another halfedge leaving from the same vertex as *edge*. Together with :py:meth:`first_vertex_edge` this method allows you to iterate over all halfedges connected to some vertex. When the end of the vertex' edge list has been reached, the method returns -1. 

The following example demonstrates how to visit all outgoing halfedges of some vertex and count them:

```python
  def count_edges(mesh: SurfaceMesh, vertex: int) -> int:
      count = 0
      edge = mesh.topology.first_vertex_edge(vertex)
      while edge != -1:
          assert mesh.topology.first_edge_vertex(edge) == vertex
          count += 1
          edge = mesh.topology.next_vertex_edge(edge)
      return count
  
  # The function defined above is equivalent to SurfaceMeshTopology.count_vertex_edges():
  assert count_edges(mesh, 0) == mesh.topology.count_vertex_edges(0)
```"""
        ...

    @overload
    def first_face_edge(self, face: int) -> int: # type: ignore
        """Returns some halfedge bounding the given face. Given that first halfedge, all other halfedges bounding the same face can be visited using :py:meth:`next_face_edge` or :py:meth:`prev_face_edge`."""
        ...

    @overload
    def first_face_edge(self, face: Sequence[int]) -> NDArray[numpy.int_]:
        """Returns some halfedge bounding the given face. Given that first halfedge, all other halfedges bounding the same face can be visited using :py:meth:`next_face_edge` or :py:meth:`prev_face_edge`."""
        ...

    @overload
    def next_face_edge(self, edge: int) -> int: # type: ignore
        """Given the halfedge *edge* bounding some face, this method returns the following halfedge when going around the face in forward direction (counter-clockwise - when looking at the front side of the face). All halfedges of the face form a circular sequence - without a particular beginning or end. You can loop over this circular sequence in forward or backward direction with the :py:meth:`next_face_edge` and :py:meth:`prev_face_edge` methods. Given some mesh face, you can obtain a first halfedge through the :py:meth:`first_face_edge` method. 

The following code example shows how to visit all halfedges of a face in order. Since the halfedges form a circular linked list, we have to remember which edge we started from to terminate the loop once we reach the first edge again:

```python
  def count_edges(mesh: SurfaceMesh, face: int) -> int:
      start_edge = mesh.topology.first_face_edge(face)
      count = 1
      edge = mesh.topology.next_face_edge(start_edge)
      while edge != start_edge:
          assert mesh.topology.adjacent_face(edge) == face
          count += 1
          edge = mesh.topology.next_face_edge(edge)
      return count
  
  # The function defined above is equivalent to SurfaceMeshTopology.count_face_edges():
  assert count_edges(mesh, 0) == mesh.topology.count_face_edges(0)
```"""
        ...

    @overload
    def next_face_edge(self, edge: Sequence[int]) -> NDArray[numpy.int_]:
        """Given the halfedge *edge* bounding some face, this method returns the following halfedge when going around the face in forward direction (counter-clockwise - when looking at the front side of the face). All halfedges of the face form a circular sequence - without a particular beginning or end. You can loop over this circular sequence in forward or backward direction with the :py:meth:`next_face_edge` and :py:meth:`prev_face_edge` methods. Given some mesh face, you can obtain a first halfedge through the :py:meth:`first_face_edge` method. 

The following code example shows how to visit all halfedges of a face in order. Since the halfedges form a circular linked list, we have to remember which edge we started from to terminate the loop once we reach the first edge again:

```python
  def count_edges(mesh: SurfaceMesh, face: int) -> int:
      start_edge = mesh.topology.first_face_edge(face)
      count = 1
      edge = mesh.topology.next_face_edge(start_edge)
      while edge != start_edge:
          assert mesh.topology.adjacent_face(edge) == face
          count += 1
          edge = mesh.topology.next_face_edge(edge)
      return count
  
  # The function defined above is equivalent to SurfaceMeshTopology.count_face_edges():
  assert count_edges(mesh, 0) == mesh.topology.count_face_edges(0)
```"""
        ...

    @overload
    def prev_face_edge(self, edge: int) -> int: # type: ignore
        """Given the halfedge *edge* bounding some face, this method returns the previous halfedge going around that face in backward direction (clockwise - when looking at the front side of the face). All halfedges of a face form a circular sequence - without a particular beginning or end. You can loop over this circular sequence in forward or backward direction with the :py:meth:`next_face_edge` and :py:meth:`prev_face_edge` methods."""
        ...

    @overload
    def prev_face_edge(self, edge: Sequence[int]) -> NDArray[numpy.int_]:
        """Given the halfedge *edge* bounding some face, this method returns the previous halfedge going around that face in backward direction (clockwise - when looking at the front side of the face). All halfedges of a face form a circular sequence - without a particular beginning or end. You can loop over this circular sequence in forward or backward direction with the :py:meth:`next_face_edge` and :py:meth:`prev_face_edge` methods."""
        ...

    @overload
    def first_edge_vertex(self, edge: int) -> int: # type: ignore
        """Returns the vertex the given halfedge is leaving from. To retrieve the vertex the halfedge is leading to, call :py:meth:`second_edge_vertex`."""
        ...

    @overload
    def first_edge_vertex(self, edge: Sequence[int]) -> NDArray[numpy.int_]:
        """Returns the vertex the given halfedge is leaving from. To retrieve the vertex the halfedge is leading to, call :py:meth:`second_edge_vertex`."""
        ...

    @overload
    def second_edge_vertex(self, edge: int) -> int: # type: ignore
        """Returns the vertex the given halfedge is leading to. To retrieve the vertex the halfedge is leaving from, call :py:meth:`first_edge_vertex`."""
        ...

    @overload
    def second_edge_vertex(self, edge: Sequence[int]) -> NDArray[numpy.int_]:
        """Returns the vertex the given halfedge is leading to. To retrieve the vertex the halfedge is leaving from, call :py:meth:`first_edge_vertex`."""
        ...

    @overload
    def adjacent_face(self, edge: int) -> int: # type: ignore
        """Returns the face that is bounded by the halfedge *edge*."""
        ...

    @overload
    def adjacent_face(self, edge: Sequence[int]) -> NDArray[numpy.int_]:
        """Returns the face that is bounded by the halfedge *edge*."""
        ...

    @overload
    def first_face_vertex(self, face: int) -> int: # type: ignore
        """Given a face, this method returns some vertex of that face. This is equivalent to retrieving the vertex to which the first halfedge of the face is connected to, i.e. ``first_edge_vertex(first_face_edge(face))``."""
        ...

    @overload
    def first_face_vertex(self, face: Sequence[int]) -> NDArray[numpy.int_]:
        """Given a face, this method returns some vertex of that face. This is equivalent to retrieving the vertex to which the first halfedge of the face is connected to, i.e. ``first_edge_vertex(first_face_edge(face))``."""
        ...

    @overload
    def opposite_edge(self, edge: int) -> int: # type: ignore
        """Given the halfedge *edge*, returns the reverse halfedge that bounds an adjacent face. This opposite halfedge connects the same two vertices as *edge* but in reverse order. You can use this method to determine whether the face bounded by *edge* has a neighboring face that is part of the same manifold:

```python
  def get_neighboring_face(mesh: SurfaceMesh, edge: int) -> int:
      opp_edge = mesh.topology.opposite_edge(edge)
      if opp_edge == -1: return -1
      assert mesh.topology.first_edge_vertex(edge) == mesh.topology.second_edge_vertex(opp_edge)
      assert mesh.topology.second_edge_vertex(edge) == mesh.topology.first_edge_vertex(opp_edge)
      return mesh.topology.adjacent_face(opp_edge)
```

You may call the convenience method :py:meth:`has_opposite_edge` to determine whether a halfedge is associated with a corresponding opposite halfedge. If the surface mesh does not form a closed manifold, the halfedges at the boundary of the manifold do not have opposite halfedges, because there are no adjacent faces where the surface terminates."""
        ...

    @overload
    def opposite_edge(self, edge: Sequence[int]) -> NDArray[numpy.int_]:
        """Given the halfedge *edge*, returns the reverse halfedge that bounds an adjacent face. This opposite halfedge connects the same two vertices as *edge* but in reverse order. You can use this method to determine whether the face bounded by *edge* has a neighboring face that is part of the same manifold:

```python
  def get_neighboring_face(mesh: SurfaceMesh, edge: int) -> int:
      opp_edge = mesh.topology.opposite_edge(edge)
      if opp_edge == -1: return -1
      assert mesh.topology.first_edge_vertex(edge) == mesh.topology.second_edge_vertex(opp_edge)
      assert mesh.topology.second_edge_vertex(edge) == mesh.topology.first_edge_vertex(opp_edge)
      return mesh.topology.adjacent_face(opp_edge)
```

You may call the convenience method :py:meth:`has_opposite_edge` to determine whether a halfedge is associated with a corresponding opposite halfedge. If the surface mesh does not form a closed manifold, the halfedges at the boundary of the manifold do not have opposite halfedges, because there are no adjacent faces where the surface terminates."""
        ...

    @overload
    def has_opposite_edge(self, edge: int) -> int: # type: ignore
        """Returns whether the given halfedge *edge* is associated with a corresponding reverse halfedge bounding an adjacent face in the same manifold. This is equivalent to checking the return value of :py:meth:`opposite_edge`, which returns -1 to indicate that *edge* does not have an opposite edge."""
        ...

    @overload
    def has_opposite_edge(self, edge: Sequence[int]) -> NDArray[numpy.bool_]:
        """Returns whether the given halfedge *edge* is associated with a corresponding reverse halfedge bounding an adjacent face in the same manifold. This is equivalent to checking the return value of :py:meth:`opposite_edge`, which returns -1 to indicate that *edge* does not have an opposite edge."""
        ...

    @overload
    def next_manifold_edge(self, edge: int) -> int: # type: ignore
        """Advanced method to visit all surface manifolds meeting at an edge (e.g. along a triple junction in a grain boundary network). Given a halfedge, this method returns the halfedge that is part of the next manifold and which connects the same two mesh vertices. 

The manifold information may only be available in certain types of surface meshes, such as those generated by the :py:class:`ConstructSurfaceModifier` when the :py:attr:`identify_regions` option is turned on."""
        ...

    @overload
    def next_manifold_edge(self, edge: Sequence[int]) -> NDArray[numpy.int_]:
        """Advanced method to visit all surface manifolds meeting at an edge (e.g. along a triple junction in a grain boundary network). Given a halfedge, this method returns the halfedge that is part of the next manifold and which connects the same two mesh vertices. 

The manifold information may only be available in certain types of surface meshes, such as those generated by the :py:class:`ConstructSurfaceModifier` when the :py:attr:`identify_regions` option is turned on."""
        ...

    @overload
    def count_vertex_edges(self, vertex: int) -> int: # type: ignore
        """Returns the number of halfedges that leave *vertex*. See the code example for :py:meth:`next_vertex_edge` to learn how this method works."""
        ...

    @overload
    def count_vertex_edges(self, vertex: Sequence[int]) -> NDArray[numpy.int_]:
        """Returns the number of halfedges that leave *vertex*. See the code example for :py:meth:`next_vertex_edge` to learn how this method works."""
        ...

    @overload
    def count_face_edges(self, face: int) -> int: # type: ignore
        """Returns the number of halfedges that bound *face*. See the code example for :py:meth:`next_face_edge` to learn how this method works."""
        ...

    @overload
    def count_face_edges(self, face: Sequence[int]) -> NDArray[numpy.int_]:
        """Returns the number of halfedges that bound *face*. See the code example for :py:meth:`next_face_edge` to learn how this method works."""
        ...

    @overload
    def count_manifolds(self, edge: int) -> int: # type: ignore
        """Given a halfedge, this method returns the number of surface manifolds that meet at the edge. This is useful to identify triple junctions in a grain boundary network, for example. For halfedges that are located at the boundary of an open surface mesh, the method returns 1. For halfedges that are part of a regular two-sided manifold, the method returns 2."""
        ...

    @overload
    def count_manifolds(self, edge: Sequence[int]) -> NDArray[numpy.int_]:
        """Given a halfedge, this method returns the number of surface manifolds that meet at the edge. This is useful to identify triple junctions in a grain boundary network, for example. For halfedges that are located at the boundary of an open surface mesh, the method returns 1. For halfedges that are part of a regular two-sided manifold, the method returns 2."""
        ...

    @overload
    def find_edge(self, face: int, vertex1: int, vertex2: int) -> int: # type: ignore
        """Given a face, finds the halfedge of that face which leads from *vertex1* to *vertex2*. If no such halfedge exists, returns -1. 

This method can be used to quickly find the edge connecting two vertices of a face without the need to explicitly visit and check each edge bounding the face."""
        ...

    @overload
    def find_edge(self, face: Sequence[int], vertex1: Sequence[int], vertex2: Sequence[int]) -> NDArray[numpy.int_]:
        """Given a face, finds the halfedge of that face which leads from *vertex1* to *vertex2*. If no such halfedge exists, returns -1. 

This method can be used to quickly find the edge connecting two vertices of a face without the need to explicitly visit and check each edge bounding the face."""
        ...

@dataclass(kw_only=True)
class SurfaceMesh(DataObject):
    """Base: :py:class:`ovito.data.DataObject`

This data object type represents a surface in three-dimensional space, i.e., a two-dimensional manifold that is usually closed and orientable.
The underlying representation of the surface is a discrete mesh made of vertices, edges, and planar faces.
See the user manual page on surface meshes for more information on this data object type.

Surface meshes are typically produced by modifiers such as :py:class:`ConstructSurfaceModifier`,
:py:class:`CreateIsosurfaceModifier`, :py:class:`CoordinationPolyhedraModifier` or
:py:class:`VoronoiAnalysisModifier`.

Each surface mesh has a unique :py:attr:`~DataObject.identifier` by which it can be looked up in the
:py:attr:`DataCollection.surfaces` dictionary:

```python
  # Apply a CreateIsosurfaceModifier to a VoxelGrid to create a SurfaceMesh:
  pipeline.modifiers.append(CreateIsosurfaceModifier(operate_on='voxels:charge-density', property='Charge Density', isolevel=0.05))
  data = pipeline.compute()
  
  # The SurfaceMesh created by the modifier has the identifier 'isosurface':
  surface = data.surfaces['isosurface']
```

**Vertices, halfedges, and faces**

A surface mesh is made of a set of `vertices`, a set of directed `halfedges` each connecting two vertices, and
a set of `faces`, each formed by a circular sequence of halfedges. The connectivity information, i.e., which vertices
are connected by halfedges and which halfedges form the faces, is stored in the :py:attr:`topology` sub-object of the :py:class:`SurfaceMesh`.
See the :py:class:`SurfaceMeshTopology` class for more information.

Vertices and faces of the surface mesh can be associated with arbitrary property values, similar to how particles can
have arbitrary properties assigned to them in OVITO. These properties are managed by the :py:attr:`vertices` and :py:attr:`faces`
:py:class:`PropertyContainer` sub-objects of the surface mesh. The vertices of the mesh are always associated with the property named
``Position``, which stores the three-dimensional coordinates of each vertex, similar to the ``Position`` property of particles
in OVITO.

```python
  vertex_coords = surface.vertices['Position']
```

The :py:class:`SurfaceMeshVis` element, which is responsible for rendering the surface mesh,
provides the option to visualize local vertex and face property values using a color mapping scheme.

```python
  surface.vis.color_mapping_mode = SurfaceMeshVis.ColorMappingMode.Vertex
  surface.vis.color_mapping_property = 'Position.Z'
  surface.vis.color_mapping_interval = (min(vertex_coords[:,2]), max(vertex_coords[:,2]))
```

If you want to modify property values of the mesh, keep in mind that you have to use underscore notation,
for example:

```python
  data.surfaces['isosurface_'].vertices_['Position_'] += (xoffset, yoffset, zoffset)
```

Periodic simulation domains

A surface mesh may be embedded in a periodic domain, i.e. in a simulation cell with periodic boundary conditions.
That means edges and faces of the surface mesh can connect vertices on opposite sides of the simulation box and will wrap around correctly.
OVITO takes care of computing the intersections of such a periodic surface with the box boundaries and automatically produces a non-periodic
representation of the mesh when it comes to displaying the surface. If needed, you can explicitly request a non-periodic version of the mesh, which
was clipped at the periodic box boundaries, by calling the :py:meth:`to_triangle_mesh` method from a script.

The spatial domain of the surface mesh is the :py:class:`SimulationCell` object stored in its :py:attr:`SurfaceMesh.domain` field.
Note that this attached :py:class:`SimulationCell` may, in some situations, not be identical with the global simulation
:py:attr:`cell` set for the :py:class:`DataCollection`.

Spatial regions

If it is a closed, orientable manifold, the surface mesh subdivides three-dimensional space into separate spatial regions.
For example, if the surface mesh was constructed by the :py:class:`ConstructSurfaceModifier` from a set of input particles,
then the volume enclosed by the surface is the "filled" interior region and the exterior space is the "empty" region containing no particles.

In general, the :py:class:`SurfaceMesh` class manages a variable list of :py:attr:`regions`, each being identified by a numeric, zero-based index.
The :py:meth:`locate_point` method allows you to determine which spatial region some point in space belongs to.

A surface mesh may be degenerate, which means it contains no vertices and faces. In such a case there is only one spatial region filling the entire space. For example,
when there exist no input particles, the :py:class:`ConstructSurfaceModifier` is unable to construct a regular surface mesh and
the "empty" region fills the entire simulation cell. Conversely, if the periodic simulation cell is completely filled with particles,
the "filled" region covers the entire periodic simulation domain and the resulting surface mesh consists of no vertices or faces, i.e., it is also degenerate.
To discriminate between the two situations, the :py:class:`SurfaceMesh` class has a :py:attr:`space_filling_region` field, which specifies
the spatial region that fills the entire space in cases where the mesh is degenerate.

File export

A surface mesh may be exported to a geometry file in the form of a triangle mesh using OVITO's :py:func:`export_file` function.
To this end, a non-periodic version is produced by truncating triangles at the domain boundaries and generating "cap polygons" filling the holes that
occur at the intersection of the surface with periodic domain boundaries. The following example code writes a VTK geometry file
(``vtk/trimesh`` export format):

```python
  from ovito.io import import_file, export_file
  from ovito.data import SurfaceMesh
  from ovito.modifiers import ConstructSurfaceModifier
  
  # Load a particle set and construct the surface mesh:
  pipeline = import_file("input/simulation.dump")
  pipeline.modifiers.append(ConstructSurfaceModifier(radius = 2.8))
  mesh = pipeline.compute().surfaces['surface']
  
  # Export the mesh to a VTK file for visualization with ParaView.
  export_file(mesh, 'output/surface_mesh.vtk', 'vtk/trimesh')
```

Clipping planes

A set of *clipping planes* can be assigned to a :py:class:`SurfaceMesh` to clip away parts of the mesh for visualization purposes.
This may be useful to e.g. cut a hole into a closed surface allowing you to look inside the enclosed volume.
The :py:class:`SurfaceMesh` object manages a list of clipping planes, which is accessible through the :py:meth:`.get_clipping_planes` and :py:meth:`.set_clipping_planes`
methods. Note that the cut operations are non-destructive and get performed only on the transient, non-periodic version of the mesh generated during image rendering or when exporting
the mesh to a file. The original surface mesh is not affected.
The :py:class:`SliceModifier`, when applied to a :py:class:`SurfaceMesh`, performs the slice by simply adding a corresponding clipping plane
to the :py:class:`SurfaceMesh`. The actual truncation of the mesh happens later on, during the final visualization step, when a non-periodic version is computed."""
    domain: Optional[SimulationCell] = None
    'domain() -> Optional[SimulationCell]\n\nThe :py:class:`SimulationCell` describing the (possibly periodic) domain which this surface mesh is embedded in. Note that this cell generally is independent of and may be different from the :py:attr:`cell` found in the :py:class:`DataCollection`.'
    space_filling_region: int = 0
    'space_filling_region() -> int\n\nIndicates the index of the spatial region that fills the entire domain in case the surface is degenerate, i.e. the mesh has zero faces. The invalid index -1 is typically associated with the empty (exterior) region.'
    vis: ovito.vis.SurfaceMeshVis = ovito.vis.SurfaceMeshVis()
    'vis() -> Optional[ovito.vis.DataVis]\n\nThe :py:class:`DataVis` element currently associated with this data object. This object is responsible for\nvisually rendering the stored data. If set to ``None``, the data object remains non-visual and does not appear in\nrendered images or viewports. Additionally, note that the same :py:class:`DataVis` element may be assigned to\nmultiple data objects to synchronize their visual appearance.\n\nSee the :py:mod:`ovito.vis` module for a list of visual element types.'

    @property
    def domain_(self) -> Optional[SimulationCell]:
        ...

    @property
    def faces(self) -> PropertyContainer:
        """The :py:class:`PropertyContainer` storing the per-face properties of the mesh. 

In general, an arbitrary set of uniquely named properties may be associated with the faces of a surface mesh. OVITO defines the following *standard face properties*, which have a well-defined meaning and prescribed data layout: 

=================================================== ========== ===================================================
Standard property name                              Data type  Component names
=================================================== ========== ===================================================
:guilabel:`Color`                                   float32    R, G, B
:guilabel:`Region`                                  int32      
:guilabel:`Selection`                               int8       
=================================================== ========== ===================================================


The property ``Color`` can be set to give each face of the surface mesh an individual color. It overrides the uniform coloring otherwise controlled by the :py:class:`SurfaceMeshVis` element. 

The property ``Region`` links each face with the volumetric region of the :py:class:`SurfaceMesh` that it bounds (see description above). The values of this property are zero-based indices into the :py:attr:`regions` list of the mesh. 

The property ``Selection`` controls the selection state of each individual mesh face. This property is set by modifiers that create selections, such as :py:class:`ExpressionSelectionModifier`, and is used by modifiers that operate on the subset of currently selected faces, such as :py:class:`AssignColorModifier`. All faces whose ``Selection`` property has a non-zero value are part of the current selection set."""
        ...

    @property
    def faces_(self) -> PropertyContainer:
        ...

    @property
    def vertices(self) -> PropertyContainer:
        """The :py:class:`PropertyContainer` storing all per-vertex properties of the mesh, including the vertex coordinates. 

In general, an arbitrary set of uniquely named properties may be associated with the vertices of a surface mesh. OVITO defines the following *standard vertex properties*, which have a well-defined meaning and prescribed data layout: 

=================================================== ========== ===================================================
Standard property name                              Data type  Component names
=================================================== ========== ===================================================
:guilabel:`Color`                                   float32    R, G, B
:guilabel:`Position`                                float64    X, Y, Z
:guilabel:`Selection`                               int8       
=================================================== ========== ===================================================


The property ``Position`` is always present and stores the Cartesian vertex coordinates.

The property ``Color`` can be set to give each vertex of the surface mesh an individual color. It overrides the uniform coloring otherwise controlled by the :py:class:`SurfaceMeshVis` element. Vertex colors get interpolated across the mesh faces during rendering. 

The property ``Selection`` controls the selection state of each individual mesh vertex. This property is set by modifiers that create selections, such as :py:class:`ExpressionSelectionModifier`, and is used by modifiers that operate on the subset of currently selected vertices, such as :py:class:`AssignColorModifier`. All vertices whose ``Selection`` property has a non-zero value are part of the current selection set."""
        ...

    @property
    def vertices_(self) -> PropertyContainer:
        ...

    @property
    def regions(self) -> PropertyContainer:
        """The :py:class:`PropertyContainer` storing the properties of the spatial regions of the mesh. 

In general, an arbitrary set of uniquely named properties may be associated with the regions of a surface mesh. OVITO defines the following *standard region properties*, which have a well-defined meaning and prescribed data layout: 

=================================================== ========== ===================================================
Standard property name                              Data type  Component names
=================================================== ========== ===================================================
:guilabel:`Color`                                   float32    R, G, B
:guilabel:`Filled`                                  int8       
:guilabel:`Selection`                               int8       
:guilabel:`Surface Area`                            float64    
:guilabel:`Volume`                                  float64    
=================================================== ========== ===================================================


The property ``Color`` can be set to give the faces bounding each of the volumetric regions a different color. It overrides the uniform mesh coloring otherwise controlled by the :py:class:`SurfaceMeshVis` element. 

The property ``Filled`` is a flag indicating for each region whether it is an interior region, e.g. inside a solid, or an empty exterior region, e.g. outside the solid bounded by the surface. This property is created by the :py:class:`ConstructSurfaceModifier`. The same is true for the per-region properties ``Surface Area`` and ``Volume``."""
        ...

    @property
    def regions_(self) -> PropertyContainer:
        ...

    @property
    def topology(self) -> SurfaceMeshTopology:
        """A :py:class:`SurfaceMeshTopology` object storing the face connectivity of the mesh."""
        ...

    def get_clipping_planes(self) -> NDArray[numpy.float64]:
        """Returns an :math:`N \\times 4` array containing the definitions of the *N* clipping planes attached to this :py:class:`SurfaceMesh`. 

Each plane is defined by its unit normal vector and a signed displacement magnitude, which determines the plane's distance from the coordinate origin along the normal, giving four numbers per plane in total. Those parts of the surface mesh which are on the positive side of the plane (in the direction the normal vector) will be cut away during rendering. 

Note that the returned Numpy array is a copy of the internal data stored by the :py:class:`SurfaceMesh`."""
        ...

    def set_clipping_planes(self, planes: ArrayLike) -> None:
        """Sets the clipping planes of this :py:class:`SurfaceMesh`. The array *planes* must follow the same format as the one returned by :py:meth:`.get_clipping_planes`."""
        ...

    def get_face_adjacency(self) -> NDArray[numpy.int_]:
        ...

    def get_face_vertices(self, flat: bool=False) -> NDArray[numpy.int_]:
        """Returns an array with the vertex indices of all mesh faces. The parameter *flat* controls how the face vertices get returned by the function:

  **flat=False**:   If all :math:`n` faces of the mesh have the same, uniform number of vertices, :math:`m`, for example, if they are all triangles, then   a 2-d NumPy array of shape :math:`(n, m)` containing the zero-based vertex indices is returned. Otherwise, a list of lists is returned,   in which nested lists may have different lengths.   

  **flat=True**:   Returns a 1-d array with the vertex lists of all mesh faces stored back to back. A face's vertex list is preceded by the number of   vertices of that face. Then the actual vertex indices of the face follow. Then the number of vertices of the next face follows, and so on."""
        ...

    def locate_point(self, pos: ArrayLike, eps: float=1e-06, return_distances: bool=False) -> Union[Optional[int], Optional[Tuple[int, float]], NDArray[numpy.int_], Tuple[NDArray[numpy.int_], NDArray[numpy.float64]]]:
        """Determines which spatial region of the mesh contains the given point in 3-d space. Optionally the distance to the mesh can be returned.

The function returns the numeric ID of the region *pos* is located in. Note that region ID -1 is typically reserved for the empty exterior region, which, if it exists, is the one *not* containing any atoms or particles. Whether non-negative indices refer to only filled (interior) regions or also empty regions depends on the algorithm that created the surface mesh and its spatial regions. 

The parameter *eps* is a numerical precision threshold to detect if the query point is positioned exactly on the surface boundary, i.e. on the manifold separating two spatial regions. This condition is indicated by the special return value ``None``. Set *eps* to 0.0 to disable the point-on-boundary test. Then the method will never return ``None`` as a result, but the determination of the spatial region will become numerically unstable if the query point is positioned right on a boundary surface.

*return_distances* can be set to compute not only the containing region(s), but also the distance(s) from each input point to the closest point on the surface mesh. 

:param pos: The Cartesian xyz coordinates of the query point(s). Either a 1-D array of length 3 or a 2-D array of shape (*N*,3).
:param float eps: Numerical precision threshold for point-on-boundary test
:param bool return_distances: Request the computation of mesh-to-*pos* distances
:return:
   - If *pos* is a 1-D array: The numeric ID of the spatial region containing *pos*; or ``None`` if *pos* is exactly on the dividing boundary between two regions. 

     If *return_distances* is set, a tuple containing the computed region and the distance from *pos* to the mesh is returned. Both are ``None`` if *pos* is located right on a region boundary.
   - If *pos* is a 2-D array: A :class:`numpy.ma.MaskedArray` containing the numeric ID of the spatial region for each entry in *pos* is returned. Entries corresponding to positions exactly on the dividing boundary between two regions are masked. Internally, the masked values are set to the smallest valid integer, however, these values must not be used. The returned array is of shape (*N*,). 

     When *return_distances* is requested, a second masked array of the same shape is returned. This array contains the distance from each *pos* to the nearest location on the mesh. Masked values are set to 0.0."""
        ...

    def to_triangle_mesh(self) -> Tuple[TriangleMesh, Optional[TriangleMesh], NDArray[numpy.int_]]:
        """Converts the surface into a non-periodic :py:class:`TriangleMesh`. 

:return: (`trimesh`, `caps`, `facemap`)

- `trimesh`: A :py:class:`TriangleMesh` representing the surface geometry after clipping it at the periodic boundaries of the   :py:attr:`.domain` and any attached clipping planes (see :py:meth:`.get_clipping_planes`).
- `caps`: A :py:class:`TriangleMesh` containing the cap polygons generated at intersections of the periodic surface mesh   with boundaries of the simulation :py:attr:`.domain`. Will be ``None`` if the surface mesh has no attached :py:attr:`.domain`, the domain   is degenerate, or the surface mesh does not represent a closed manifold.
- `facemap`: A NumPy array of indices into the face list of this :py:class:`SurfaceMesh`, one for each triangular face of the   :py:class:`TriangleMesh` `trimesh`. This map lets you look up for each face of the output mesh what the corresponding face of the input surface mesh is."""
        ...

    def create_face(self, vertices: Sequence[int]) -> int:
        """Adds a new face to the mesh. *vertices* must be a sequence of two or more zero-based indices into the mesh's vertex list. The method creates a loop of halfedges connecting the given vertices to form a closed polygon. The zero-based index of the newly created face is returned. 

Code example:

```python
  # Add a new SurfaceMesh object to the DataCollection with unique object identifier 'quad'.
  # The simulation cell of the particle system is adopted also as domain of the SurfaceMesh.
  mesh = data.surfaces.create(identifier='quad', title='Quad', domain=data.cell)
  
  # Create 4 mesh vertices forming a quadrilateral.
  verts = [[0,0,0], [10,0,0], [10,10,0], [0,10,0]]
  mesh.create_vertices(verts)
  
  # Create a face connecting the 4 vertices.
  mesh.create_face([0,1,2,3])
  
  # Initialize the 'Color' property of the newly created face.
  mesh.faces.create_property('Color', data=[(1,0,0)])
```"""
        ...

    def create_faces(self, vertex_lists: Union[Sequence[Sequence[int]], ArrayLike]) -> int:
        """Adds several new polygonal faces to the mesh. 

:param vertex_lists: A sequence of sequences, one for each face to be created, which specify the vertex indices to be connected by the new mesh faces. May also be a two-dimensional array. 
:return: Index of the first newly created face. 

*vertex_lists* may be list of tuples for example. The following call creates a 3-sided and a 4-sided polygonal face:

```python
  mesh.create_faces([(0,1,2), (3,4,5,6)])
```

For best performance, pass a two-dimensional NumPy array to create multiple faces which all have the same number of vertices:

```python
  # Nx3 array [[0,1,2], [3,4,5], [6,7,8], ...] for connecting 3N vertices with triangle faces.
  triangle_list = numpy.arange(mesh.vertices.count).reshape((mesh.vertices.count//3, 3))
  mesh.create_faces(triangle_list)
```

A third option is to specify the faces as one linear array, in which each face's vertex list is prefixed with the number of vertices. For example, to create a 3-sided face *0-1-2* and a 4-sided face *3-4-5-6*, one would write:

```python
  mesh.create_faces(numpy.asarray([3,0,1,2,  4,3,4,5,6]))
```

Note that the data *must* be provided as a NumPy array in this case, not a Python list.

The :py:meth:`create_faces` method has two effects: It increments the mesh's `topology.face_count` and it extends the arrays in the mesh's :py:attr:`faces` property container, which stores all per-face properties. The method raises an error if any of the specified vertex indices does not exist in the mesh. That means you should first call :py:meth:`create_vertices` to add vertices to the mesh before creating faces referencing these vertices. 

Usage example:

```python
  # Add a new SurfaceMesh object to the DataCollection with unique object identifier 'tetrahedron'.
  # The simulation cell of the particle system is adopted also as domain of the SurfaceMesh.
  mesh = data.surfaces.create(identifier='tetrahedron', title='Tetrahedron', domain=data.cell)
  
  # Create 4 mesh vertices.
  verts = [[0,0,0], [10,0,0], [0,10,0], [0,0,10]]
  mesh.create_vertices(verts)
  
  # Create 4 triangular faces forming a tetrahedron.
  mesh.create_faces([[0,1,2], [0,2,3], [0,3,1], [1,3,2]])
  
  # Initialize the 'Color' property of the newly created faces with RGB values.
  mesh.faces.create_property('Color', data=[(1,0,0), (1,1,0), (0,0,1), (0,1,0)])
  
  # Make it a "closed" mesh, connecting the four faces to form a surface manifold.
  mesh.connect_opposite_halfedges()
```"""
        ...

    def create_vertices(self, coords: ArrayLike) -> int:
        """Adds a set of new vertices to the mesh. *coords* must be an :math:`n \\times 3` array specifying the xyz coordinates of the :math:`n` vertices to create. The coordinates will be copied into the ``Position`` vertex property, which is managed by the :py:attr:`vertices` property container. Furthermore, the :py:attr:`~SurfaceMeshTopology.vertex_count` value of the mesh's :py:attr:`topology` will be incremented by :math:`n`. 

Initially, the new vertices will not be associated with any faces. Use :py:meth:`create_face` or :py:meth:`create_faces` to create faces connecting the vertices."""
        ...

    def connect_opposite_halfedges(self) -> bool:
        """Links together pairs of halfedges in the mesh to form a two-dimensional manifold made of connected faces. For each halfedge :math:`a \\to b` the method tries to find the corresponding reverse halfedge :math:`b \\to a`, which bounds the adjacent face. The two halfedges are then linked together to form a pair. The method returns ``True`` to indicate that all halfedges of the mesh have been successfully associated with a corresponding opposite halfedge. In this case, the mesh is said to be *closed*, i.e., its faces form a contiguous manifold."""
        ...

@dataclass(kw_only=True)
class DislocationNetwork(DataObject):
    """Base: :py:class:`ovito.data.DataObject`

A network of dislocation lines extracted from a crystal model by the :py:class:`DislocationAnalysisModifier`.
The modifier stores the dislocation network in a pipeline's output data collection, from where it can be retrieved via
the :py:attr:`DataCollection.dislocations` field:

```python
  data = pipeline.compute()
  network = data.dislocations
```

The visual appearance of the dislocation lines in rendered images and videos is controlled by the associated :py:class:`DislocationVis` element.
You can access it as :py:attr:`~DataObject.vis` attribute of the :py:class:`DataObject` base class:

```python
  network.vis.line_width = 1.5
  network.vis.coloring_mode = DislocationVis.ColoringMode.ByBurgersVector
```

The :py:attr:`lines` list gives you access to the list of individual dislocations, which are all represented by
instances of the :py:class:`DislocationNetwork.Line` class. Furthermore, you can use the :py:meth:`find_nodes` method
to obtain a list of nodes at which dislocation lines are connected. These connections are represented by :py:class:`DislocationNetwork.Connector`
objects.

Code example

Complete script example for loading a molecular dynamics simulation, performing the DXA on a single snapshot, printing the list of extracted dislocation lines,
and exporting the dislocation network to disk:

```python
  from ovito.io import import_file, export_file
  from ovito.modifiers import DislocationAnalysisModifier
  from ovito.data import DislocationNetwork
  
  import ovito
  ovito.enable_logging()
  
  pipeline = import_file("input/simulation.dump")
  
  # Extract dislocation lines from a crystal with diamond structure:
  modifier = DislocationAnalysisModifier()
  modifier.input_crystal_structure = DislocationAnalysisModifier.Lattice.CubicDiamond
  pipeline.modifiers.append(modifier)
  data = pipeline.compute()
  
  total_line_length = data.attributes['DislocationAnalysis.total_line_length']
  cell_volume = data.attributes['DislocationAnalysis.cell_volume']
  print("Dislocation density: %f" % (total_line_length / cell_volume))
  
  # Print list of dislocation lines:
  print("Found %i dislocation lines" % len(data.dislocations.lines))
  for line in data.dislocations.lines:
      print("Dislocation %i: length=%f, Burgers vector=%s" % (line.id, line.length, line.true_burgers_vector))
      print(line.points)
  
  # Export dislocation lines to a CA file:
  export_file(pipeline, "output/dislocations.ca", "ca")
  
  # Or export dislocations to a ParaView VTK file:
  export_file(pipeline, "output/dislocations.vtk", "vtk/disloc")
```

File import and export

Dislocation networks can be exported as a set of polylines to the `legacy VTK file format <https://docs.vtk.org/en/latest/design_documents/VTKFileFormats.html>`__ using the
:py:func:`ovito.io.export_file` function (specify the "`vtk/disloc`" format).
During export to this file format, which does not support periodic boundary conditions, lines that
cross a periodic domain boundary get split (i.e., wrapped around) at the simulation box boundaries.

OVITO's native format for storing dislocation networks on disk is the CA file format,
a simple text-based format that supports periodic boundary conditions. This format can be written
by the :py:func:`export_file` function ("`ca`" format) and read by the :py:func:`import_file` function.
It stores the dislocation lines, their connectivity, as well as the "defect mesh" produced
by the :py:class:`DislocationAnalysisModifier`."""
    vis: ovito.vis.DislocationVis = ovito.vis.DislocationVis()
    'vis() -> Optional[ovito.vis.DataVis]\n\nThe :py:class:`DataVis` element currently associated with this data object. This object is responsible for\nvisually rendering the stored data. If set to ``None``, the data object remains non-visual and does not appear in\nrendered images or viewports. Additionally, note that the same :py:class:`DataVis` element may be assigned to\nmultiple data objects to synchronize their visual appearance.\n\nSee the :py:mod:`ovito.vis` module for a list of visual element types.'

    @property
    def lines(self) -> Sequence[Line]:
        """The list of dislocation lines in this dislocation network. This list-like object contains :py:class:`Line` objects in arbitrary order and is read-only."""
        ...

    def set_line(self, index: int, true_burgers_vector: Optional[ovito.vis.Vector3]=None, cluster_id: Optional[int]=None, points: Optional[ArrayLike]=None, custom_color: Optional[ovito.vis.Color]=None) -> None:
        """This method can be used to manipulate certain aspects of a :py:class:`Line` in the
network. Fields for which no new value is specified will keep their current values.

:param index: The zero-based index of the dislocation line to modify in the :py:attr:`lines` array.
:param true_burgers_vector: The new lattice-space Burgers vector (:py:attr:`true_burgers_vector`).
:param cluster_id: The numeric ID of the crystallite cluster the dislocation line is embedded in (:py:attr:`cluster_id`).
:param points: An :math:`(N, 3)` NumPy array of Cartesian coordinates containing the dislocation's vertices (:py:attr:`points`).
:param custom_color: RGB color to be used for rendering the line instead of the automatically determined color (:py:attr:`custom_color`).

Example of a user-defined modifier function that manipulates the dislocation line data:

```python
  import numpy as np
  
  def modify(frame: int, data: DataCollection):
  
      # Flip Burgers vector and line sense of each dislocation:
      for index, line in enumerate(data.dislocations.lines):
          data.dislocations_.set_line(index,
              true_burgers_vector = np.negative(line.true_burgers_vector),
              points = np.flipud(line.points))
  
      # Highlight all 1/6[121] dislocations by giving them a red color:
      for index, line in enumerate(data.dislocations.lines):
          if np.allclose(line.true_burgers_vector, (1/6, 2/6, 1/6)):
              data.dislocations_.set_line(index, custom_color=(1, 0, 0))
```"""
        ...

    def find_nodes(self) -> list[DislocationNetwork.Connector]:
        """Returns a list of all unique dislocation nodes in the network, each represented by a :py:class:`~DislocationNetwork.Connector`.

For a detailed description of what a "node" is, see the :py:class:`~DislocationNetwork.Connector` class.
The list returned by this method contains one (arbitrary) :py:class:`~DislocationNetwork.Connector` from each network node, in no particular order.
Each of these connectors serves as access into a node and can be used to visit the other connectors (dislocation arms) in the same node
via `Connector.arms`:

```python
  network = data.dislocations
  
  for node in network.find_nodes():
      print(node.arm_count, node.position)
      for arm in node.arms():
          print(arm.line.true_burgers_vector)
```"""
        ...

    class Line:
        """Describes a single continuous dislocation line that is part of a :py:class:`DislocationNetwork`.

A dislocation line is a curve in 3d space, approximated by a sequence of :py:attr:`points` connected by linear
line segments. You can query its total curve :py:attr:`length` or compute some location on the line
from a linear path coordinate *t* using the method :py:meth:`point_along_line`.
The line is terminated by two :py:attr:`connectors` at its two end points, which represent the connectivity of the dislocation
network.

A dislocation line is embedded in some crystallite (a region with uniform lattice orientation), which is identified by the numerical :py:attr:`cluster_id`.
All dislocation lines belonging to the same crystallite share the same lattice coordinate system in which their *true Burgers vectors*
are expressed. A line's :py:attr:`true_burgers_vector` is given in Bravais lattice units.

Each crystallite has a particular mean orientation within the global simulation coordinate system and a mean lattice parameter and
elastic strain. Applying these mean crystal properties to the :py:attr:`true_burgers_vector` yields the line's :py:attr:`spatial_burgers_vector`,
which is expressed in the global coordinate system shared by all dislocations of the entire :py:class:`DislocationNetwork`.
The :py:attr:`spatial_burgers_vector` is given in simulation coordinate units (typically Angstroms).

The :py:attr:`is_loop` property flag indicates that the two end points of the dislocation line form a 2-junction. This property does not necessarily
mean that the dislocation forms an actual *circular* loop. In simulations using periodic boundary conditions, a straight dislocation can also connect to itself
through the periodic cell boundaries and form an infinite periodic line. This situation is indicated by the :py:attr:`is_infinite_line` property, which implies that
the :py:attr:`is_loop` property is also true.

All fields of this class are read-only. To modify a dislocation line, you can use the :py:meth:`DislocationNetwork.set_line` method.

  Renamed this class from :py:class:`DislocationSegment` to :py:class:`DislocationNetwork.Line`."""

        @property
        def cluster_id(self) -> int:
            """The numeric identifier of the crystal cluster containing this dislocation line. A crystal cluster is the technical term for a contiguous group of atoms forming a spatial region with uniform lattice orientation, i.e., a *crystallite* or *grain*. 

The :py:attr:`true_burgers_vector` of the dislocation is expressed in the local coordinate system of the crystal cluster. The :py:attr:`spatial_burgers_vector` of the dislocation is computed by transforming the true Burgers vector with the mean elastic deformation gradient tensor of the crystal cluster."""
            ...

        @property
        def custom_color(self) -> ovito.vis.Color:
            """The RGB color value to be used for visualizing this particular dislocation line, overriding the default coloring scheme imposed by the `DislocationVis.coloring_mode` setting. The custom color is only used if its RGB components are non-negative (i.e. in the range 0-1); otherwise the line will be rendered using the computed color depending on the line's Burgers vector. 

Default: ``(-1.0, -1.0, -1.0)``"""
            ...

        @property
        def id(self) -> int:
            """The unique numerical identifier of this dislocation line within the :py:class:`DislocationNetwork`. This is simply the zero-based index of the line in the :py:attr:`DislocationNetwork.lines` list. 

Important: This identifier is derived from the arbitrary storage order of the lines in the network and *cannot* be used to identify the same dislocation in another simulation snapshot."""
            ...

        @property
        def is_infinite_line(self) -> bool:
            """Indicates that this dislocation is an infinite line passing through a periodic simulation box boundary. A dislocation is considered infinite if it is a closed loop but its start and end do not coincide (because they are located in different periodic images)."""
            ...

        @property
        def is_loop(self) -> bool:
            """Indicates whether this line forms a loop, i.e., its end is connected to its start point. Note that an infinite dislocation line passing through a periodic simulation cell boundary is also considered a logical loop (see :py:attr:`is_infinite_line` property)."""
            ...

        @property
        def length(self) -> float:
            """Computes the length of this dislocation line in simulation units of length, integrating the piecewise linear segments it is made of."""
            ...

        @property
        def points(self) -> ArrayLike:
            """The sequence of spatial points that define the curved shape of this dislocation (in simulation coordinates). This is a *N* x 3 Numpy array, with *N>2* being the number of points along the line. 

For true dislocation loops, the first and the last point in the list coincide exactly. For infinite lines, the first and the last point coincide modulo a periodicity vector of the simulation domain. 

The point sequence always forms a continuous line, which may lead outside the primary :py:class:`SimulationCell` if periodic boundary conditions (PBCs) are used, i.e., only the start of the dislocation is always inside the primary simulation cell but its end point may not. Thus, the line is stored in unwrapped form. A wrapping happens ad-hoc during visualization, when the :py:class:`DislocationVis` element renders the dislocation network or if the network is exported to a file format, e.g. VTK, which does not support PBCs."""
            ...

        @property
        def spatial_burgers_vector(self) -> ovito.vis.Vector3:
            """The Burgers vector of the segment, expressed in the global coordinate system of the simulation. This vector is calculated by transforming the true Burgers vector from the local lattice coordinate system to the global simulation coordinate system using the average orientation matrix of the crystal cluster the dislocation segment is embedded in."""
            ...

        @property
        def true_burgers_vector(self) -> ovito.vis.Vector3:
            """The Burgers vector of the dislocation expressed in the local coordinate system of the crystal the dislocation is located in. The true Burgers vectors of two dislocation lines may only be added if both belong to the same :py:attr:`cluster_id`."""
            ...

        def point_along_line(self, t: float) -> ovito.vis.Vector3:
            """Returns the Cartesian coordinates of a point on the dislocation line. The location to be calculated must be specified in the form of a fractional position *t* along the continuous dislocation line. 

:param t: Normalized path coordinate in the range [0,1]
:return: The xyz coordinates of the requested point on the dislocation line."""
            ...

        @property
        def connectors(self) -> Tuple[DislocationNetwork.Connector, DislocationNetwork.Connector]:
            """.. image:: /introduction/graphics/dislocation_connectors.png
  :width: 50%
  :align: right

A tuple of two :py:class:`~DislocationNetwork.Connector` objects representing the two end points of the dislocation line. The first connector is located at the start of the line (its tail), the second connector at the end of the line (its head)."""
            ...

    class Connector:
        """.. figure:: /introduction/graphics/dislocation_connectors.png
  :figwidth: 50%
  :align: right

  A dislocation :py:class:`~DislocationNetwork.Line` has two end point :py:attr:`~DislocationNetwork.Line.connectors` and
  is described by a sequence of spatial :py:attr:`~DislocationNetwork.Line.points`.

A connector object represents one of the two end points of each
:py:class:`~DislocationNetwork.Line` in the network. In other words,
every :py:class:`~DislocationNetwork.Line` has exactly two unique
:py:class:`~DislocationNetwork.Connector` objects belonging to the line.
This pair is accessible via the `Line.connectors`
attribute. Since dislocations always have a direction (their line sense, with respect to which their Burgers vector is defined),
one connector is located at the "head" (forward) and one at the "tail" (backward) end of the directed line.

.. figure:: /introduction/graphics/dislocation_node.png
  :figwidth: 50%
  :align: right

  Three dislocation arms meet at a dislocation node (junction).
  The node is formed by a circular linked-list of connectors.

A dislocation network node (junction) is formed by several connectors located at the same point in space,
as illustrated in the figure to the right. This node structure consists of three interlinked connectors belonging
to the three dislocation arms meeting in the node. Dislocation arms can be either inbound or outbound.

Network nodes may consist of one, two, three or more connectors:

#. A single connector, only interlinked with itself, represents a dangling line end.
   They occur when a dislocation terminates in another extended crystal defect, such as a grain boundary or free surface.
#. A 2-node, consisting of two interlinked connectors, is part of a dislocation loop or infinite periodic dislocation line.
   They occur when a dislocation line is closed on itself, i.e, its head and tail are connected.
#. A node with three or more connectors represents a physical dislocation junction, where three or more arms with non-zero Burgers vector meet.

The connectors belonging to the same network node are interlinked with each other in the form of a circular linked list.
The `Connector.next_arm` field leads to the next connector in the circular list. The last connector of the node
points back to the first connector of the node. This way, all connectors can be visited by starting from
any connector of the node and following the `Connector.next_arm` field until the starting connector is reached again.
For a 1-node (a dangling line end), the `Connector.next_arm` field points to itself.

The `Connector.arms` method yields a list of all connectors belonging to the same local node as this connector
(including the connector itself). The `Connector.arm_count` property counts the number of connectors in the local node.
The `Connector.line` field points to the :py:class:`~DislocationNetwork.Line` object that the connector belongs to.

The :py:meth:`DislocationNetwork.find_nodes` method can be used to generate a list of :py:class:`~DislocationNetwork.Connector`
objects, one for each node in the network. It is useful if you want to iterate over all unique nodes in the network."""

        @property
        def line(self) -> DislocationNetwork.Line:
            """The dislocation :py:class:`~DislocationNetwork.Line` this connector belongs to."""
            ...

        @property
        def is_head(self) -> bool:
            """True if the connector is located at the head of its dislocation line, i.e., ``self`` is ``self.line.connectors[1]``. Then the connected :py:class:`~DislocationNetwork.Line` is inbound on the node."""
            ...

        @property
        def is_tail(self) -> bool:
            """True if the connector is located at the tail of its dislocation line, i.e., ``self`` is ``self.line.connectors[0]``. Then the connected :py:class:`~DislocationNetwork.Line` is outbound from the node."""
            ...

        @property
        def position(self) -> NDArray[numpy.float64]:
            """The Cartesian coordinates of the connector in the global simulation coordinate system. This corresponds to the start or end point of the dislocation, i.e., either ``self.line.points[0]`` or ``self.line.points[-1]``."""
            ...

        @property
        def arm_count(self) -> int:
            """The number of arms meeting at the node formed by this connector and others, including the connector itself."""
            ...

        def arms(self) -> list[DislocationNetwork.Connector]:
            """This method builds a list of :py:class:`Connector` objects representing the arms connected to the node. Each connector object links to a different dislocation line incident to the network node."""
            ...

        @property
        def next_arm(self) -> DislocationNetwork.Connector:
            """The :py:class:`~DislocationNetwork.Connector` belonging to the next dislocation line incident to the node."""
            ...

@dataclass(kw_only=True)
class TriangleMesh(DataObject):
    """Base: :py:class:`ovito.data.DataObject`

This data object type stores a three-dimensional mesh made of vertices and triangular faces. 
Such a mesh can describe general polyhedral geometry to be visualized side by side with the particle simulation data. 

Typically, triangle meshes are imported from external geometry data files (e.g. STL, OBJ, VTK formats) using the :py:func:`import_file` function.
See also the corresponding section of the OVITO user manual. 
All triangle meshes in a data collection are accessible through the `DataCollection.triangle_meshes`
dictionary view.

Note that the :py:class:`SurfaceMesh` class is a second object type that can represent surface geometries, just like a :py:class:`TriangleMesh`.
In contrast to triangle meshes, surface meshes may be embedded in periodic simulation domains and are closed manifolds in most cases.
Furthermore, surface meshes can store arbitrary per-vertex and per-face property values -- something triangle meshes cannot do.
A triangle mesh is a more low-level data structure, which can be sent directly to a GPU for rendering. A surface mesh, in contrast,
is a more high-level data structure, which gets automatically converted to a triangle mesh for visualization.

The visual appearance of the triangle mesh is controlled through the attached :py:class:`TriangleMeshVis`
element (see `DataObject.vis` field of base class).

A triangle mesh consists of :math:`n_{\\mathrm{v}}` vertices and :math:`n_{\\mathrm{f}}` triangular faces. These counts are exposed by the class as attributes
:py:attr:`.vertex_count` and :py:attr:`.face_count`. Each face connects three vertices of the mesh, and several faces may share a vertex. 
The faces are stored as triplets of zero-based indices into the vertex list."""
    vis: ovito.vis.TriangleMeshVis = ovito.vis.TriangleMeshVis()
    'vis() -> Optional[ovito.vis.DataVis]\n\nThe :py:class:`DataVis` element currently associated with this data object. This object is responsible for\nvisually rendering the stored data. If set to ``None``, the data object remains non-visual and does not appear in\nrendered images or viewports. Additionally, note that the same :py:class:`DataVis` element may be assigned to\nmultiple data objects to synchronize their visual appearance.\n\nSee the :py:mod:`ovito.vis` module for a list of visual element types.'
    vertex_count: int = 0
    'vertex_count() -> int\n\nThe number of vertices of the mesh, :math:`n_{\\mathrm{v}}`.\n\nDefault: 0'
    face_count: int = 0
    'face_count() -> int\n\nThe number of triangular faces of the mesh, :math:`n_{\\mathrm{f}}`.\n\nDefault: 0'

    def get_vertices(self) -> NDArray[numpy.float64]:
        """Returns the xyz coordinates of the vertices of the mesh as a NumPy array of shape :math:`(n_{\\mathrm{v}}, 3)`. The returned array holds a copy of the internal vertex data managed by the :py:class:`TriangleMesh`."""
        ...

    def get_faces(self) -> NDArray[numpy.int_]:
        """Returns the list of triangles of the mesh as a NumPy array of shape :math:`(n_{\\mathrm{f}}, 3)`. The array contains for each face three zero-based indices into the mesh's vertex list as returned by :py:meth:`get_vertices`. The returned face array holds a copy of the internal data managed by the :py:class:`TriangleMesh`."""
        ...

    def get_edge_visibility(self) -> NDArray[numpy.int_]:
        ...

    def get_vertex_colors(self) -> Optional[NDArray[numpy.float64]]:
        ...

    def get_vertex_pseudocolors(self) -> Optional[NDArray[numpy.float64]]:
        ...

    def get_face_colors(self) -> Optional[NDArray[numpy.float64]]:
        ...

    def get_face_pseudocolors(self) -> Optional[NDArray[numpy.float64]]:
        ...

    def get_normals(self) -> Optional[NDArray[numpy.float64]]:
        ...

    def set_vertices(self, coordinates: ArrayLike) -> None:
        """Sets the xyz coordinates of the vertices of the mesh. *coordinates* must be an array-like object of shape :math:`(n_{\\mathrm{v}}, 3)`. The :py:class:`TriangleMesh` copies the data from the array into its internal storage. If necessary, the value of :py:attr:`.vertex_count` is automatically adjusted to match the first dimension of the *coordinates* array."""
        ...

    def set_faces(self, vertex_indices: ArrayLike) -> None:
        """Sets the faces of the mesh. *vertex_indices* must be an array-like object of shape :math:`(n_{\\mathrm{f}}, 3)` containing one integer triplet per triangular face. Each integer is a zero-based index into the mesh's vertex list. The :py:class:`TriangleMesh` copies the data from the array into its internal storage. If necessary, the value of :py:attr:`.face_count` is automatically adjusted to match the first dimension of the *vertex_indices* array."""
        ...

    def set_edge_visibility(self, edge_flags: ArrayLike) -> None:
        ...

    def set_vertex_colors(self, colors: Optional[ArrayLike]) -> None:
        ...

    def set_vertex_pseudocolors(self, pseudocolors: Optional[ArrayLike]) -> None:
        ...

    def set_face_colors(self, colors: Optional[ArrayLike]) -> None:
        ...

    def set_face_pseudocolors(self, pseudocolors: Optional[ArrayLike]) -> None:
        ...

    def set_normals(self, normals: Optional[ArrayLike]) -> None:
        ...

    def flip_faces(self) -> None:
        ...

    def determine_edge_visibility(self, threshold: float=0.349) -> None:
        ...

    def remove_duplicate_vertices(self, eps: float) -> None:
        ...

    def clip_at_plane(self, normal: ovito.vis.Vector3, dist: float) -> None:
        ...

    def save_as_obj(self, path: os.PathLike) -> None:
        ...

@dataclass(kw_only=True)
class Lines(PropertyContainer):
    """Base: :py:class:`ovito.data.PropertyContainer`

The :py:class:`Lines` class represents one or more 3D polylines. You can create an instance of this
class in a :py:class:`DataCollection` using the :py:class:`GenerateTrajectoryLinesModifier` or the
`DataCollection.lines.create()` method.

You can retrieve existing :py:class:`Lines` objects from a pipeline's output through the
`DataCollection.lines` dictionary view. Each :py:class:`Lines` object
has a unique :py:attr:`identifier` name that serves as a lookup key.

:py:class:`Lines` objects are always associated with :py:class:`LinesVis` element, which controls the visual
appearance of the lines in rendered images. You can access the visual element through the :py:attr:`~DataObject.vis`
attribute provided by the :py:class:`DataObject` base class. The :py:class:`LinesVis` element provides the capability to visualize
a local quantity defined at each line vertex using pseudo-color mapping.

The :py:class:`Lines` container uses the following standard properties with predefined names and data layouts.
Additional per-vertex properties may be added using the :py:meth:`~PropertyContainer.create_property`
method of the base class.

================================ ======================== ========== =======================
Property name                    Python access            Data type  Component names
================================ ======================== ========== =======================
:guilabel:`Color`                :py:attr:`colors`        float32    R, G, B
:guilabel:`Position`             :py:attr:`positions`     float64    X, Y, Z
:guilabel:`Section`              :py:attr:`sections`      int64
:guilabel:`Selection`            :py:attr:`selection`     int8
:guilabel:`Time`                 :py:attr:`time_stamps`   int32
================================ ======================== ========== =======================

Data model

The :py:class:`Lines` data object type is :py:class:`PropertyContainer`, which means
it consist of a set of uniform property arrays. The standard property :guilabel:`Position` stores the
vertex coordinates. When rendering a :py:class:`Lines` object, consecutive vertices
get connected by linear line segments to form a *polyline* (which is also called a line "section").
To denote the end of a contiguous polyline and the start of a new one, the property :guilabel:`Section` must be filled with *section identifiers*,
which mark consecutive sequences of vertices, each forming a separate polyline. Section identifiers are arbitrary integer numbers
that stay the same within a polyline but change from one polyline to the next:

.. image:: ../introduction/graphics/lines_example_definition.svg
   :width: 30%
   :align: right

.. table:: Example: Definition of three polylines of length 2, 3, and 2, respectively.
    :align: center
    :width: 60%

    ========= ================================ ========================
    Vertex    Position                         Section
    ========= ================================ ========================
    0         :math:`(x_0, y_0, z_0)`          0
    1         :math:`(x_1, y_1, z_1)`          0
    2         :math:`(x_2, y_2, z_2)`          1
    3         :math:`(x_3, y_3, z_3)`          1
    4         :math:`(x_4, y_4, z_4)`          1
    5         :math:`(x_5, y_5, z_5)`          2
    6         :math:`(x_6, y_6, z_6)`          2
    ========= ================================ ========================

The :py:class:`Lines` class provides the :py:meth:`create_line` method to add new polylines to the container.
It appends a list of vertex coordinates to the :guilabel:`Position` property and automatically assigns a new unique
:guilabel:`Section` value to these vertices to define a new polyline.

If the :guilabel:`Time` property is present, it stores the animation frame number at which each
vertex should appear in a trajectory animation. This feature allows to animate the lines over time
and is used for particle trajectory visualization. The gradual rendering of lines is only active if the
`LinesVis.upto_current_time` option is enabled in the attached visual element."""
    vis: ovito.vis.LinesVis = ovito.vis.LinesVis()
    'vis() -> Optional[ovito.vis.DataVis]\n\nThe :py:class:`DataVis` element currently associated with this data object. This object is responsible for\nvisually rendering the stored data. If set to ``None``, the data object remains non-visual and does not appear in\nrendered images or viewports. Additionally, note that the same :py:class:`DataVis` element may be assigned to\nmultiple data objects to synchronize their visual appearance.\n\nSee the :py:mod:`ovito.vis` module for a list of visual element types.'

    @property
    def positions(self) -> Property:
        """The :py:class:`Property` array containing the XYZ coordinates of the line vertices (standard property :guilabel:`Position`). May be ``None`` if the property is not defined yet. Use :py:meth:`~PropertyContainer.create_property` to add the property to the container if necessary. Use :py:attr:`positions_` (with an underscore) to access an independent copy of the array, whose contents can be safely modified in place."""
        ...

    @property
    def positions_(self) -> Property:
        ...

    @property
    def colors(self) -> Property:
        """The :py:class:`Property` data array for the :guilabel:`Color` standard (line property); or ``None`` if that property is undefined."""
        ...

    @property
    def colors_(self) -> Property:
        ...

    @property
    def sections(self) -> Property:
        """The :py:class:`Property` array with the section each line vertex belongs to (standard property :guilabel:`Section`). May be ``None`` if the property is not defined yet. Use :py:meth:`~PropertyContainer.create_property` to add the property to the container if necessary. Use :py:attr:`sections_` (with an underscore) to access an independent copy of the array, whose contents can be safely modified in place."""
        ...

    @property
    def sections_(self) -> Property:
        ...

    @property
    def selection(self) -> Property:
        """The :py:class:`Property` data array for the ``Selection`` standard line vertices property; or ``None`` if that property is undefined."""
        ...

    @property
    def selection_(self) -> Property:
        ...

    @property
    def time_stamps(self) -> Property:
        """The :py:class:`Property` array with the time stamps of the line vertices (standard property :guilabel:`Time`). May be ``None`` if the property is not defined yet. Use :py:meth:`~PropertyContainer.create_property` to add the property to the container if necessary. Use :py:attr:`time_stamps_` (with an underscore) to access an independent copy of the array, whose contents can be safely modified in place."""
        ...

    @property
    def time_stamps_(self) -> Property:
        ...

    def create_line(self, positions: ArrayLike) -> int:
        """Adds a new section (a *polyline*) to an existing :py:class:`Lines` container. The container's vertex :py:attr:`count`
will be incremented by the number of newly inserted points. The method copies *positions* into the ``Position`` property array after extending the array
and gives the new polyline a unique ``Section`` property value.

:param positions: The xyz coordinates for the new lines section (*N* x 3 array). *N* must be at least 2.
:return: The unique section identifier that was assigned to the newly added polyline."""
        ...

@dataclass(kw_only=True)
class Vectors(PropertyContainer):
    """Base: :py:class:`ovito.data.PropertyContainer`

The :py:class:`Vectors` class represents a set of 3D arrow glyphs rendered by a :py:class:`VectorVis`
visual element. You can add an instance of this class to a :py:class:`DataCollection` using the
`DataCollection.vectors.create()` method.

You can retrieve existing :py:class:`Vectors` objects from a pipeline's output data collection through the
`DataCollection.vectors` dictionary view. Each :py:class:`Vectors` object
has a unique :py:attr:`identifier` name that serves as a lookup key.

:py:class:`Vectors` objects are always associated with :py:class:`VectorVis` element, which
controls the visual appearance of the arrows in rendered images. You can access the visual element
through the :py:attr:`~DataObject.vis` attribute provided by the :py:class:`DataObject` base class.
The :py:class:`VectorVis` element provides the capability to visualize a scalar quantity associated with
each vector using pseudo-color mapping.

The :py:class:`Vectors` container uses the following standard properties with predefined names and data layouts.
Additional per-arrow properties may be added using the :py:meth:`~PropertyContainer.create_property`
method of the base class.

================================ ================================ ========== =======================
Property name                    Python access                    Data type  Component names
================================ ================================ ========== =======================
:guilabel:`Color`                :py:attr:`colors`                float32    R, G, B
:guilabel:`Direction`            :py:attr:`directions`            float64    X, Y, Z
:guilabel:`Position`             :py:attr:`positions`             float64    X, Y, Z
:guilabel:`Selection`            :py:attr:`selection`             int8
:guilabel:`Transparency`         :py:attr:`transparencies`        float32
================================ ================================ ========== ======================="""
    vis: ovito.vis.VectorVis = ovito.vis.VectorVis()
    'vis() -> Optional[ovito.vis.DataVis]\n\nThe :py:class:`DataVis` element currently associated with this data object. This object is responsible for\nvisually rendering the stored data. If set to ``None``, the data object remains non-visual and does not appear in\nrendered images or viewports. Additionally, note that the same :py:class:`DataVis` element may be assigned to\nmultiple data objects to synchronize their visual appearance.\n\nSee the :py:mod:`ovito.vis` module for a list of visual element types.'

    @property
    def colors(self) -> Property:
        """The :py:class:`Property` data array for the ``Color`` standard vector property; or ``None`` if that property is undefined. Use :py:meth:`~PropertyContainer.create_property` to add the property to the container if necessary. Use :py:attr:`colors_` (with an underscore) to access an independent copy of the array, whose contents can be safely modified in place."""
        ...

    @property
    def colors_(self) -> Property:
        ...

    @property
    def directions(self) -> Property:
        """The :py:class:`Property` array containing the XYZ components of the vectors (standard property :guilabel:`Direction`). May be ``None`` if the property is not defined yet. Use :py:meth:`~PropertyContainer.create_property` to add the property to the container if necessary. Use :py:attr:`directions_` (with an underscore) to access an independent copy of the array, whose contents can be safely modified in place."""
        ...

    @property
    def directions_(self) -> Property:
        ...

    @property
    def positions(self) -> Property:
        """The :py:class:`Property` array containing the XYZ coordinates of vectors' base points (standard property :guilabel:`Position`). May be ``None`` if the property is not defined yet. Use :py:meth:`~PropertyContainer.create_property` to add the property to the container if necessary. Use :py:attr:`positions_` (with an underscore) to access an independent copy of the array, whose contents can be safely modified in place."""
        ...

    @property
    def positions_(self) -> Property:
        ...

    @property
    def selection(self) -> Property:
        """The :py:class:`Property` data array for the ``Selection`` standard vectors property; or ``None`` if that property is undefined."""
        ...

    @property
    def selection_(self) -> Property:
        ...

    @property
    def transparencies(self) -> Property:
        """The :py:class:`Property` array containing the transparency values (standard property :guilabel:`Transparency`) of the vectors. May be ``None`` if the property is not defined yet. Use :py:meth:`~PropertyContainer.create_property` to add the property to the container if necessary. Use :py:attr:`transparencies_` (with an underscore) to access an independent copy of the array, whose contents can be safely modified in place."""
        ...

    @property
    def transparencies_(self) -> Property:
        ...

class CutoffNeighborFinder:
    """A utility class that computes particle neighbor lists.

This class lets you iterate over all neighbors of a particle that are located within a specified spherical cutoff.
You can use it to build neighbor lists or perform computations that require neighbor vector information.

The constructor takes a positive cutoff radius and a `DataCollection`
providing the input particles and the :py:class:`SimulationCell` (needed for periodic systems).

Once the :py:class:`CutoffNeighborFinder` has been constructed, you can call its :py:meth:`.find` method to
iterate over the neighbors of a particle, for example:

```python
  from ovito.io import import_file
  from ovito.data import CutoffNeighborFinder
  
  # Load input simulation file.
  pipeline = import_file("input/simulation.dump")
  data = pipeline.compute()
  
  # Initialize neighbor finder object:
  cutoff = 3.5
  finder = CutoffNeighborFinder(cutoff, data)
  
  # Prefetch the property array containing the particle type information:
  ptypes = data.particles.particle_types
  
  # Loop over all particles:
  for index in range(data.particles.count):
      print("Neighbors of particle %i:" % index)
  
      # Iterate over the neighbors of the current particle:
      for neigh in finder.find(index):
          print(neigh.index, neigh.distance, neigh.delta, neigh.pbc_shift)
  
          # The index can be used to access properties of the current neighbor, e.g.
          type_of_neighbor = ptypes[neigh.index]
```

Note: If you want to determine the *N* nearest neighbors of a particle,
use the :py:class:`NearestNeighborFinder` class instead."""

    class Neighbor:
        index: int
        distance: float
        distance_squared: float
        delta: Tuple[float, float, float]
        pbc_shift: Tuple[int, int, int]

    def __init__(self, cutoff: float, data_collection: DataCollection) -> None:
        """This is the constructor."""
        ...

    def find(self, index: int) -> Iterator[Neighbor]:
        """Returns an iterator over all neighbors of the given particle.

:param index: The zero-based index of the central particle whose neighbors should be enumerated.
:returns: A Python iterator that visits all neighbors of the central particle within the cutoff distance.
          For each neighbor the iterator returns an object with the following property fields:

              * index: The zero-based global index of the current neighbor particle.
              * distance: The distance of the current neighbor from the central particle.
              * distance_squared: The squared neighbor distance.
              * delta: The three-dimensional vector connecting the central particle with the current neighbor (taking into account periodicity).
              * pbc_shift: The periodic shift vector, which specifies how often each periodic boundary of the simulation cell is crossed when going from the central particle to the current neighbor.

The `index` value returned by the iterator can be used to look up properties of the neighbor particle, as demonstrated in the example above.

Note that all periodic images of particles within the cutoff radius are visited. Thus, the same particle index may appear multiple times in the neighbor
list of the central particle. In fact, the central particle may be among its own neighbors in a small periodic simulation cell.
However, the computed vector (``delta``) and PBC shift (``pbc_shift``) will be unique for each visited image of the neighbor particle."""
        ...

    def find_at(self, coords: ovito.vis.Vector3) -> Iterator[Neighbor]:
        """Returns an iterator over all particles located within the spherical range of the given center position. In contrast to :py:meth:`find` this method can search for neighbors around arbitrary
spatial locations, which don't have to coincide with any physical particle position.

:param coords: A (x,y,z) coordinate triplet specifying the center location around which to search for particles.
:returns: A Python iterator enumerating all particles within the cutoff distance.
          For each neighbor the iterator returns an object with the following properties:

              * index: The zero-based global index of the current neighbor particle.
              * distance: The distance of the current particle from the center position.
              * distance_squared: The squared distance.
              * delta: The three-dimensional vector from the center to the current neighbor (taking into account periodicity).
              * pbc_shift: The periodic shift vector, which specifies how often each periodic boundary of the simulation cell is crossed when going from the center point to the current neighbor.

The index value returned by the iterator can be used to look up properties of the neighbor particle, as demonstrated in the example above.

Note that all periodic images of particles within the cutoff radius are visited. Thus, the same particle index may appear multiple times in the neighbor list.
However, the computed vector (``delta``) and image offset (``pbc_shift``) will be unique for each visited image of a neighbor particle."""
        ...

    def find_all(self, indices: Optional[ArrayLike]=None, sort_by: Optional[str]=None) -> Tuple[NDArray[numpy.int64], NDArray[numpy.float64]]:
        """This is a vectorized version of the :py:meth:`find` method, computing the neighbor lists and neighbor vectors of several particles in a single operation.
Thus, this method can help you avoid a slow, nested Python loop in your code and it will make use of all available processor cores.
You can request the neighbor lists for the whole system in one go, or just for a specific subset of particles given by *indices*.

The method produces a uniform array of neighbor list entries. Each entry comprises a pair of indices, i.e. the central particle and one of its neighboring particles within the cutoff distance,
and the corresponding spatial neighbor vector in 3d Cartesian coordinates. For best performance, the method returns all neighbors of all particles as one large
array, which is unsorted by default (*sort_by* = ``None``). That means the neighbors of central particles will *not* form contiguous blocks in the output array;
entries belonging to different central particles may rather appear in intermingled order!

Set *sort_by* to ``'index'`` to request grouping the entries in the output array based on the central particle index.
That means each particle's neighbor list will be output as a contiguous block. All blocks are stored back-to-back in the output array
in ascending order of the central particle index or, if parameter *indices* was specified, in that order.
The ordering of neighbor entries within each block will still be arbitrary though. To change this, set *sort_by* to ``'distance'``, which additionally
sorts the neighbors of each particle by increasing distance.

The method returns two NumPy arrays:

``neigh_idx`` : Array of shape (*M*, *2*) containing pairs of indices of neighboring particles, with *M* equal to the
total number of neighbors in the system. Note that the array will contain symmetric entries (*a*, *b*) and (*b*, *a*) if
neighbor list computation was requested for both particles *a* and *b* and they are within reach of each other.

``neigh_vec`` : Array of shape (*M*, 3) containing the xyz components of the Cartesian neighbor vectors ("delta"),
which connect the *M* particle pairs stored in ``neigh_idx``.

:param indices: List of zero-based indices of central particles for which the neighbor lists should be computed.
                If left unspecified, neighbor lists will be computed for every particle in the system.
:param sort_by: One of *"index"* or *"distance"*. Requests ordering of the output arrays based on central particle index and, optionally, neighbor distance.
                If left unspecified, neighbor list entries will be returned in arbitrary order.

:returns: ``(neigh_idx, neigh_vec)``"""
        ...

    def neighbor_distances(self, index: int) -> NDArray[numpy.float64]:
        """Returns the list of distances between some central particle and all its neighbors within the cutoff range. 

:param index: The 0-based index of the central particle whose neighbors should be enumerated.
:returns: NumPy array containing the radial distances to all neighbor particles within the cutoff range (in arbitrary order).

This method is equivalent to the following code, but performance is typically a lot better::

   def neighbor_distances(index):
       distances = []
       for neigh in finder.find(index):
           distances.append(neigh.distance)
       return numpy.asarray(distances)"""
        ...

    def neighbor_vectors(self, index: int) -> NDArray[numpy.float64]:
        """Returns the list of vectors from some central particle to all its neighbors within the cutoff range. 

:param index: The 0-based index of the central particle whose neighbors should be enumerated.
:returns: Two-dimensional NumPy array containing the vectors to all neighbor particles within the cutoff range (in arbitrary order).


The method is equivalent to the following code, but performance is typically a lot better::

   def neighbor_vectors(index):
       vecs = []
       for neigh in finder.find(index):
           vecs.append(neigh.delta)
       return numpy.asarray(vecs)"""
        ...

class NearestNeighborFinder:
    """A utility class that finds the *N* nearest neighbors of a particle or around some other spatial query point.

The class constructor takes the requested number of nearest neighbors, *N*, and a `DataCollection`
containing the input particles and the optional simulation cell.
*N* must be a positive integer not greater than 64, which is the maximum number of neighbors supported by this class.

Once the :py:class:`NearestNeighborFinder` has been initialized, you can call its :py:meth:`.find` method to
iterate over the sorted list of nearest neighbors of a given central particle:

```python
  # Set up a neighbor finder for visiting the 12 closest neighbors of each particle.
  finder = NearestNeighborFinder(12, data)
  
  # Loop over all input particles:
  for index in range(data.particles.count):
      print("Nearest neighbors of particle %i:" % index)
      # Iterate over the neighbors of the current particle, starting with the closest:
      for neigh in finder.find(index):
          print(neigh.index, neigh.distance, neigh.delta)
          # The index can be used to access properties of the current neighbor, e.g.
          type_of_neighbor = data.particles.particle_types[neigh.index]
```

In addition, the class provides the :py:meth:`find_at` method, which determines the *N* nearest particles around some
arbitrary spatial location:

```python
  # Visit particles closest to some spatial point (x,y,z):
  xyz_coords = (0.0, 0.0, 0.0)
  for neigh in finder.find_at(xyz_coords):
      print(neigh.index, neigh.distance, neigh.delta)
```

The corresponding :py:meth:`find_all` and :py:meth:`find_all_at` methods allow you to perform these queries efficiently
for multiple particles or spatial locations at once."""

    class Neighbor:
        index: int
        distance: float
        distance_squared: float
        delta: Tuple[float, float, float]

    def __init__(self, N: int, data_collection: DataCollection) -> None:
        """Initializes the neighbor finder facility."""
        ...

    def find(self, index: int) -> Iterator[Neighbor]:
        """Returns an iterator that visits the *N* nearest neighbors of the given particle in order of ascending distance.

:param index: The zero-based index of the central particle whose neighbors should be determined.
:returns: A Python iterator that visits the *N* nearest neighbors of the central particle in order of ascending distance.
          For each neighbor being visited, the iterator returns an object having the following attributes:

              * index: The global index of the current neighbor particle.
              * distance: The distance of the current neighbor from the central particle.
              * distance_squared: The squared neighbor distance.
              * delta: The three-dimensional vector connecting the central particle with the current neighbor (correctly taking into account periodic boundary conditions).

The index can be used to look up properties of the neighbor particle, as demonstrated in the first example code above.

Note that several periodic images of the same particle may be visited if the periodic simulation cell is sufficiently small.
Then the same particle index will appear more than once in the neighbor list. In fact, the central particle may be among its own neighbors in a sufficiently small periodic simulation cell.
However, the computed neighbor vector (`delta`) will be unique for each image of a neighboring particle.

The number of neighbors actually visited may be smaller than the requested number, *N*, if the
system contains too few particles and is non-periodic.

Note that the :py:meth:`!find()` method will not find other particles located exactly at the same spatial position as the central particle for technical reasons.
To find such particles too, which are positioned exactly on top of each other, use :py:meth:`.find_at` instead."""
        ...

    def find_at(self, coords: ovito.vis.Vector3) -> Iterator[Neighbor]:
        """Returns an iterator that visits the *N* nearest particles around a spatial point given by *coords* in order of ascending distance.
Unlike the :py:meth:`find` method, which queries the nearest neighbors of a physical particle, :py:meth:`find_at` allows
searching for nearby particles at arbitrary locations in space.

:param coords: A coordinate triplet (x,y,z) specifying the spatial location where the *N* nearest particles should be queried.
:returns: A Python iterator that visits the *N* nearest neighbors in order of ascending distance.
          For each visited particle the iterator returns an object with the following attributes:

              * index: The index of the current particle (starting at 0).
              * distance: The distance of the current neighbor from the query location.
              * distance_squared: The squared distance to the query location.
              * delta: The three-dimensional vector from the query point to the current particle (correctly taking into account periodic boundary conditions).

If there is a particle located exactly at the query location *coords*, it will be among the returned neighbors.
This is in contrast to the :py:meth:`find` function, which skips the central particle itself.

The number of neighbors actually visited may be smaller than the requested number, *N*, if the
system contains too few particles and is non-periodic."""
        ...

    def find_all(self, indices: Optional[Sequence[int]]=None) -> Tuple[NDArray[numpy.int64], NDArray[numpy.float64]]:
        """Finds the *N* nearest neighbors of each particle in the system or of the subset of particles specified by *indices*.
This is the batch-processing version of :py:meth:`find`, allowing you to efficiently compute the neighbor lists and neighbor vectors of several
particles at once, without explicit for-loop and by making parallel use of all available processor cores.

:param indices: List of zero-based particle indices for which the neighbor lists should be computed.
                If left unspecified, neighbor lists will be computed for every particle in the system.
:returns: ``(neigh_idx, neigh_vec)``

The method returns two arrays:

``neigh_idx`` : NumPy array of shape (*M*, *N*) storing the indices of neighbor particles,
with *M* equal to *len(indices)* or, if *indices* is *None*, the total number of particles in the system.
*N* refers to the number of nearest neighbors requested in the :py:class:`NearestNeighborFinder` constructor.
The computed indices in this array can be used to look up properties of neighbor particles in the global :py:class:`Particles` object.

``neigh_vec`` : NumPy array of shape (*M*, *N*, 3) storing the xyz components of the three-dimensional neighbor vectors ("delta"),
which connect the *M* central particles with their *N* respective nearest neighbors."""
        ...

    def find_all_at(self, coords: Sequence[ovito.vis.Vector3]) -> Tuple[NDArray[numpy.int64], NDArray[numpy.float64]]:
        """Finds the *N* nearest neighbors around each spatial point specified by *coords*.
This is the batch-processing version of :py:meth:`find_at`, allowing you to efficiently determine the
closest neighbors around several spatial locations at once, without an explicit for-loop and by making parallel
use of all available processor cores.

:param coords: NumPy array of shape (*M*, 3) containing the xyz coordinates of *M* query points at each of which the *N* nearest particles should be found.
:returns: ``(neigh_idx, neigh_vec)``

The method returns two NumPy arrays:

``neigh_idx`` : NumPy array of shape (*M*, *N*) storing the indices of nearest particles, with *M* equal to *len(coords)*.
*N* refers to the number of nearest neighbors requested in the :py:class:`NearestNeighborFinder` constructor.
Each neighbor list is sorted by distance from the corresponding query point.

``neigh_vec`` : NumPy array of shape (*M*, *N*, 3) storing the xyz components of the three-dimensional neighbor vectors ("delta"),
which connect the *M* query points with their *N* respective nearest neighbors.

If there is a particle located exactly at a query location, it will be among the returned neighbors for that point.
This is in contrast to the :py:meth:`find_all` function, which skips the central particles at the query locations.

The number of returned neighbors may be smaller than the requested number, *N*, if the
system contains fewer than *N* particles and is non-periodic. In this case, the corresponding columns of ``neigh_idx`` will be filled up with -1."""
        ...

class BondsEnumerator:
    """Utility class that permits efficient iteration over the bonds connected to specific particles. 

The constructor takes a :py:class:`Bonds` object as input. 
From the generally unordered list of bonds, the :py:class:`BondsEnumerator` will build a lookup table for quick enumeration  
of bonds of particular particles. 

All bonds connected to a specific particle can be subsequently visited using the :py:meth:`.bonds_of_particle` method. 

Warning: Do not modify the underlying :py:class:`Bonds` object while the :py:class:`BondsEnumerator` is in use. 
Adding or deleting bonds would render the internal lookup table of the :py:class:`BondsEnumerator` invalid. 

Usage example

```python
  from ovito.io import import_file
  from ovito.data import BondsEnumerator
  from ovito.modifiers import ComputePropertyModifier
  
  # Load a dataset containing atoms and bonds.
  pipeline = import_file('input/bonds.data.gz', atom_style='bond')
  
  # For demonstration purposes, let's define a compute modifier that calculates the length 
  # of each bond, storing the results in a new bond property named 'Length'.
  pipeline.modifiers.append(ComputePropertyModifier(operate_on='bonds', output_property='Length', expressions=['BondLength']))
  
  # Obtain pipeline results.
  data = pipeline.compute()
  positions = data.particles.positions  # array with atomic positions
  bond_topology = data.particles.bonds.topology  # array with bond topology
  bond_lengths = data.particles.bonds['Length']     # array with bond lengths
  
  # Create bonds enumerator object.
  bonds_enum = BondsEnumerator(data.particles.bonds)
  
  # Loop over atoms.
  for particle_index in range(data.particles.count):
      # Loop over bonds of current atom.
      for bond_index in bonds_enum.bonds_of_particle(particle_index):
          # Obtain the indices of the two particles connected by the bond:
          a = bond_topology[bond_index, 0]
          b = bond_topology[bond_index, 1]
          
          # Bond directions can be arbitrary (a->b or b->a):
          assert(a == particle_index or b == particle_index)
          
          # Obtain the length of the bond from the 'Length' bond property:
          length = bond_lengths[bond_index]
  
          print("Bond from atom %i to atom %i has length %f" % (a, b, length))
```"""

    def __init__(self, bonds: Bonds) -> None:
        """Initializes the bond enumerator.

:param Bonds bonds: The data object holding the (unsorted) list of input bonds."""
        ...

    def bonds_of_particle(self, index: int) -> Iterator[int]:
        """Returns an iterator yielding the indices of the bonds connected to the given particle. The indices can be used to index into the :py:class:`Property` arrays of the :py:class:`Bonds` object."""
        ...

class DelaunayTessellation:
    """This class computes the 3D triangulation of the given set of points in space.
The Delaunay triangulation is a partitioning of the convex hull of the input points into tetrahedra,
such that no point lies inside the circumsphere of any tetrahedron.

The input points are specified as a NumPy array of shape (*N*, 3), where *N* is the number of points.
An optional :py:class:`SimulationCell` with periodic boundary conditions can be provided to build a periodic tessellation.
In this case a positive *ghost_layer_size* must be specified, which defines the thickness of the ghost layer around the simulation cell
where periodic images of the input points are created."""

    def __init__(self, coords: ArrayLike, cell: Optional[SimulationCell]=None, ghost_layer_size: float=0.0) -> None:
        ...

    @property
    def vertex_count(self) -> int:
        """Returns the number of Delaunay vertices in the tessellation, including ghost vertices."""
        ...

    @property
    def cell_count(self) -> int:
        """Returns the total number of tetrahedra in the tessellation, including ghost cells and infinite cells."""
        ...

    @property
    def primary_cell_count(self) -> int:
        """Returns the number of finite tetrahedra in the tessellation, only including those belonging to the primary image of the periodic simulation box."""
        ...

    @property
    def simulation_cell(self) -> Optional[SimulationCell]:
        """The input simulation cell (if any)."""
        ...

    @property
    def points(self) -> NDArray[numpy.float64]:
        """Returns the coordinates of the vertices in the tessellation as a NumPy array. This includes ad-hoc generated ghost vertices and helper points.

:return: A NumPy array of shape (N, 3) containing the coordinates of the vertices."""
        ...

    @staticmethod
    def cell_facet_vertices(cell_facet_index: int) -> Tuple[int, int, int]:
        """Returns the cell-local indices of the three vertices of the specified triangular facet.

:param cell_facet_index: The index of a cell facet (0-3).
:return: The three cell-local vertex indices, all in the range 0-3."""
        ...

    @overload
    def is_finite_cell(self, cell: int) -> bool: # type: ignore
        """Returns whether the given tessellation cell connects four physical vertices. Returns false if one of the four vertices is the infinite vertex.

:param cell: The index of the cell to check.
:return: ``True`` if the cell is finite, ``False`` otherwise."""
        ...

    @overload
    def is_finite_cell(self, cell: Sequence[int]) -> NDArray[numpy.bool_]:
        """Returns whether the given tessellation cell connects four physical vertices. Returns false if one of the four vertices is the infinite vertex.

:param cell: The index of the cell to check.
:return: ``True`` if the cell is finite, ``False`` otherwise."""
        ...

    @overload
    def is_ghost_cell(self, cell: int) -> bool: # type: ignore
        """Returns whether the given tessellation cell is a ghost cell.

:param cell: The index of the cell to check.
:return: ``True`` if the cell is a ghost cell or an infinite cell, ``False`` if it is a primary cell."""
        ...

    @overload
    def is_ghost_cell(self, cell: Sequence[int]) -> NDArray[numpy.bool_]:
        """Returns whether the given tessellation cell is a ghost cell.

:param cell: The index of the cell to check.
:return: ``True`` if the cell is a ghost cell or an infinite cell, ``False`` if it is a primary cell."""
        ...

    @overload
    def is_primary_cell(self, cell: int) -> bool: # type: ignore
        """Returns whether the given tessellation cell is a primary cell.

:param cell: The index of the cell to check.
:return: ``True`` if the cell is a primary cell, ``False`` if it is either a ghost or an infinite cell."""
        ...

    @overload
    def is_primary_cell(self, cell: Sequence[int]) -> NDArray[numpy.bool_]:
        """Returns whether the given tessellation cell is a primary cell.

:param cell: The index of the cell to check.
:return: ``True`` if the cell is a primary cell, ``False`` if it is either a ghost or an infinite cell."""
        ...

    @overload
    def cell_primary_index(self, cell: int) -> Optional[int]: # type: ignore
        """Returns the index of the given Delaunay cell in the contiguous list of primary cells. Returns ``None`` if the cell is a ghost or infinite cell.

:param cell: The cell to be queried.
:return: The cell's index in the contiguous list of primary cells; or ``None`` if the cell is a ghost or infinite cell."""
        ...

    @overload
    def cell_primary_index(self, cell: Sequence[int]) -> NDArray[numpy.int_]:
        """Returns the index of the given Delaunay cell in the contiguous list of primary cells. Returns ``None`` if the cell is a ghost or infinite cell.

:param cell: The cell to be queried.
:return: The cell's index in the contiguous list of primary cells; or ``None`` if the cell is a ghost or infinite cell."""
        ...

    @overload
    def is_ghost_vertex(self, vertex: int) -> bool: # type: ignore
        """Returns whether the given vertex is a ghost vertex.

:param vertex: The index of the vertex to check.
:return: ``True`` if the vertex is a ghost vertex, ``False`` otherwise."""
        ...

    @overload
    def is_ghost_vertex(self, vertex: Sequence[int]) -> NDArray[numpy.bool_]:
        """Returns whether the given vertex is a ghost vertex.

:param vertex: The index of the vertex to check.
:return: ``True`` if the vertex is a ghost vertex, ``False`` otherwise."""
        ...

    @overload
    def cell_vertex(self, cell: int, local_index: int) -> int: # type: ignore
        """Returns the index of the vertex at the given local index in the specified cell.

:param cell: The index of the cell.
:param local_index: The local index of the vertex (0-3).
:return: The (global) index of the vertex."""
        ...

    @overload
    def cell_vertex(self, cell: int, local_index: Sequence[int]) -> NDArray[numpy.int_]:
        """Returns the index of the vertex at the given local index in the specified cell.

:param cell: The index of the cell.
:param local_index: The local index of the vertex (0-3).
:return: The (global) index of the vertex."""
        ...

    @overload
    def local_vertex_index(self, cell: int, vertex: int) -> int: # type: ignore
        """Returns the local index of the specified vertex in the given cell.

:param cell: The index of the cell.
:param vertex: The global index of the vertex to look up. Must not be the infinite vertex.
:return: The local index of the vertex in the cell; or -1 if the vertex is not part of the given cell."""
        ...

    @overload
    def local_vertex_index(self, cell: int, vertex: Sequence[int]) -> NDArray[numpy.int_]:
        """Returns the local index of the specified vertex in the given cell.

:param cell: The index of the cell.
:param vertex: The global index of the vertex to look up. Must not be the infinite vertex.
:return: The local index of the vertex in the cell; or -1 if the vertex is not part of the given cell."""
        ...

    @overload
    def input_point_index(self, vertex: int) -> int: # type: ignore
        """Returns the index of the input point corresponding to the specified Delaunay vertex. This is the index into the original list of input points passed to the constructor. Several Delaunay vertices may correspond to the same input point if they are periodic images of each other, in which case this function returns the same index for all of them. 

:param vertex: The index of the Delaunay vertex; or ``None`` if the vertex does not correspond to any physical input point.
:return: The index into the original list of input points passed to the constructor."""
        ...

    @overload
    def input_point_index(self, vertex: Sequence[int]) -> NDArray[numpy.int_]:
        """Returns the index of the input point corresponding to the specified Delaunay vertex. This is the index into the original list of input points passed to the constructor. Several Delaunay vertices may correspond to the same input point if they are periodic images of each other, in which case this function returns the same index for all of them. 

:param vertex: The index of the Delaunay vertex; or ``None`` if the vertex does not correspond to any physical input point.
:return: The index into the original list of input points passed to the constructor."""
        ...

    @overload
    def alpha_test(self, cell: int, alpha: float) -> Optional[bool]: # type: ignore
        """Returns whether the specified cell passes the alpha test.

:param cell: The index of the cell.
:param alpha: The alpha value to test.
:return: ``True`` if the cell passes the alpha test, ``False`` if not. ``None`` if the cell is a degenerate sliver element, for which an alpha value cannot be computed."""
        ...

    @overload
    def alpha_test(self, cell: Sequence[int], alpha: float) -> NDArray[numpy.bool_]:
        """Returns whether the specified cell passes the alpha test.

:param cell: The index of the cell.
:param alpha: The alpha value to test.
:return: ``True`` if the cell passes the alpha test, ``False`` if not. ``None`` if the cell is a degenerate sliver element, for which an alpha value cannot be computed."""
        ...

    @overload
    def cell_adjacent(self, cell: int, local_facet: int) -> int: # type: ignore
        """Returns the index of the adjacent cell for the specified cell and local facet.

:param cell: The index of the cell.
:param local_facet: The local facet index (0-3).
:return: The adjacent cell."""
        ...

    @overload
    def cell_adjacent(self, cell: int, local_facet: Sequence[int]) -> NDArray[numpy.int_]:
        """Returns the index of the adjacent cell for the specified cell and local facet.

:param cell: The index of the cell.
:param local_facet: The local facet index (0-3).
:return: The adjacent cell."""
        ...

    def adjacent_facet(self, cell1: int, cell2: int) -> Optional[int]:
        """Returns the local index of the facet in *cell1* that leads from *cell1* to *cell2*. 

:param cell1: The first cell.
:param cell2: The second cell.
:return: The local index of the facet of *cell1* that is shared by *cell2*; or ``None`` if the two cells don't share any facet."""
        ...

    def mirror_facet(self, cell: int, local_facet: int) -> Tuple[int, int]:
        """Returns the adjacent cell and the local facet index that are opposite to the specified input facet.

:param cell: The index of the cell.
:param local_facet: The local facet index (0-3).
:return: A tuple containing the index of the adjacent cell and the local facet index within that cell."""
        ...

    def incident_facets(self, cell: int, i: int, j: int) -> List[Tuple[int, int]]:
        """Returns the list of facets incident to the specified Delaunay edge.

:param cell: The index of the cell.
:param i: The cell-local index of the first vertex of the edge.
:param j: The cell-local index of the second vertex of the edge.
:return: A list of tuples, each containing the index of the adjacent cell and the local facet index."""
        ...

@dataclass(kw_only=True)
class DataTable(PropertyContainer):
    """Base: :py:class:`ovito.data.PropertyContainer`

This data object type in OVITO represents a series of data points and is primarily used for histogram plots and other 2d graphs.
More generally, however, it can store tabulated data consisting of an arbitrary number of columns of numeric values.

When used for 2d plots, a data table consists of an array of y-values and, optionally, an array of corresponding x-values,
one value pair for each data point. These arrays are regular :py:class:`Property` objects managed by the data table (a sub-class of :py:class:`PropertyContainer`).

If no :py:attr:`x` data array has been set, the x-coordinates of the data points are implicitly determined by the table's
:py:attr:`interval`, which specifies a range along the x-axis over which the data points are evenly distributed.
This is used, for example, for histograms with equisized bins, which don't require explicit x-coordinates.

Data tables generated by modifiers such as :py:class:`RadialDistributionFunctionModifier` and :py:class:`HistogramModifier`
are accessible via the `DataCollection.tables` dictionary. You can retrieve them based on their
unique :py:attr:`identifier`::

   >>> print(data.tables)  # Print list of available data tables
   {'coordination-rdf': DataTable(), 'clusters': DataTable()}

   >>> rdf = data.tables['coordination-rdf']  # Look up tabulated RDF produced by a RadialDistributionFunctionModifier

Exporting the values in a data table to a simple text file is possible using the :py:func:`export_file` function (use file format ``txt/table``).
You can either export a single :py:class:`DataTable` or, as in the following code example, write a series of text files
to export all the tables generated by a :py:class:`Pipeline` for a simulation trajectory in one go. The ``key`` parameter
selects which table from the `DataCollection.tables` dict is to be exported based on its
unique :py:attr:`identifier`::

   export_file(pipeline, 'output/rdf.*.txt', 'txt/table', key='coordination-rdf', multiple_frames=True)

To programmatically create a new data table in Python, you should use the `data.tables.create()` method,
for example when implementing a custom modifier function that should output its results as a data plot.
The following code examples demonstrate how to add a new :py:class:`DataTable` to the data collection and fill it with values.

To create a simple x-y scatter point plot:

```python
  # Create a DataTable object and specify its plot type and a human-readable title:
  table = data.tables.create(identifier='myplot', plot_mode=DataTable.PlotMode.Scatter, title='My Scatter Plot')
  # Set the x- and y-coordinates of the data points:
  table.x = table.create_property('X coordinates', data=numpy.linspace(0.0, 10.0, 50))
  table.y = table.create_property('Y coordinates', data=numpy.cos(table.x))
```

Note how the :py:meth:`create_property` method is being used here to create two :py:class:`Property`
objects storing the coordinates of the data points. These property objects are then set as :py:attr:`x` and :py:attr:`y` arrays
of the :py:class:`DataTable`. This is necessary because a data table is a general :py:class:`PropertyContainer`, which can store an arbitrary
number of data columns. We have to tell the table which of these properties should be used as x- and y-coordinates for plotting.

A multi-line plot is obtained by using a *vectorial* property for the :py:attr:`y` array of the :py:class:`DataTable`:

```python
  table = data.tables.create(identifier='plot', plot_mode=DataTable.PlotMode.Line, title='Trig functions')
  table.x = table.create_property('Parameter x', data=numpy.linspace(0.0, 14.0, 100))
  # Use the x-coords to compute two y-coords per data point: y(x) = (cos(x), sin(x))
  y1y2 = numpy.stack((numpy.cos(table.x), numpy.sin(table.x)), axis=1)
  table.y = table.create_property('f(x)', data=y1y2, components=['cos(x)', 'sin(x)'])
```

To generate a bar chart, the table's :py:attr:`x` property must be filled with numeric IDs 0,1,2,3,... denoting the individual bars.
Each bar is then given a text label by adding an :py:class:`ElementType` to the `Property.types` list
using `Property.add_type_id()`:

```python
  table = data.tables.create(identifier='chart', plot_mode=DataTable.PlotMode.BarChart, title='My Bar Chart')
  table.x = table.create_property('Structure Type', data=[0, 1, 2, 3])
  table.x.add_type_id(0, table, name='Other')
  table.x.add_type_id(1, table, name='FCC')
  table.x.add_type_id(2, table, name='HCP')
  table.x.add_type_id(3, table, name='BCC')
  table.y = table.create_property('Count', data=[65, 97, 10, 75])
```

For histogram plots, one can specify the complete range of values covered by the histogram by setting the table's :py:attr:`interval` property.
The bin counts must be stored in the table's :py:attr:`y` property. The number of elements in the :py:attr:`y` property array, together with the
:py:attr:`interval`, determine the number of histogram bins and their uniform widths:

```python
  table = data.tables.create(identifier='histogram', plot_mode=DataTable.PlotMode.Histogram, title='My Histogram')
  table.y = table.create_property('Counts', data=[65, 97, 10, 75])
  table.interval = (0.0, 2.0)   # Four histogram bins of width 0.5 each.
  table.axis_label_x = 'Values' # Set the x-axis label of the plot.
```

If you are going to access or export the data table after it was inserted into the :py:class:`DataCollection`,
refer to it using its unique :py:attr:`identifier` given at construction time, as shown in the following example:

```python
  def modify(frame: int, data: DataCollection):
      table = data.tables.create(identifier='trig-func', title='My Plot', plot_mode=DataTable.PlotMode.Line)
      table.x = table.create_property('X coords', data=numpy.linspace(0.0, 10.0, 50))
      table.y = table.create_property('Y coords', data=numpy.cos(frame * table.x))
  
  pipeline.modifiers.append(modify)
  export_file(pipeline, 'output/data.*.txt', 'txt/table', key='trig-func', multiple_frames=True)
```"""

    class PlotMode(enum.Enum):
        NoPlot = enum.auto()
        Line = enum.auto()
        Histogram = enum.auto()
        BarChart = enum.auto()
        Scatter = enum.auto()
    x: Optional[Property] = None
    "x() -> Optional[Property]\n\nThe :py:class:`Property` containing the x-coordinates of the data points (for the purpose of plotting). The data points may not have explicit x-coordinates, so this property may be ``None`` for a data table. In such a case, the x-coordinates of the data points are implicitly determined by the table's :py:attr:`interval`. \n\nDefault: ``None``"
    y: Optional[Property] = None
    'y() -> Optional[Property]\n\nThe :py:class:`Property` containing the y-coordinates of the data points (for the purpose of plotting). This may be a vector property having more than one component per data point, in which case this data table represents a family of data plots. \n\nDefault: ``None``'
    axis_label_x: str = ''
    "axis_label_x() -> str\n\nThe text label of the x-axis. This string is only used for a data plot if the :py:attr:`x` property of the data table is ``None`` and the x-coordinates of the data points are implicitly defined by the table's :py:attr:`interval` property. Otherwise the :py:attr:`name` of the :py:attr:`x` property is used as axis label. \n\nDefault: ``''``"
    interval: Tuple[float, float] = (0.0, 0.0)
    "A pair of float values specifying the x-axis interval covered by the data points in this table.\nThis interval is only used by the table if the data points do not possess explicit x-coordinates (i.e. if the table's :py:attr:`x` property is ``None``).\nIn the absence of explicit x-coordinates, the interval specifies the range of equispaced x-coordinates implicitly generated\nby the data table.\n\nImplicit x-coordinates are typically used in data tables representing histograms, which consist of equally-sized bins\ncovering a certain value range along the x-axis. The bin size is then given by the interval width divided by the\nnumber of data points (see :py:attr:`PropertyContainer.count` property). The implicit x-coordinates of data points are placed in the centers of the bins.\nYou can call the table's :py:meth:`xy` method to let it explicitly calculate the x-coordinates from the value interval for every data point.\n\nDefault: ``(0.0, 0.0)``"
    plot_mode: DataTable.PlotMode = PlotMode.Line
    'The type of graphical plot for rendering the data in this :py:class:`DataTable`. Must be one of the following predefined constants:\n\n  * ``DataTable.PlotMode.NoPlot``\n  * ``DataTable.PlotMode.Line``\n  * ``DataTable.PlotMode.Histogram``\n  * ``DataTable.PlotMode.BarChart``\n  * ``DataTable.PlotMode.Scatter``\n\n\nDefault: ``DataTable.PlotMode.Line``'

    def xy(self) -> NDArray[Any]:
        """This convenience method returns a two-dimensional NumPy array containing both the x- and the y-coordinates of the data points in this data table.
If the table has no explicit :py:attr:`x` coordinate property set, this method automatically computes equispaced x-coordinates from the :py:attr:`interval`."""
        ...

@dataclass(kw_only=True)
class VoxelGrid(PropertyContainer):
    """Base: :py:class:`ovito.data.PropertyContainer`

.. figure:: ../introduction/graphics/grid_example_2d.png
  :figwidth: 20%
  :align: right

  Cell-data grid

A two- or three-dimensional structured grid. Each cell (voxel) of the uniform grid is of the same size and shape.
The overall geometry of the grid, its :py:attr:`domain`, is specified by the attached
:py:class:`SimulationCell` object, which describes a three-dimensional parallelepiped or a two-dimensional parallelogram.
See also the corresponding user manual page for more information on this object type.

The :py:attr:`shape` property of the grid specifies the number of data points uniformly distributed along each
cell vector of the domain. The size of individual voxels depends on the overall domain size
as well as the number of data points in each spatial direction. See the :py:attr:`grid_type` property, which
controls whether the data values of the uniform grid are associated with the *voxel interiors* or the *vertices* (grid line intersections).

.. figure:: ../introduction/graphics/grid_example_2d_pointdata.png
  :figwidth: 20%
  :align: right

  Point-data grid

Each data point or voxel of the grid may be associated with one or more field values. The data of these *voxel properties*
is stored in standard :py:class:`Property` array objects, similar to particle or bond properties. Voxel properties can be accessed by name through
the dictionary interface that the :py:class:`VoxelGrid` class inherits from its :py:class:`PropertyContainer`
base class.

Voxel grids can be loaded from input data files, e.g. a CHGCAR file containing the electron density computed by the VASP code,
or they can be dynamically generated within OVITO. The :py:class:`SpatialBinningModifier` lets you
project the information associated with the unstructured particle set to a structured voxel grid.

Given a voxel grid, the :py:class:`CreateIsosurfaceModifier` can then generate a :py:class:`SurfaceMesh`
representing an isosurface for a field quantity defined on the voxel grid.

Example

The following code example demonstrates how to create a new :py:class:`VoxelGrid` from scratch
and initialize it with data from a NumPy array:

  ```python
    # Starting with an empty DataCollection:
    data = DataCollection()
    
    # Create a new SimulationCell object defining the outer spatial dimensions
    # of the grid and the boundary conditions, and add it to the DataCollection:
    cell = data.create_cell(
        matrix=[[10,0,0,0],[0,10,0,0],[0,0,10,0]],
        pbc=(True, True, True)
    )
    
    # Generate a three-dimensional Numpy array containing the grid cell values.
    nx = 10; ny = 6; nz = 8
    field_data = numpy.random.random((nx, ny, nz))
    
    # Create the VoxelGrid object and give it a unique identifier by which it can be referred to later on.
    # Link the voxel grid to the SimulationCell object created above, which defines its spatial extensions.
    # Specify the shape of the grid, i.e. the number of cells in each spatial direction.
    # Finally, assign a VoxelGridVis visual element to the data object to make the grid visible in the scene.
    grid = data.grids.create(
        identifier="field",
        domain=cell,
        shape=(nx,ny,nz),
        grid_type=VoxelGrid.GridType.CellData,
        vis_params={ "enabled": True, "transparency": 0.6 }
    )
    
    # Add a new property to the voxel grid cells and initialize it with the data from the NumPy array.
    # Note that the data must be provided as linear (1-dim.) array with the following type of memory layout:
    # The first grid dimension (x) is the fasted changing index while the third grid dimension (z) is the
    # slowest varying index. In this example, this corresponds to the "Fortran" memory layout of Numpy.
    grid.create_property('Field Value', data=field_data.flatten(order='F'))
    
    # Instead of the flatten() method above, we could also make use of the method VoxelGrid.view()
    # to obtain a 3-dimensional view of the property array, which supports direct assignment of grid values.
    field_prop = grid.create_property('Field Value', dtype=field_data.dtype, components=1)
    grid.view(field_prop)[...] = field_data
    
    # For demonstration purposes, compute an isosurface on the basis of the VoxelGrid created above.
    data.apply(CreateIsosurfaceModifier(operate_on='voxels:field', property='Field Value', isolevel=0.7))
  ```

The :py:class:`VoxelGrid` container uses the following standard properties with predefined names and data layouts.
Additional per-voxel properties may be added using the :py:meth:`~PropertyContainer.create_property`
method of the base class.

================================ ======================== ========== =======================
Property name                    Python access            Data type  Component names
================================ ======================== ========== =======================
:guilabel:`Color`                                         float32    R, G, B
================================ ======================== ========== ======================="""

    class GridType(enum.Enum):
        CellData = enum.auto()
        PointData = enum.auto()
    domain: Optional[SimulationCell] = None
    'domain() -> Optional[SimulationCell]\n\nThe :py:class:`SimulationCell` describing the (possibly periodic) domain which this grid is embedded in. Note that this cell generally is independent of and may be different from the :py:attr:`cell` found in the :py:class:`DataCollection`. \n\nDefault: ``None``'
    vis: ovito.vis.VoxelGridVis = ovito.vis.VoxelGridVis()
    'vis() -> Optional[ovito.vis.DataVis]\n\nThe :py:class:`DataVis` element currently associated with this data object. This object is responsible for\nvisually rendering the stored data. If set to ``None``, the data object remains non-visual and does not appear in\nrendered images or viewports. Additionally, note that the same :py:class:`DataVis` element may be assigned to\nmultiple data objects to synchronize their visual appearance.\n\nSee the :py:mod:`ovito.vis` module for a list of visual element types.'
    shape: Tuple[int, int, int] = (0, 0, 0)
    'shape() -> tuple[int, int, int]\n\nA 3-tuple specifying the number of sampling points along each of the three cell vectors of the :py:attr:`domain`. \n\nFor two-dimensional grids, for which the :py:attr:`is2D` property of the :py:attr:`domain` is set, the third entry in this :py:attr:`shape` tuple must be equal to 1. \n\nAssigning a new shape to the grid automatically resizes the one-dimensional data arrays stored by this :py:class:`PropertyContainer` and updates its :py:attr:`PropertyContainer.count` property to match the product of the three dimensions, i.e. the total number of data points. \n\nDefault: ``(0, 0, 0)``'
    grid_type: VoxelGrid.GridType = GridType.CellData
    "grid_type() -> VoxelGrid.GridType\n\nThis attribute specifies whether the values stored by the grid object are associated with the voxel cell centers or the grid points (vertices). Possible values are: \n\n  * ``VoxelGrid.GridType.CellData`` (default)\n  * ``VoxelGrid.GridType.PointData``\n\n\nA ``CellData`` grid represents a field where the sampling points are located in the centers of the voxel cells. This grid type is typically used for volumetric datasets, which represent some quantity within the discrete voxel cell volumes. \n\n.. image:: ../introduction/graphics/grid_type_celldata.png\n   :width: 20%\n\nA ``PointData`` grid represents a field where the sampling points are located at the intersections of the grid lines. Note that, for this grid type only, the type of boundary conditions of the grid's :py:attr:`domain` affect the uniform spacing of the sampling points:\n\n.. figure:: ../introduction/graphics/grid_type_pointdata.png\n   :figwidth: 50%\n\n\n   4 x 4 point-data grid (left: non-periodic domain, right: periodic domain)\n\nDefault: ``VoxelGrid.GridType.CellData``"

    @property
    def domain_(self) -> Optional[SimulationCell]:
        ...

    def view(self, key: Union[str, Property]) -> NDArray[Any]:
        """Returns a shaped view of the given grid property, which reflects the 2- or 3-dimensional :py:attr:`shape` of the grid.

:param key: The name of the grid property to look up. May include the underscore suffix to make the property mutable. Alternatively, you can directly specify a :py:class:`Property` object from this :py:class:`VoxelGrid`.
:returns: A `NumPy view <https://numpy.org/doc/stable/user/basics.copies.html>`__ of the underlying property array.

Because the :py:class:`VoxelGrid` class internally uses linear :py:class:`Property` arrays to store the voxel cell values,
you normally would have to convert back and forth between the linear index space of the underlying property storage and
the 2- or 3-dimensional grid space to access individual voxel cells.

The :py:meth:`view` helper method frees you from having to map grid coordinates to array indices because it gives you a shaped NumPy view
of the underlying linear storage, which reflects the correct multi-dimensional shape of the grid. For 3-dimensional grids, the ordering of the
view's dimensions is :math:`x,y,z[,k]`, with :math:`k` being an extra dimension that is only present if the accessed property
is a vector field quantity. For 2-dimensional grids, the ordering of the view's dimensions is :math:`x,y[,k]`.

The returned view lets you conveniently access the values of individual grid cells based on multi-dimensional grid coordinates.
Here, as an example, the scalar field property ``c_ave`` of a 3-dimensional voxel grid:

```python
  nx, ny, nz = grid.shape
  field = grid.view('c_ave')
  for x in range(nx):
      for y in range(ny):
          for z in range(nz):
              print(field[x,y,z])
```"""
        ...