"""This module contains classes that are part of OVITO's data pipeline system.

Pipelines:

  * :py:class:`Pipeline` - a sequence of data input and processing steps (a data *source* followed by *modifiers*)
  * :py:class:`Modifier` - base class of all built-in data modification and processing algorithms of OVITO
  * :py:class:`ModifierInterface` - abstract base class for user-defined modifiers
  * :py:class:`PipelineNode` - base class for all types of pipeline steps
  * :py:class:`ModificationNode` - a pipeline step that processes some input data by applying a given modifier algorithm

Pipeline data sources:

  * :py:class:`FileSource` - loads input data from an external file
  * :py:class:`StaticSource` - passes an existing :py:class:`DataCollection` object to the pipeline as input
  * :py:class:`PythonSource` - encapsulates a :py:class:`PipelineSourceInterface` or user-defined pipeline source function
  * :py:class:`PipelineSourceInterface` - abstract base class for user-defined dynamic data sources"""
__all__ = ['Pipeline', 'Modifier', 'StaticSource', 'FileSource', 'PythonSource', 'ModifierInterface', 'PipelineSourceInterface', 'PipelineNode', 'ModificationNode', 'ModifierGroup', 'ReferenceConfigurationModifier']
from __future__ import annotations
from typing import Optional, Any, Union, Sequence, MutableSequence, Callable, List, Generator, Iterator, overload, Callable, Mapping, Dict, TYPE_CHECKING
import ovito.data
import ovito.modifiers
from ovito import ArrayLike
import enum
import abc
import os
from dataclasses import dataclass

@dataclass(kw_only=True)
class PipelineNode:
    """This abstract base class represents one step in a :py:class:`Pipeline`. Every node in a data pipeline is either a *data source* or a *data modification* step.

.. figure:: ../introduction/graphics/PipelineNodes.svg
   :figwidth: 42%
   :align: right

   The nodes of a pipeline form a linked-list structure. Each node has a reference to the preceding node in the pipeline, where it receives its input data from.
   Modification nodes are associated with a :py:class:`Modifier` instance, which is the algorithm to be applied to the data during pipeline execution.

Source nodes represent the first stage of a pipeline. Concrete types of source nodes in OVITO are:
:py:class:`FileSource`, :py:class:`StaticSource`, and :py:class:`PythonSource`. They are responsible for loading input data from a file, managing a static data collection, or evaluating
a Python function that dynamically generates new data, respectively.

Data modification steps in a pipeline are instances of the :py:class:`ModificationNode` class, which is a specialization of :py:class:`PipelineNode`
managing a reference to a preceding node in the pipeline (:py:attr:`ModificationNode.input`). This reference specifies where the modification node will receive its input data from.
Additionally, each modification node manages a reference to a :py:class:`Modifier` object (:py:attr:`ModificationNode.modifier`), which implements the actual data modification
algorithm that gets executed during pipeline evaluation. The *modifier* is also where the control parameters of the data modification step are stored.
Several :py:class:`ModificationNode` instances can share the same :py:class:`Modifier` object, which allows reusing the same data modification algorithm in multiple
places, e.g. two different pipelines.

A pipeline has exactly one *head* node, which is stored in the :py:attr:`Pipeline.head` field. It represents the outlet of the pipeline as
it is the last processing step in the data flow sequence. It's called *head* node, because it's the head of a linked-list structure formed by the pipeline steps.

Every pipeline also has a *source* node, which is the one at the tail of the chain of nodes. That tail node is directly accessible as the pipeline's :py:attr:`~Pipeline.source` property."""
    cache_trajectory: bool = False

    @property
    def num_frames(self) -> int:
        """Read-only attribute reporting the number of output trajectory frames this pipeline node can compute or produce.

In case of a :py:class:`FileSource`, returns the number of trajectory frames found in the input file or sequence of input files.
In case of a :py:class:`StaticSource`, returns constant 1.
In case of a :py:class:`PythonSource`, returns the result of the :py:meth:`PipelineSourceInterface.compute_trajectory_length` method.
In case of a :py:class:`ModificationNode`, returns the number of frames generated by the :py:attr:`~ModificationNode.input` node,
which may be altered by the associated :py:attr:`~ModificationNode.modifier`."""
        ...

    def get_pipelines(self, in_scene_only: bool=False) -> List[Pipeline]:
        ...

    def compute(self, frame: Optional[int]=None) -> ovito.data.DataCollection:
        """Requests the results from this pipeline node. Calling this function may implicitly lead to an evaluation of
all preceding pipeline nodes in the pipeline, if necessary. The function returns a new :py:class:`DataCollection` object containing the
result data for a single trajectory frame.

The optional *frame* parameter determines the frame to compute, which must be in the range 0 through (:py:attr:`num_frames`-1).
If you don't specify a frame number, the current time slider position of the OVITO GUI will be used
(always frame 0 if called from a non-interactive Python script).

The pipeline node uses a caching mechanism, keeping the output data for one or more trajectory frames in memory. Thus, invoking :py:meth:`compute`
repeatedly to retrieve the same frame will typically be very fast.

:param frame: The trajectory frame to retrieve or compute.
:return: A new :py:class:`DataCollection` containing the frame's data."""
        ...

