.. _particle-readers-page:

##########################
Particle Dataset Readers
##########################

Custom Particle Format Integration
===================================

If your data is in a format not natively supported, you can implement the abstract
:class:`ParticleDatasetReader <cee::pt::ParticleDatasetReader>` interface and pass an instance to the model:

.. code-block:: cpp

    #include "CeeParticleModel/ParticleModel.h"
    #include "CeeParticleModel/ParticleDatasetReader.h"

    auto reader = std::make_unique<MyCustomReader>();
    cee::pt::Error error;
    cee::PtrRef<cee::pt::ParticleModel> model = new cee::pt::ParticleModel;

    if (!model->open(std::move(reader), &error))
    {
        // Handle error
    }

The custom reader must implement the pure virtual methods:

- :func:`open() <cee::pt::ParticleDatasetReader::open>` - open the file and populate the :class:`DatasetHeader <cee::pt::DatasetHeader>`.
- :func:`readFrame() <cee::pt::ParticleDatasetReader::readFrame>` - read a single frame and return a :class:`FrameData <cee::pt::FrameData>` instance.
- :func:`setActiveScalarField() <cee::pt::ParticleDatasetReader::setActiveScalarField>` - select which scalar field to include in subsequent frames.

The base class provides additional public methods:

- :func:`close() <cee::pt::ParticleDatasetReader::close>` - close the reader and release resources.
- :func:`clearActiveScalarField() <cee::pt::ParticleDatasetReader::clearActiveScalarField>` - deactivate the current scalar field.
- :func:`header() <cee::pt::ParticleDatasetReader::header>` - return the :class:`DatasetHeader <cee::pt::DatasetHeader>`.
- :func:`getFrameData() <cee::pt::ParticleDatasetReader::getFrameData>` - read (or retrieve from cache) a decoded frame.

Each frame is returned as a :class:`FrameData <cee::pt::FrameData>` structure containing:

- ``particleCount`` - number of particles in the frame
- ``positions`` - interleaved XYZ floats (size = particleCount x 3)
- ``ids`` - per-particle identifiers
- ``scalars`` - per-particle scalar values for the active field (empty if none active)
- ``bbox`` - frame bounding box
- ``scalarRangeMin`` / ``scalarRangeMax`` - scalar value range for the frame

The :class:`FrameData <cee::pt::FrameData>` struct also provides utility methods:

- :func:`clone() <cee::pt::FrameData::clone>` - create a deep copy of the frame data
- :func:`memoryBytes() <cee::pt::FrameData::memoryBytes>` - get the approximate memory usage of the frame in bytes

The :class:`DatasetHeader <cee::pt::DatasetHeader>` returned by
:func:`header() <cee::pt::ParticleDatasetReader::header>` describes the overall dataset:

- ``maxParticleCount`` - maximum number of particles across all frames
- ``frameCount`` - total number of timesteps
- ``bbox`` - global bounding box
- ``scalarFieldNames`` - list of available scalar field names
- ``scalarRangeMin`` / ``scalarRangeMax`` - global scalar range
- ``scalarRangeValid`` - whether the global scalar range has been populated
- ``activeScalarField`` - currently active scalar field name (empty if none)


Using the Built-in Particle Readers Directly
==============================================

While :class:`ParticleModel <cee::pt::ParticleModel>` provides the easiest path for visualization, you can also use the
concrete readers directly for data access without rendering:

.. code-block:: cpp

    #include "CeeParticleModel/PtfxDatasetReader.h"
    #include "CeeParticleModel/VtpDatasetReader.h"

    // PTFX reader
    cee::pt::PtfxDatasetReader ptfxReader;
    ptfxReader.open("data/simulation.ptfx");

    // VTP reader
    cee::pt::VtpDatasetReader vtpReader;
    vtpReader.open("data/particles_0000.vtp");  // Opens and discovers the full sequence

    // Query header info
    cee::pt::DatasetHeader header = vtpReader.header();
    size_t frameCount = header.frameCount;

    // Read a frame
    vtpReader.setActiveScalarField("Temperature");
    auto frame = vtpReader.getFrameData(0);

The :class:`VtpDatasetReader <cee::pt::VtpDatasetReader>` additionally provides
:func:`readMultiScalarFrame() <cee::pt::VtpDatasetReader::readMultiScalarFrame>` for reading multiple scalar fields in
a single pass (returned as a :class:`MultiScalarFrameData <cee::pt::MultiScalarFrameData>` struct):

.. code-block:: cpp

    std::vector<cee::Str> fields = {"Temperature", "Pressure"};
    auto multiFrame = vtpReader.readMultiScalarFrame(0, fields);

    // MultiScalarFrameData contains:
    //   particleCount  - number of particles
    //   positions      - interleaved XYZ floats
    //   ids            - per-particle identifiers
    //   scalarArrays   - one vector<float> per requested field (same order as input)
    uint32_t count = multiFrame->particleCount;
    const std::vector<float>& tempValues = multiFrame->scalarArrays[0];
    const std::vector<float>& pressValues = multiFrame->scalarArrays[1];

You can also query the discovered frame file paths with :func:`framePaths() <cee::pt::VtpDatasetReader::framePaths>`:

.. code-block:: cpp

    std::vector<cee::Str> paths = vtpReader.framePaths();
    // Returns the ordered list of .vtp files discovered for the sequence

You can also use the factory function :func:`createParticleDatasetReader() <cee::pt::createParticleDatasetReader>` to
create the appropriate reader for a file path without knowing the format in advance:

.. code-block:: cpp

    #include "CeeParticleModel/ParticleDatasetReaderFactory.h"

    auto reader = cee::pt::createParticleDatasetReader("data/particles.ptfx");
