{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "\n# Faux Volume Rendering\n\nThis example demonstrates a technique for simulating volumetric rendering of\ncoronal density by deconstructing a 3-D\n:class:`~pyvisual.core.mesh3d.SphericalMesh` into stacked semi-transparent\nradial shells \u2014 a *faux* volume that avoids the GPU memory cost of true\nvolumetric rendering while still conveying the large-scale 3-D structure of\nthe corona.\n\nThe scene is animated along a Parker Solar Probe (PSP) trajectory\nobtained from the JPL Horizons ephemeris service via\n:func:`~pyvisual.utils.geometry.spacecraft_trajectory`, placing the virtual\nobserver at each spacecraft position and framing the field of view in\nhelioprojective angular coordinates using\n:attr:`~pyvisual.core.mixins.ObserverMixin.observer_los_view`.\n\n.. seealso::\n\n   `sphx_glr_gallery_04_observer_mixin_p02_orbit.py`\n      Simpler orbit animation driven by a synthetic observer path.\n\n   `sphx_glr_gallery_99_advanced_plots_p01_combining_multiple_elements.py`\n      Multi-layer coronal scene that combines slices, contours, and fieldlines.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "import os\nfrom pathlib import Path\n\nimport numpy as np\nfrom pyvisual import Plot3d\nfrom pyvisual.core.mesh3d import SphericalMesh\nfrom pyvisual.utils.data import fetch_datasets\nfrom pyvisual.utils.geometry import spacecraft_trajectory"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Load Data\n\n:func:`~pyvisual.utils.geometry.spacecraft_trajectory` queries the JPL\nHorizons ephemeris for Parker Solar Probe over a four-day window and returns\na :class:`numpy.ndarray` of shape ``(3, N)`` whose rows are\n$(r,\\,\\theta,\\,\\phi)$ in the Carrington frame, sampled at the default\n1-hour cadence.\n\n:func:`~pyvisual.utils.data.fetch_datasets` downloads (or loads from cache)\nthe CR 2282 coronal density field on the\n$1\\text{\u2013}30\\,R_\\odot$ grid and returns the path to the HDF5 file as\nthe named attribute ``cor_rho``.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "trajectory = spacecraft_trajectory('psp', '2024-03-27', '2024-03-31')\nrho_file = fetch_datasets(\"cor\", \"rho\").cor_rho"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Build the Faux Volume Representation\n\nThe full density grid is loaded into a\n:class:`~pyvisual.core.mesh3d.SphericalMesh`.  Indexing with ``[0, ...]``\nselects the innermost radial shell at $r \\approx 1\\,R_\\odot$ as a\n2-D reference surface for the photospheric density.\n\nThe remaining shells ``[1:, ...]`` form the 3-D coronal volume.  Applying\n:obj:`numpy.log` via the mesh arithmetic suite compresses the large dynamic\nrange of coronal density.\n\n:meth:`~pyvisual.core.mesh3d.SphericalMeshFilters.deconstruct` with\n``method='slices'`` converts the 3-D :class:`pyvista.RectilinearGrid` into a\ncollection of 2-D radial shell surfaces (via\n:func:`~pyvisual.core.mesh3d.build_slice_polydata`).  Rendered with PyVista's\n``'sigmoid_7'`` opacity transfer function, the stacked shells map low\nlog-density regions to near-transparent and high log-density regions to\nnear-opaque, mimicking the appearance of a volumetric render without\nrequiring volume rendering hardware support.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "mesh = SphericalMesh(rho_file)\nradial_slice_at_photosphere = mesh[0, ...]\ndeconstructed_mesh_volume = np.log(mesh[1:, ...]).deconstruct(method='slices')"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Animate Along the Spacecraft Trajectory\n\nThe scene is assembled once and then animated by stepping the observer through\neach position in the PSP trajectory.  Iterating over ``trajectory.T`` yields\none $(r,\\,\\theta,\\,\\phi)$ column per time step, which is assigned\ndirectly to :attr:`~pyvisual.core.mixins.ObserverMixin.observer_position`.\n\n:attr:`~pyvisual.core.mixins.ObserverMixin.observer_los_view` frames the\nfield of view as helioprojective angular extents\n$(x_0,\\, x_1,\\, y_0,\\, y_1)$ in degrees. Re-applying the FOV at\nevery frame ensures consistent framing as the spacecraft distance changes\nover the trajectory.\n\n<div class=\"alert alert-danger\"><h4>Warning</h4><p>The interactive 3-D viewer is omitted here because the deconstructed\n   shell mesh is prohibitively large to embed in a browser. The animation\n   is also generated and cached outside the sphinx-gallery pipeline:\n   sphinx-gallery natively scrapes GIF output, but the 256-color palette\n   limit of the GIF format renders poorly at this scene's dynamic range, so\n   the movie is written as an MP4 via a separate pre-build step and embedded\n   below using a raw HTML ``<video>`` tag.  To regenerate the MP4 locally,\n   run the script directly (without the ``SPHINX_GALLERY_BUILD`` environment\n   variable set).</p></div>\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "if not os.environ.get('SPHINX_GALLERY_BUILD'):\n    # The following lines are included to standardize the output path for the sphinx-gallery\n    # pre-processing pipeline mentioned above. These values can be omitted/altered if running\n    # the script directly.\n\n    output_dir = Path(os.environ.get(\"STATIC_ASSETS\", \"\")).resolve()\n    movie_name = f\"p07_faux_volume_render.mp4\"\n    screenshot_name = f\"p07_faux_volume_render.png\"\n\n    plotter = Plot3d()\n    plotter.add_axes()\n    plotter.add_mesh(radial_slice_at_photosphere, show_scalar_bar=False)\n    plotter.add_mesh(deconstructed_mesh_volume, opacity='sigmoid_7', show_scalar_bar=False)\n    plotter.open_movie(output_dir / movie_name, framerate=10)\n    for position in trajectory.T:\n        plotter.observer_position = position\n        plotter.observer_los_view = -50, 50, -45, 45\n        plotter.write_frame()\n    plotter.screenshot(output_dir / screenshot_name)\n    plotter.close()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        ".. raw:: html\n\n   <video autoplay loop muted playsinline style=\"width:100%;border-radius:4px;\">\n     <source src=\"../../_static/assets/p07_faux_volume_render.mp4\" type=\"video/mp4\">\n   </video>\n\n"
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.13.12"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}