@dataclass(kw_only=True)
class ModificationNode(PipelineNode):
    """Base: :py:class:`ovito.pipeline.PipelineNode`

.. image:: ../introduction/graphics/PipelineNodes.svg
   :width: 42%
   :align: right

Represents a modification step in a data processing pipeline, i.e., the application of a :py:class:`Modifier` in a particular :py:class:`Pipeline`.

Each :py:class:`ModificationNode` has an :py:attr:`.input` property, which is a reference to its preceding node in the upstream pipeline,
where it receives its input data from. Since each :py:class:`ModificationNode` is associated with exactly one :py:attr:`.input` node, they
form a singly-linked list structure. The pipeline chain always terminates in a *source* :py:class:`PipelineNode`, i.e., a node that is *not* a :py:class:`ModificationNode`
and doesn't have another input node.

A :py:class:`ModificationNode` always has a :py:attr:`modifier` field, which is a reference to a :py:class:`Modifier` object
implementing the actual data processing algorithm that gets executed during pipeline evaluation. The *modifier* is also the object that stores the
specific control parameters of the data processing step. The :py:class:`ModificationNode` itself simply represents the *use* of a modifier in
a particular data pipeline."""
    modifier: Optional[Modifier]
    'modifier() -> Modifier\n\nReference to the :py:class:`Modifier` that gets applied to the data as it flows through the pipeline. Several modification nodes may share the same modifier object, which allows using (applying) the same modifier in multiple pipelines. All uses of the modifier share the same parametrization in this case.'
    group: Optional[ModifierGroup]
    input: Optional[PipelineNode]
    'input() -> PipelineNode\n\nReference to the upstream :py:class:`PipelineNode` that provides the input data for this modification step in the pipeline. Several modification nodes, each located in a different pipeline, may share the same input node, which means it is possible to build branched pipelines that are fed by the same data source.'

class ModifierInterface:
    """Base: :py:class:`traits.has_traits.HasTraits`

Abstract base class for Python-based modifiers that follow the advanced programming interface."""

    class InputSlot:
        """Represents the upstream pipeline generating the input data for a custom modifier implementation."""

        @property
        def num_frames(self) -> int:
            ...

        @property
        def input_node(self) -> PipelineNode:
            ...

        def compute(self, frame: int) -> ovito.data.DataCollection:
            ...

    @abc.abstractmethod
    def modify(self, data: ovito.data.DataCollection, *, frame: int, input_slots: Dict[str, InputSlot], data_cache: ovito.data.DataCollection, pipeline_node: ModificationNode, **kwargs: Any) -> Optional[Generator[str | float, None, None]]:
        """The actual work function which gets called by the pipeline system to let the modifier do its thing.

:param data: Data snapshot which should be modified by the modifier function in place.
:param frame: Zero-based trajectory frame number.
:param input_slots: One or more :py:class:`InputSlot` objects representing the upstream data pipeline(s) connected to this modifier.
:param data_cache: A data container (initially empty) which may be used by the modifier function to store intermediate results.
:param pipeline_node: An object representing the use of this modifier in the pipeline that is currently being evaluated.
:param kwargs: Any further arguments that may be passed in by the pipeline system. This parameter should always be part of the function signature for forward compatibility with future versions of OVITO."""
        ...

    def notify_trajectory_length_changed(self) -> None:
        """Notifies the pipeline system that the number of output animation frames this modifier can compute has changed.
The modifier class should call this method whenever the return value of its :py:meth:`compute_trajectory_length` method
changes, for example, as a consequence of a parameter change."""
        ...

class PipelineSourceInterface:
    """Base: :py:class:`traits.has_traits.HasTraits`

Abstract base class for custom pipeline sources in Python.
Implementations of the interface must at least provide the :py:meth:`create` method.

Example:

```python
  from ovito.data import DataCollection
  from ovito.pipeline import PipelineSourceInterface
  
  class ExampleSource(PipelineSourceInterface):
      def create(self, data: DataCollection, **kwargs):
          cell_matrix = [
              [10,0,0,0],
              [0,10,0,0],
              [0,0,10,0]
          ]
          data.create_cell(cell_matrix, pbc=(False, False, False))
```

Next, you can build a new :py:class:`Pipeline` using this pipeline source by wrapping it in a :py:class:`PythonSource` object:

```python
  from ovito.pipeline import Pipeline, PythonSource
  
  example_source = ExampleSource()
  pipeline = Pipeline(source = PythonSource(delegate = example_source))
```"""

    @abc.abstractmethod
    def create(self, data: ovito.data.DataCollection, *, frame: int, **kwargs: Any) -> Optional[Generator[str | float, None, None]]:
        """The generator function which gets called by the pipeline system to let the source do its thing and produce a data collection.

:param data: Data collection which should be populated by the function. It may already contain data from previous runs.
:param frame: Zero-based trajectory frame number.
:param kwargs: Any further arguments that may be passed in by the pipeline system. This parameter should always be part of the function signature for forward compatibility with future versions of OVITO."""
        ...

    def notify_trajectory_length_changed(self) -> None:
        """Notifies the pipeline system that the number of output animation frames this source can generate has changed.
The class should call this method whenever the return value of its :py:meth:`compute_trajectory_length` method
changes, for example, as a consequence of a parameter change."""
        ...

