{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "\n# Opening MHD Model Files with PsiData\n\nExplore a MAS radial-magnetic-field file through the :func:`~psi_io.mhd_io.PsiData`\ninterface: inspect metadata attributes, trace the connections to :mod:`psi_io.models`,\n:mod:`psi_io.mesh`, and :mod:`psi_io.units`, and observe the lazy-loading and\ncaching behavior.\n\nThis example demonstrates:\n\n1. Opening a MAS HDF5 file and exploring the reader's metadata attributes.\n2. The role of :mod:`psi_io.models` in defining physical quantity properties.\n3. How :mod:`psi_io.mesh` encodes Yee-grid stagger positions for each quantity.\n4. How :mod:`psi_io.units` supplies the MAS code-unit normalization factors.\n5. Lazy loading \u2014 no data leaves the disk until explicitly requested.\n6. Automatic caching of full-array reads for quick re-access.\n\n<div class=\"alert alert-info\"><h4>Note</h4><p>:func:`~psi_io.mhd_io.PsiData` is the *only* public symbol exported by\n   :mod:`psi_io.mhd_io`.  HDF4 (``.hdf``) and HDF5 (``.h5``) files are supported\n   transparently; the file extension selects the I/O backend.</p></div>\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "from pathlib import Path\nfrom psi_data import fetch_mas_data\nfrom psi_io.mhd_io import PsiData"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "**Opening a file**\n\n:func:`~psi_io.mhd_io.PsiData` takes a path to any PSI MAS or POT3D HDF file.\nNo data is read at this point \u2014 only the filename is parsed and minimal HDF\nmetadata is inspected to identify the quantity, units, and mesh code.\n\nTo define the metadata data of the given input file, the reader follows a hierarchy of\ninference steps to determine the values of the following core attributes:\n\n``'name'``\n    Canonical lower-case quantity identifier.\n``'sequence'``\n    Integer time-step sequence number.\n``'unit'``\n    Code-to-physical unit for this quantity, as an :class:`~astropy.units.Unit`\n    or a string parseable by it.\n``'scalar'``\n    ``True`` if the quantity is a scalar field; ``False`` for vector components.\n``'mesh'``\n    Mesh code (:data:`~psi_io.mesh.MeshCodeType`) describing data staggering.\n\nIf these values are not explicitly included in the :func:`~psi_io.mhd_io.PsiData`\nconstructor the reader falls back to reading the HDF metadata attributes (if present) and then\nparsing the filename according to the PSI filename schema. The\nreader then cross-references the quantity against the canonical properties defined\nin :mod:`psi_io.models` to infer the remaining metadata attributes.\n\nThe ``model`` argument selects which property table to consult.  Passing\n``model='mas'`` tells the reader to resolve metadata from the MAS quantity\nmapping; without it, the default ``model='custom'`` requires every metadata\nfield to be supplied explicitly.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "br_filepath = fetch_mas_data(domains=\"cor\", variables=\"br\").cor_br\nprint(f\"Filename : {Path(br_filepath).name}\")\nreader = PsiData(br_filepath, model='mas')"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "**Core metadata attributes**\n\nThe :attr:`name` and :attr:`sequence` attributes are extracted from the\nfilename stem using the PSI filename schema (*e.g.* ``br001001.h5`` gives\n``name='br'``, ``sequence=1001``). Since the provided filename does not\ncontain an explicit sequence number, the reader defaults to ``sequence=0``.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "print(f\"name      : {reader.name!r}\")\nprint(f\"sequence  : {reader.sequence}\")\nprint(f\"ndim      : {reader.ndim}\")\nprint(f\"shape     : {reader.shape}  (Nr \u00d7 N\u03b8 \u00d7 N\u03c6 in physical order)\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "**Array ordering**\n\nThe :attr:`order` attribute records the in-memory layout of the dataset, mirroring\nthe :attr:`~psi_io.models.ModelProps.order` field: ``'F'`` for Fortran (column-major,\nthe PSI default) or ``'C'`` for C (row-major).  PSI HDF files are written Fortran-ordered,\nso the on-disk storage order is ``(N\u03c6, N\u03b8, Nr)`` \u2014 the *reverse* of the physical\n``(r, \u03b8, \u03c6)`` order.  This is precisely why :attr:`shape` (reported above in physical\n``(r, \u03b8, \u03c6)`` order) is the reverse of the raw HDF storage shape, and why every positional\nargument to :meth:`~psi_io.mhd_io.PsiData.read` is supplied in physical order regardless of\nhow the bytes are laid out on disk.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "print(f\"order        : {reader.order!r}  ('F' = Fortran/column-major, PSI default)\")\nprint(f\"shape (phys) : {reader.shape}        (r, \u03b8, \u03c6)\")\nprint(f\"shape (HDF)  : {reader.shape[::-1]}        (N\u03c6, N\u03b8, Nr storage order)\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "**Connection to** :mod:`psi_io.models`\n\nThe reader's metadata is resolved against the\n:class:`~psi_io.models.ModelProps` dataclass stored in :mod:`psi_io.models`,\nwhich bundles the canonical name, description, native unit, and mesh code for\nevery recognised PSI quantity.  The :attr:`name` and :attr:`desc` attributes\nexpose this resolved metadata directly.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "print(f\"name : {reader.name}\")\nprint(f\"desc : {reader.desc}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "**Connection to** :mod:`psi_io.mesh`\n\nThe :attr:`mesh` attribute is a :class:`~psi_io.mesh.Mesh` instance\n(one stagger flag per spatial axis in physical ``(r, \u03b8, \u03c6)`` order) that encodes\nthe Yee-grid stagger position of the field quantity.\n\nFor the radial magnetic field ``br``, the field is face-centred in the radial\ndirection (*half*-mesh) and cell-centred in both angular directions (*main*-mesh):\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "from psi_io.mesh import Mesh\nprint(f\"mesh : {reader.mesh}\")\nprint(f\"  r  \u2192 {reader.mesh[0]}\")\nprint(f\"  \u03b8  \u2192 {reader.mesh[1]}\")\nprint(f\"  \u03c6  \u2192 {reader.mesh[2]}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "**Connection to** :mod:`psi_io.units`\n\nThe :attr:`unit` attribute is one of the custom MAS normalization units defined\nin :mod:`psi_io.units`.  Multiplying a code-unit value by this factor converts\nit to physical CGS units.  Here, ``MAS_b`` represents approximately 2.2 Gauss\nper code unit.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "from psi_io.units import MAS_b\nprint(f\"unit     : {reader.unit}\")\nprint(f\"MAS_b    : {MAS_b}\")\nprint(f\"in Gauss : {reader.unit.to('G'):.4f}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "**Coordinate scale readers**\n\nThe :attr:`scales` attribute is a named tuple of coordinate readers whose field\nnames \u2014 and their order \u2014 come from the :attr:`~psi_io.models.ModelProps.scales`\nfield (default ``('r', 't', 'p')``).  This tuple *defines* the physical\n``(r, \u03b8, \u03c6)`` axis ordering used everywhere else in the API: it is the order of\nthe :attr:`shape`, the per-axis flags of :attr:`mesh`, and every positional\nargument accepted by :meth:`~psi_io.mhd_io.PsiData.read` and\n:meth:`~psi_io.mhd_io.PsiData.vslice`.  Each element is itself a lightweight\nreader; calling :meth:`read` on a scale returns the 1-D coordinate array as a\n:class:`~astropy.units.Quantity`.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "print(f\"scale order : {reader.scales._fields}  (from ModelProps.scales)\")\n\nr_scale = reader.scales.r.read()\nt_scale = reader.scales.t.read()\np_scale = reader.scales.p.read()\nprint(f\"r scale  : shape={r_scale.shape}  range=[{r_scale[0]:.5f}, {r_scale[-1]:.5f}]\")\nprint(f\"\u03b8 scale  : shape={t_scale.shape}  range=[{t_scale[0]:.5f}, {t_scale[-1]:.5f}]\")\nprint(f\"\u03c6 scale  : shape={p_scale.shape}  range=[{p_scale[0]:.5f}, {p_scale[-1]:.5f}]\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "**Lazy loading**\n\nReading the coordinate scales above did *not* load the main data array.  The\n:attr:`data_cached` property confirms the primary dataset has not yet been\ntransferred from disk:\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "print(f\"data_cached before read : {reader.data_cached}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Calling :meth:`~psi_io.mhd_io.PsiData.read` with no arguments loads the full\ndataset.  Because no spatial restrictions are applied, the result is stored in\nthe reader's internal cache:\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "data_arr, r, t, p = reader.read()\nprint(f\"data shape  : {data_arr.shape}  (N\u03c6 \u00d7 N\u03b8 \u00d7 Nr in HDF storage order)\")\nprint(f\"data unit   : {data_arr.unit}\")\nprint(f\"data_cached : {reader.data_cached}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Subsequent full-array calls return the cached copy without a second disk read.\nThe cache is populated only for unrestricted reads; any partial read (*i.e.* any\ncall that restricts at least one axis) bypasses and never updates the cache.\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.9"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}