.. _particle-python-page:

####################################
Using the Particle Module in Python
####################################

The ``cee_envision.pt`` module exposes the :ref:`particle-page` to Python via SWIG bindings.
The API mirrors the C++ ``cee::pt`` namespace exactly — all classes, methods and constants are
available under the same names.

Opening a Model
===============

.. code-block:: python

    from cee_envision import pt

    model = pt.ParticleModel()

    err = pt.Error()
    if not model.openFile("data/particles.ptfx", err):
        print("Error:", err.errorCode())

``openFile()`` is a Python-only convenience wrapper that calls the C++ ``open(filePath)`` overload.
To use a pre-constructed reader instead, call ``openWithReader(reader, err)``. Python ownership of
the reader is transferred to C++ and the reader must not be used afterwards.

Adding the Model to a View and Rendering
=========================================

.. code-block:: python

    from cee_envision import vis, osmesa, core

    context_group = osmesa.OSMesaComponent.createOpenGLContextGroupForOSMesa()
    viewer = osmesa.ViewerOSMesa(context_group)

    view = vis.View()
    viewer.setView(view)
    viewer.resize(1024, 768)

    view.addModel(model)
    model.updateVisualization()

    box = view.boundingBox()
    if box.isValid():
        view.camera().fitView(box, core.Vec3d(0, 0, 1), core.Vec3d(0, 1, 0))

See :ref:`particle-model-page` for the full C++ model API.

Scalar Mapping
==============

.. code-block:: python

    fields = model.scalarFieldNames()   # returns a list of strings
    if fields:
        model.setActiveScalarField(fields[0])

        mapper = vis.ScalarMapperContinuous()
        scalar_range = model.scalarRangeAsTuple()   # Python-only: returns (min, max) or None
        if scalar_range:
            mapper.setRange(scalar_range[0], scalar_range[1])
        mapper.setColors(vis.ColorTableFactory.BLUE_RED)
        model.setScalarMapper(mapper)

``scalarRangeAsTuple()`` is a Python-only helper. The underlying C++ method ``scalarRange()`` takes
output pointer arguments and is not directly callable from Python.

.. note::

    ``ParticleModel`` automatically adds a color legend overlay to the view when a scalar mapper is
    set. There is no need to add one manually via ``view.overlay()``.

Frame Animation
===============

.. code-block:: python

    frame_count = model.frameCount()

    # Pre-load frames into the cache for smooth playback
    model.preloadFrames(0, frame_count)

    for i in range(frame_count):
        model.setCurrentFrameIndex(i)
        model.updateVisualization()
        # render frame ...

See :ref:`particle-frames-scalars-page` and :ref:`particle-cache-page` for details.

Low-level Data Access
======================

Use a reader directly to inspect raw frame data without rendering:

.. code-block:: python

    reader = pt.createReader("data/particles.ptfx")   # auto-detects format

    err = pt.Error()
    if not reader.open("data/particles.ptfx", err):
        print("Error:", err.errorCode())

    header = reader.header()
    print("Frames:", header.frameCount)
    print("Fields:", list(header.scalarFieldNames))

    frame = reader.getFrameDataAsPtr(0)   # Python-only: returns FrameData* (or None)
    if frame:
        print("Particles:", frame.particleCount)
        print("Positions:", frame.positions[:6])  # first two XYZ triples

``getFrameDataAsPtr()`` is a Python-only wrapper around the C++ ``getFrameData()`` which returns a
``unique_ptr`` that cannot be expressed directly in Python. The returned object is owned by Python
and will be freed when it goes out of scope.

See :ref:`particle-readers-page` for the full reader API.

Writing PTFX Files
==================

.. code-block:: python

    writer = pt.PtfxDatasetWriter()
    err = pt.Error()

    scalar_infos = [pt.PtfxScalarFieldInfo()]
    scalar_infos[0].name = "Temperature"
    scalar_infos[0].minValue = 273.0
    scalar_infos[0].maxValue = 1500.0

    writer.open("output.ptfx", max_particles, frame_count,
                bbox_min, bbox_max, scalar_infos, err)

    for frame_idx in range(frame_count):
        writer.writeFrame(ids, positions, scalars, err)

    writer.close(err)

See :ref:`particle-writer-page` for the full writer API.

Example Scripts
===============

Four ready-to-run scripts are provided in ``Examples/ParticleExamples/``:

-   ``render_particles.py`` — renders a single mid-animation frame to a PNG.
-   ``animate_particles.py`` — renders all frames to PNGs and optionally assembles an MP4
    (requires ``pip install opencv-python``).
-   ``particles_with_decimation.py`` — renders the same frame at 10 %, 50 % and 100 % density.
-   ``read_and_write_particles.py`` — reads frame data and writes a subset to a new ``.ptfx`` file.

All scripts default to the demo datasets in ``Examples/DemoFiles/particles/`` but accept a custom
file path as a command-line argument::

    python render_particles.py path/to/my/particles.vtp