@dataclass(kw_only=True)
class StaticSource(PipelineNode):
    """Base: :py:class:`ovito.pipeline.PipelineNode`

Serves as a data :py:attr:`~Pipeline.source` for a :py:class:`Pipeline`.
A :py:class:`StaticSource` manages a :py:class:`DataCollection`, which it will pass to the :py:class:`Pipeline` as input data.
One typically initializes a :py:class:`StaticSource` with a collection of data objects, then wires it to a :py:class:`Pipeline` as follows:

```python
  from ovito.pipeline import StaticSource, Pipeline
  from ovito.data import DataCollection, SimulationCell, Particles
  from ovito.modifiers import CreateBondsModifier
  from ovito.io import export_file
  
  # Insert a new SimulationCell object into a data collection:
  data = DataCollection()
  cell = SimulationCell(pbc = (False, False, False))
  cell[:,0] = (4,0,0)
  cell[:,1] = (0,2,0)
  cell[:,2] = (0,0,2)
  data.objects.append(cell)
  
  # Create a Particles object containing two particles:
  particles = Particles()
  particles.create_property('Position', data=[[0,0,0],[2,0,0]])
  data.objects.append(particles)
  
  # Create a new Pipeline with a StaticSource as data source:
  pipeline = Pipeline(source = StaticSource(data = data))
  
  # Apply a modifier:
  pipeline.modifiers.append(CreateBondsModifier(cutoff = 3.0))
  
  # Write pipeline results to an output file:
  export_file(pipeline, 'output/structure.data', 'lammps/data', atom_style='bond')
```"""
    data: Optional[ovito.data.DataCollection] = None
    'data() -> Optional[ovito.data.DataCollection]\n\nThe :py:class:`DataCollection` managed by this object, which will be fed to the pipeline. \n\nDefault: ``None``'

@dataclass(kw_only=True)
class FileSource(PipelineNode):
    """Base: :py:class:`ovito.pipeline.PipelineNode`

This object type serves as a :py:attr:`Pipeline.source` and takes care of reading the input data for a :py:class:`Pipeline` from an external file.

You normally do not need to create an instance of this class yourself; the :py:func:`import_file` function does it for you and wires the fully configured :py:class:`FileSource`
to the new :py:attr:`Pipeline`. However, if needed, the :py:meth:`FileSource.load` method allows you to load a different input file later on and replace the
input of the existing pipeline with a new dataset:

```python
  from ovito.io import import_file
  
  # Create a new pipeline with a FileSource:
  pipeline = import_file('input/first_file.dump')
  
  # Get the data from the first file:
  data1 = pipeline.compute()
  
  # Use FileSource.load() method to replace the pipeline's input with a different file 
  pipeline.source.load('input/second_file.dump')
  
  # Now the pipeline gets its input data from the new file:
  data2 = pipeline.compute()
```

Furthermore, you will encounter other :py:class:`FileSource` objects in conjunction with certain modifiers that need secondary input data from a separate file.
The :py:class:`CalculateDisplacementsModifier`, for example, manages its own :py:class:`FileSource` for loading reference particle positions from a separate input file.
Another example is the :py:class:`LoadTrajectoryModifier`,
which employs its own separate :py:class:`FileSource` instance to load the particle trajectories from disk and combine them
with the topology data previously loaded by the main :py:class:`FileSource` of the data pipeline.

Data access

A :py:class:`FileSource` is a :py:class:`PipelineNode`, which provides a :py:meth:`~PipelineNode.compute` method
returning a copy of the data loaded from the external input file(s). The :py:meth:`~PipelineNode.compute` method loads
the data of a specific trajectory frame from the input file(s) and returns it as a :py:class:`DataCollection` object:

```python
  # This creates a new Pipeline with an attached FileSource.
  pipeline = import_file('input/simulation.dump')
  
  # Request data of trajectory frame 0 from the FileSource.
  data = pipeline.source.compute(0)
  print(data.particles.positions[...])
```

The returned :py:class:`DataCollection` is a copy: modifying it has no effect on what the pipeline
produces. To modify or amend the data loaded by the :py:class:`FileSource`, insert a modifier into the pipeline.
A typical use case is assigning radii and names to particle types loaded from a simulation file that doesn't contain
named atom types:

```python
  pipeline = import_file('input/simulation.dump')

  # User-defined modifier function that assigns names and radii to numeric atom types:
  def setup_atom_types(frame, data):
      types = data.particles_.particle_types_
      types.type_by_id_(1).name = "Cu"
      types.type_by_id_(1).radius = 1.35
      types.type_by_id_(2).name = "Zr"
      types.type_by_id_(2).radius = 1.55

  pipeline.modifiers.append(setup_atom_types)
```"""

    def load(self, location: Union[str, os.PathLike, Sequence[str]], **params: Any) -> None:
        """Sets a new input file, from which this pipeline source will retrieve its data.

The function accepts additional keyword arguments, which are forwarded to the format-specific file reader
managed internally by the :py:class:`FileSource`.
For further information, please see the documentation of the :py:func:`import_file` function,
which has the same call interface as this method.

:param location: The local file(s) or remote URL to load.
:param params: Additional keyword parameters to be passed to the file reader."""
        ...

    @property
    def data(self) -> Optional[ovito.data.DataCollection]:
        """This field provides access to the internal :py:class:`DataCollection` managed by the file source, which stores a master copy of the data loaded from the input file (only the most recently loaded trajectory frame)."""
        ...

    @property
    def source_path(self) -> Union[str, Sequence[str]]:
        """This read-only attribute returns the path(s) or URL(s) of the file(s) where this :py:class:`FileSource` retrieves its input data from.
You can change the source path by calling :py:meth:`.load`."""
        ...

    @property
    def playback_ratio(self, ratio: str) -> str:
        """Controls the trajectory playback ratio of the animation.
This property controls how snapshots loaded from the file source are mapped to OVITO's animation timeline.
You can change the default 1:1 mapping to either a 1:n mapping, in which case each trajectory frame is replicated
and rendered n times, or to an n:1 mapping, where only every n-th trajectory frame will be rendered.
See also :ref:`manual:usage.animation.frames` for more information about OVITO's animation timeline.

Input has to be provided as a string in the form "a:b" or "a/b", where a and b are integers.

:param str ratio: New playback ratio.
:return: The current playback ratio as a string in the form "a:b".
:default: ``"1:1"``"""
        ...
    playback_start_time: int = 0
    'playback_start_time() -> int\n\nControls at which animation frame the trajectory should start playing. The first trajectory snapshot will be displayed until the `playback_start_time` animation frame is reached. \n\nDefault: ``0``'
    static_frame: Optional[int] = None
    'static_frame() -> Optional[int]\n\nThis field can be set to restrict the loaded trajectory to a single static frame, which is selected by a zero-based frame index. This option is equivalent to the setting Extract a static frame in the GUI. The default value ``None`` activates full trajectory playback. \n\nDefault: ``None``'

@dataclass(kw_only=True)
class PythonSource(PipelineNode):
    """Base: :py:class:`ovito.pipeline.PipelineNode`

A pipeline node type that executes a user-defined Python function to dynamically create the input data for the :py:class:`Pipeline`.
It allows you to feed a pipeline with dynamically generated data instead of loading the data from an external file
using one of OVITO's file format readers.

When setting up a :py:class:`PythonSource`, you have the choice between two different programming interfaces:
The *function-based* interface is simple and involves less boilerplate code, but it is also less powerful.
It involves defining a single Python function with a predefined signature that is called by OVITO's pipeline system to
generate the data of one trajectory frame at a time.

Code example for the simple *function-based* interface:

```python
  from ovito.pipeline import Pipeline, PythonSource
  from ovito.io import export_file
  from ovito.data import DataCollection
  import numpy

  # User-defined data source function, which populates an empty DataCollection with
  # some data objects:
  def create_model(frame: int, data: DataCollection):
      particles = data.create_particles(count=20)
      coordinates = particles.create_property('Position')
      coordinates[:,0] = numpy.linspace(0.0, 50.0, particles.count)
      coordinates[:,1] = numpy.cos(coordinates[:,0]/4.0 + frame/5.0)
      coordinates[:,2] = numpy.sin(coordinates[:,0]/4.0 + frame/5.0)

  # Create a data pipeline with a PythonSource, which executes our
  # script function defined above.
  pipeline = Pipeline(source = PythonSource(function = create_model))

  # Export the results of the data pipeline to an output file.
  # The system will invoke the Python function defined above once per animation frame.
  export_file(pipeline, 'output/trajectory.xyz', format='xyz',
      columns=['Position.X', 'Position.Y', 'Position.Z'],
      multiple_frames=True, start_frame=0, end_frame=10)
```

For more advanced applications, a *class-based* programming interface is also available,
which involves defining a new Python class that implements the :py:class:`PipelineSourceInterface`.
This approach gives you control over aspects such as the length of the dynamically-generated trajectory sequence
and it allows you to define adjustable user parameters that control the behavior of your custom data source.
See the :py:attr:`PythonSource.delegate` field and the :py:class:`PipelineSourceInterface` class for more details."""
    function: Optional[Callable[[int, ovito.data.DataCollection], Any]] = None
    'function() -> Optional[collections.abc.Callable[[int, ovito.data.DataCollection], Any]]\n\nThe Python function to be invoked when the data pipeline is evaluated by the system.\n\nThe function must have the signature shown in the code example above. The *frame* parameter specifies the current trajectory frame at which the data pipeline is being evaluated. The :py:class:`DataCollection` *data* is initially empty and should be populated with data objects by the user-defined Python function. \n\nDefault: ``None``'
    delegate: Optional[PipelineSourceInterface] = None
    'delegate() -> Optional[PipelineSourceInterface]\n\nThe :py:class:`PipelineSourceInterface` object implementing the logic of the user-defined pipeline source. The pipeline system will invoke its :py:meth:`create` method whenever it needs the input data for a particular trajectory frame. \n\nDefault: ``None``'
    script: str = ''
    kwargs: Optional[dict] = None
    code_template_path: str = ''
    working_dir: str = ''
    "working_dir() -> str\n\nA filesystem path that should be used as active working directory while the Python function is executed by the pipeline system. This setting mainly plays a role if the :py:class:`PythonSource` is being used in the GUI version of OVITO and if it performs some file I/O. Relative file paths will then get resolved with respect to the specified working directory. \n\nIf no working directory is explicitly specified, the application's current working directory will be used. \n\nDefault: ``''``"

@dataclass(kw_only=True)
class Modifier:
    """Base class for all data modification and processing algorithms in OVITO.
See the :py:mod:`ovito.modifiers` module for a list of all concrete modifier types that can be inserted into a data processing :py:class:`Pipeline`."""
    enabled: bool = True
    'enabled() -> bool\n\nControls whether the modifier will be applied to the data or not. Disabled modifiers are skipped during evaluation of a data pipeline. \n\nDefault: ``True``'
    title: str = ''
    "title() -> str\n\nA human-readable name for the modifier to be displayed in the pipeline editor of the OVITO desktop application. If left unspecified (empty string), the display title is automatically determined by OVITO based on the modifier's concrete class type. \n\nDefault: ``''``"

    def get_modification_nodes(self) -> List[ModificationNode]:
        """Returns a list of all `ModificationNodes` currently associated with this modifier, i.e., whose :py:attr:`~ModificationNode.modifier` field points to this modifier. Each :py:class:`ModificationNode` in the returned list represents the use or application of this modifier in a particular data pipeline."""
        ...

@dataclass(kw_only=True)
class ModifierGroup:
    collapsed: bool = False
    "collapsed() -> bool\n\nControls whether this group's member modifiers are currently collapsed (hidden) or expanded (shown) in the pipeline editor of the OVITO GUI.\n\nDefault: ``False``"
    enabled: bool = True
    title: str = ''

    @property
    def nodes(self) -> List[ModificationNode]:
        """The list of :py:class:`ModificationNode` steps that are currently members of this group, in the order in which they appear in their common pipeline (all member nodes of a group always belong to the same, single pipeline branch). A node becomes a member of the group by setting its :py:attr:`~ModificationNode.group` field to this group."""
        ...

    def get_pipelines(self, in_scene_only: bool=False) -> List[Pipeline]:
        """Returns the list of :py:class:`Pipeline` objects that contain this modifier group, i.e., whose modifiers include at least one member of :py:attr:`nodes`. If *in_scene_only* is ``True``, only pipelines that are currently part of the three-dimensional scene (see :py:meth:`Pipeline.add_to_scene`) are returned."""
        ...

@dataclass(kw_only=True)
class ReferenceConfigurationModifier(Modifier):
    """Base: :py:class:`ovito.pipeline.Modifier`

This is the common base class of analysis modifiers that perform some kind of comparison
of the current particle configuration with a reference configuration. For example,
the :py:class:`CalculateDisplacementsModifier`, the :py:class:`AtomicStrainModifier`
and the :py:class:`WignerSeitzAnalysisModifier` are modifier types that require
a reference configuration as additional input.

Constant and sliding reference configurations

The :py:class:`ReferenceConfigurationModifier` base class provides various fields that
allow you to specify the reference particle configuration. By default, frame 0 of the currently loaded
simulation sequence is used as reference. You can select any other frame with the :py:attr:`reference_frame` field.
Sometimes an incremental analysis is desired, instead of a fixed reference configuration. That means the sliding reference configuration and the current configuration
are separated along the time axis by a constant period (*delta t*). The incremental analysis mode is activated by
setting the :py:attr:`use_frame_offset` flag and specifying the desired :py:attr:`frame_offset`.

External reference configuration file

By default, the reference particle positions are obtained by evaluating the same data pipeline that also
provides the current particle positions, i.e. which the modifier is part of. That means any modifiers preceding this modifier in the pipeline
will also act upon the reference particle configuration, but not modifiers that follow in the pipeline.

Instead of taking it from the same data pipeline, you can explicitly provide a reference configuration by loading it from a separate data file.
To this end the :py:attr:`reference` field contains a :py:class:`FileSource` object and you can
use its :py:meth:`load` method to load the reference particle positions from a separate file.

Handling of periodic boundary conditions and cell deformations

Certain analysis modifiers such as the :py:class:`CalculateDisplacementsModifier` and the :py:class:`AtomicStrainModifier`
calculate the displacements particles experienced between the reference and the current configuration.
Since particle coordinates in periodic simulation cells are often stored in a *wrapped* form,
calculating the displacement vectors is non-trivial when particles have crossed the periodic boundaries.
By default, the *minimum image convention* is used in these cases, but you can turn it off by
setting :py:attr:`minimum_image_convention` to ``False``, for example if the input particle coordinates
are given in unwrapped form.

Furthermore, if the simulation cell of the reference and the current configuration are different, it makes
a slight difference whether displacements are calculated in the reference or in the current frame.
The :py:attr:`affine_mapping` property controls the type of coordinate mapping that is used."""

    class AffineMapping(enum.Enum):
        Off = enum.auto()
        ToReference = enum.auto()
        ToCurrent = enum.auto()
    affine_mapping: ReferenceConfigurationModifier.AffineMapping = AffineMapping.Off
    'affine_mapping() -> ReferenceConfigurationModifier.AffineMapping\n\nSelects how particle displacements are calculated to compensate for changes in the shape of the simulation cell.\nAn affine deformation may be applied to the particle coordinates of either the reference or the\ncurrent configuration prior to the actual analysis computation. Must be one of the following enum values:\n\n:py:data:`ReferenceConfigurationModifier.AffineMapping.Off`\n    Calculate displacement vectors simply from the differences between\n    Cartesian particle coordinates in the current and reference configuration, irrespective of any cell shape change.\n    Note that this may introduce a small geometric error if the shape of the periodic simulation cell\n    did change considerably between reference and current configuration.\n\n:py:data:`ReferenceConfigurationModifier.AffineMapping.ToReference`\n    Map the current particle positions to the reference cell before calculating the displacements.\n    This is done by applying an affine transformation calculated from\n    the inverse of the cell shape change to the current particle coordinates.\n\n:py:data:`ReferenceConfigurationModifier.AffineMapping.ToCurrent`\n    Map the reference particle positions to the deformed cell before calculating the displacements.\n    This is done by applying an affine transformation calculated from\n    the cell shape change to the reference particle coordinates.\n\nDefault: :py:data:`ReferenceConfigurationModifier.AffineMapping.Off`'
    frame_offset: int = -1
    'frame_offset() -> int\n\nThe relative frame offset when using a sliding reference configuration (if :py:attr:`use_frame_offset` == ``True``). Negative frame offsets correspond to reference configurations that precede the current configuration in time. \n\nDefault: ``-1``'
    minimum_image_convention: bool = True
    'minimum_image_convention() -> bool\n\nIf ``False``, then displacements are calculated from the particle coordinates in the reference and the current configuration as is. Note that in this case the calculated displacements of particles that have crossed a periodic simulation cell boundary will be wrong if their coordinates are stored in a wrapped form. If ``True``, then the minimum image convention is applied when calculating the displacements of particles that have crossed a periodic boundary. \n\nDefault: ``True``'
    reference: Optional[FileSource] = None
    "reference() -> Optional[PipelineNode]\n\nA source :py:class:`PipelineNode` object that provides the reference particle positions. By default this field is ``None``, in which case the modifier obtains the reference particle positions from current data pipeline it is part of. You can explicitly set a different data source such as a new :py:class:`FileSource` or a :py:class:`StaticSource` to specify an explicit reference configuration that is not a snapshot from the current simulation trajectory. \n\n```python\n  # A modifier that needs a reference config as input:\n  mod = CalculateDisplacementsModifier()\n  \n  # Load the reference config from a separate input file.\n  mod.reference = ovito.pipeline.FileSource()\n  mod.reference.load('input/simulation.0.dump')\n```\n\nDefault: ``None``"
    reference_frame: int = 0
    'reference_frame() -> int\n\nThe zero-based animation frame to use as reference configuration. Ignored if :py:attr:`use_frame_offset` is set.\n\nDefault: ``0``'
    use_frame_offset: bool = False
    'use_frame_offset() -> bool\n\nDetermines whether a sliding reference configuration is taken at a constant time offset (specified by :py:attr:`frame_offset`) relative to the current frame. If ``False``, a constant reference configuration is used (set by the :py:attr:`reference_frame` parameter) irrespective of the current frame.\n\nDefault: ``False``'

class _PipelineModifiersList(MutableSequence[Modifier]):

    @overload
    def append(self, value: Modifier) -> None: # type: ignore
        ...

    @overload
    def append(self, value: ModifierInterface) -> None: # type: ignore
        ...

    @overload
    def append(self, value: Callable[[int, ovito.data.DataCollection], Optional[Generator[str | float, None, None]]]) -> None:
        ...

@dataclass(kw_only=True)
class Pipeline:
    """This class encapsulates a data pipeline, consisting of a *data source* and a chain of zero or more *modifiers*,
which manipulate the data on the way through the pipeline.

Pipeline creation

Every pipeline has a *data source*, which loads or dynamically generates the input data entering the
pipeline. This source is accessible through the :py:attr:`Pipeline.source` field and may be replaced with a different kind of source object if needed.
For pipelines created by the :py:func:`import_file` function, the data source is automatically set to be a
:py:class:`FileSource` object, which loads the input data
from the external file and feeds it into the pipeline. Another kind of data source is the
:py:class:`StaticSource`, which can be used if you want to programmatically specify the input data for the pipeline
instead of loading it from a file.

The modifiers that are part of the pipeline are accessible through the :py:attr:`Pipeline.modifiers` field.
This list is initially empty and you can populate it with the modifier types found in the :py:mod:`ovito.modifiers` module.
Note that it is possible to employ the same :py:class:`Modifier` instance in more than one pipeline. And it is
okay to use the same data source object for several pipelines, letting them process the same input data.

Pipeline evaluation

Once the pipeline is set up, its computation results can be requested by calling :py:meth:`.compute`, which means that the input data will be loaded/generated by the :py:attr:`source`
and all modifiers of the pipeline are applied to the data one after the other. The :py:meth:`.compute` method
returns a new :py:class:`DataCollection` storing the data objects produced by the pipeline.
Under the hood, an automatic caching system ensures that unnecessary file accesses and computations are avoided.
Repeatedly calling :py:meth:`.compute` will not trigger a recalculation of the pipeline's results unless you
alter the pipeline's data source, the chain of modifiers, or a modifier's parameters.

Usage example

The following code example shows how to create a new pipeline by importing an MD simulation file and inserting a :py:class:`SliceModifier` to
cut away some of the particles. Finally, the total number of remaining particles is printed.

```python
  from ovito.io import import_file
  from ovito.modifiers import SliceModifier
  
  # Import a simulation file. This creates a Pipeline object.
  pipeline = import_file('input/simulation.dump')
  
  # Insert a modifier that operates on the data:
  pipeline.modifiers.append(SliceModifier(normal=(0,0,1), distance=0))
  
  # Compute the effect of the slice modifier by evaluating the pipeline.
  output = pipeline.compute()
  print("Remaining particle count:", output.particles.count)
```

To access the unmodified input data of the pipeline, i.e. *before* it has been processed by any of the modifiers,
you can call the :py:meth:`PipelineNode.compute` method of the pipeline's :py:attr:`source` node:

```python
  # Access the pipeline's input data provided by the FileSource:
  input = pipeline.source.compute()
  print("Input particle count:", input.particles.count)```

Data visualization

If you intend to produce graphical renderings of the output data produced by a pipeline,
you must make the pipeline part of the current three-dimensional scene by calling the :py:meth:`Pipeline.add_to_scene` method.

Data export

To export the generated data of the pipeline to an output file, simply call the :py:func:`ovito.io.export_file` function with the pipeline."""
    source: Optional[PipelineNode] = None
    'source() -> Optional[PipelineNode]\n\nThis property returns the :py:class:`PipelineNode` responsible for producing or loading the input data for this pipeline. This typically is a :py:class:`FileSource` object, if the pipeline was created by the :py:func:`ovito.io.import_file` function. \n\nYou can replace the source node of the pipeline if needed. Available types of sources are: :py:class:`FileSource`, :py:class:`StaticSource`, and :py:class:`PythonSource`. It is possible for several pipelines to share the same source node. \n\nDefault: ``None``'
    head: Optional[PipelineNode] = None
    "head() -> Optional[PipelineNode]\n\nThis field holds the final :py:class:`PipelineNode` of the pipeline, which is the last processing step producing the output of the pipeline. The data obtained from this node is what the pipeline's :py:meth:`compute` method returns. \n\nIf the pipeline contains no `ModificationNodes` yet (an empty :py:attr:`modifiers` list), then the pipeline's :py:attr:`.head` is identical with its :py:attr:`.source`. The following code example demonstrates how the pipeline's head node gets updated when a modifier is inserted into the pipeline:\n\n```python\n  pipeline = import_file('input/nylon.data')\n  assert isinstance(pipeline.source, FileSource)\n  assert pipeline.head is pipeline.source\n  \n  slice = SliceModifier(normal=(0, 0, 1), distance=10.0)\n  pipeline.modifiers.append(slice)\n  assert isinstance(pipeline.head, ModificationNode)\n  assert pipeline.head.modifier is slice\n  assert pipeline.head.input is pipeline.source\n```\n\nExplicitly specifying a pipeline's head node can be used to branch off from another pipeline:\n\n```python\n  pipeline_A = import_file('input/nylon.data')\n  pipeline_A.modifiers.append(SliceModifier(normal=(0, 0, 1), distance=10.0))\n  pipeline_B = Pipeline(head=pipeline_A.head)\n  pipeline_A.modifiers.append(ClusterAnalysisModifier(cutoff=2.5))\n  pipeline_B.modifiers.append(ColorCodingModifier(property='Potential Energy'))\n```\n\nIn this example, pipeline *A* and pipeline *B* share the same :py:class:`FileSource` and a :py:class:`SliceModifier`. After the bifurcation, pipeline branch *A* continues with a :py:class:`ClusterAnalysisModifier` while pipeline branch *B* continues with a :py:class:`ColorCodingModifier`.\n\nDefault: ``None``"
    preliminary_updates: bool = True
    'preliminary_updates() -> bool\n\nThis flag controls whether interactive :py:class:`Viewport` windows should get refreshed while a pipeline computation is in progress to display intermediate computation results of modifiers computed so far. This flag only has an effect in the graphical user interface and if the pipeline is part of the scene. Setting this flag to ``False`` turns frequent, sometimes undesired viewport updates off. Then a single viewport refresh will occur only once the final pipeline output is fully computed. \n\nDefault: ``True``'
    trajectory_caching: bool = False
    first_frame: int = 0
    num_frames: int = 0
    "num_frames() -> int\n\nRead-only property indicating the number of trajectory frames that can be obtained from this pipeline. \n\nThis value matches the :py:attr:`PipelineNode.num_frames` value of the pipeline's :py:attr:`head` node unless the :py:attr:`FileSource.playback_ratio` parameter has been set to a value other than 1:1. Note that the number of *output* animation frames produced by the pipeline may differ from the number of *input* trajectory frames if the pipeline contains modifiers that alter the frame count in some way, e.g. the :py:class:`LoadTrajectoryModifier`."

    def add_to_scene(self, *, translation: ArrayLike=(0, 0, 0), rotation: ArrayLike=(0, 0, 0), name: str='') -> None:
        """Inserts the pipeline into the three-dimensional scene by appending it to the :py:attr:`ovito.Scene.pipelines` list. The visual representation of the pipeline's output data will appear in rendered images and in the interactive viewports. You can remove the pipeline from the scene again by calling :py:meth:`.remove_from_scene`.

The *translation* parameter controls the position of the pipeline's visual representation in the three-dimensional scene. The translation is specified as a 3d vector in units of the scene's coordinate system. The *rotation* parameter can be used to apply a rotational transformation to the entire output produced by the pipeline. This 3d rotation is specified as a *Rodrigues vector* in units of radians. The axis of rotation is given by the vector's direction, while its length determines the rotation angle around that axis. 

If you call :py:meth:`add_to_scene` again while the pipeline is already part of the scene, the function will update the pipeline's transformation in the scene. 

  The *translation* and *rotation* parameters were added.

  The *name* parameter was added."""
        ...

    def remove_from_scene(self) -> None:
        """Removes the visual representation of the pipeline from the scene by deleting it from the :py:attr:`ovito.Scene.pipelines` list. The output data of the pipeline will disappear from the viewports."""
        ...

    def compute(self, frame: Optional[int]=None) -> ovito.data.DataCollection:
        """Computes and returns the output of this data pipeline (for one trajectory frame).

This method requests an evaluation of the pipeline and blocks until the input data has been obtained from the
data :py:attr:`source`, e.g. a simulation file, and all modifiers have been applied to the data. If you invoke the :py:meth:`compute` method repeatedly
without changing the modifiers in the pipeline between calls, the pipeline may serve subsequent requests by returning cached output data.

The optional *frame* parameter specifies the animation frame at which the pipeline should be evaluated. Frames are consecutively numbered and range from
0 to :py:attr:`num_frames`-1. If you don't specify a particular frame, the current time slider position will be used when running in an interactive OVITO Pro session,
or frame 0 will be assumed if running in a non-interactive context.

:param frame: The zero-based animation frame number at which the pipeline should be evaluated.
:returns: A :py:class:`DataCollection` produced by the pipeline holding the data of the requested frame.

The method raises a ``RuntimeError`` if the pipeline could not be successfully evaluated for some reason.
This may happen due to invalid modifier settings and file I/O errors, for example."""
        ...

    @property
    def frames(self) -> Iterator[ovito.data.DataCollection]:
        """Returns an iterator that yields the :py:class:`DataCollection` computed by the pipeline for each frame.
It can be used instead of the more explicit :py:meth:`compute` method to obtain the data of all frames of a trajectory.

The following iteration loop calculates the particles center of mass for each frame produced by the pipeline:

```python
  for frame, data in enumerate(pipeline.frames):
      com = numpy.mean(data.particles.position, axis=0)
      print(f"Center of mass at frame {frame} is {com}")
```

The length of the iterator, `len(pipeline.frames)`, is equal to the pipeline's :py:attr:`num_frames` property."""
        ...

    def make_vis_element_independent(self, vis: ovito.vis.DataVis) -> ovito.vis.DataVis:
        """Replaces a :py:class:`DataVis` visual element in the pipeline's output :py:class:`DataCollection` by an independent copy. The copy will be exclusively associated with this pipeline, allowing changes to the visualization settings without affecting other pipelines that share the same data source. 

:param vis: The common visual element to be replaced by an independent copy. 
                              Must be a :py:class:`DataVis` instance associated with some :py:class:`DataObject` 
                              in the pipeline's output data collection
:returns: The replacement :py:class:`DataVis` object, which will be used at rendering time only by this pipeline.


The pipeline manages the created replacement visual element internally. You can later call :py:meth:`get_replacement_vis_element` to retrieve it again if necessary. Note that the pipeline does not actually replace the original element in the output :py:class:`DataCollection`. The new visual element will instead be used at rendering time in place of the original one. 

```python
  pipeline_A = import_file('input/nylon.data')
  pipeline_B = Pipeline(head=pipeline_A.head) # Branch pipeline B off of pipeline A
  
  # Insert both pipelines into the visualization scene
  pipeline_A.add_to_scene()
  pipeline_B.add_to_scene()
  
  # Obtain output data collection of pipelines A and B. Both pipelines share the same pipeline nodes,
  # which means they yield an identitical output, which also means they implicitly share the same
  # visual elements attached to the data objects.
  data = pipeline_B.compute()
  assert pipeline_A.compute().particles.vis is data.particles.vis
  
  # A change to a visual element thus affects the rendering of both pipelines A and B.
  data.particles.vis.scaling = 0.8
  
  # In order to configure the visualization of particles differently in pipelines A and B,
  # we need to create an independent visual element in pipeline B, for example.
  vis_B = pipeline_B.make_vis_element_independent(data.particles.vis)
  
  # Configure replacement visual element for pipeline B (leaving the original one in pipeline A unchanged)
  vis_B.shape = ParticlesVis.Shape.Circle
```"""
        ...

    def get_replacement_vis_element(self, vis: ovito.vis.DataVis) -> ovito.vis.DataVis:
        """Returns the :py:class:`DataVis` element that is used by this pipeline in place of the original element *vis*. If *vis* has not been replaced by an independent copy using :py:meth:`make_vis_element_independent`, the function returns *vis* itself. 

:param ovito.vis.DataVis vis: A visual element associated with some :py:class:`DataObject` 
                              in this pipeline's output data collection.
:returns: The :py:class:`DataVis` element used in place of the given element when rendering the pipeline's output data."""
        ...

    def set_viewport_visibility(self, viewport: ovito.vis.Viewport, visible: bool) -> None:
        ...

    @property
    def modifiers(self) -> _PipelineModifiersList:
        """The sequence of modifiers in the pipeline.

This list contains any modifiers that are applied to the input data provided by the pipeline's data :py:attr:`source`. You
can add and remove modifiers as needed using standard Python ``append()`` and ``del`` operations. The
head of the list represents the beginning of the pipeline, i.e. the first modifier receives the data from the
data :py:attr:`source`, manipulates it and passes the results on to the second modifier in the list and so forth.

Example: Adding a new modifier to the end of a data pipeline::

   pipeline.modifiers.append(WrapPeriodicImagesModifier())"""
        ...