Source code for psi_io._models

   1r"""Physical property metadata for PSI's model quantities and scales.
   2
   3This module provides the :class:`Props` dataclass and three read-only mapping objects
   4that fully describe every quantity PSI's modeling codes write to HDF files:
   5
   6- The filename/lookup identifier (e.g., ``'br'``)
   7- Human-readable description (e.g., ``'MAS Magnetic Field (Radial Component)'``)
   8- Native code :class:`~astropy.units.Unit` (e.g., ``Unit("2.20689 G")`` or ``Unit("MAS_b")``)
   9- Dimensionality (e.g., 3 for 3-D output fields)
  10- Scalar or vector classification
  11- Mesh staggering code (e.g., ``0b011``)
  12
  13For metadata access, one should use the provided getter functions rather than the mapping
  14objects directly.
  15
  16
  17.. list-table::
  18   :header-rows: 1
  19
  20   * - Mapping
  21     - Model
  22     - Contents
  23   * - :func:`get_mas_quantity_properties`
  24     - `MAS <https://github.com/predsci/MAS>`_ (Magnetohydrodynamic Algorithm outside a Sphere)
  25     - 19 3-D output fields: velocity, magnetic field, current density, temperature,
  26       density, pressure, wave energy, Elsässer amplitudes, heating
  27   * - :func:`get_pot3d_quantity_properties`
  28     - `POT3D <https://github.com/predsci/POT3D>`_ (High Performance Potential Field Solver)
  29     - 3 magnetic field components (br, bt, bp)
  30   * - :func:`get_psi_scale_properties`
  31     - PSI coordinate grids (shared by MAS, POT3D, and related codes)
  32     - 3 1-D coordinate scale arrays (r, t, p)
  33
  34Coordinate scales
  35-----------------
  36Every PSI HDF file stores three 1-D coordinate arrays alongside its data.  These
  37arrays define the spherical grid on which model output is sampled and are standard
  38across MAS, POT3D, and most other PSI codes:
  39
  40.. list-table::
  41   :header-rows: 1
  42   :widths: 10 12 28 24 16
  43
  44   * - Key
  45     - Symbol
  46     - Physical quantity
  47     - Units
  48     - Range
  49   * - ``'r'``
  50     - :math:`r`
  51     - Radial distance
  52     - solar radii (:data:`~psi_io._units.PSI_rsun`)
  53     - :math:`r \geq 1\,R_\odot`
  54   * - ``'t'``
  55     - :math:`\theta`
  56     - Co-latitude (pole = 0)
  57     - radians (:data:`~psi_io._units.PSI_angle`)
  58     - :math:`[0,\,\pi]`
  59   * - ``'p'``
  60     - :math:`\varphi`
  61     - Longitude
  62     - radians (:data:`~psi_io._units.PSI_angle`)
  63     - :math:`[0,\,2\pi]`
  64
  65PSI HDF files are written in **Fortran order** (column-major), so the *first* Fortran
  66dimension varies fastest in memory.  When read into NumPy (row-major), the axis order
  67is reversed: a 3-D array has shape :math:`(N_\varphi, N_\theta, N_r)` and the three
  68scale arrays map to axes as follows:
  69
  70.. list-table::
  71   :header-rows: 1
  72
  73   * - Key
  74     - Fortran dimension
  75     - NumPy axis
  76     - ``arr.shape`` index
  77   * - ``'r'``
  78     - 1st (fastest-varying)
  79     - last
  80     - ``-1``
  81   * - ``'t'``
  82     - 2nd
  83     - middle
  84     - ``-2``
  85   * - ``'p'``
  86     - 3rd (slowest-varying)
  87     - first
  88     - ``0``
  89
  90
  91MAS Model Quantities
  92--------------------
  93MAS outputs 19 3-D fields in spherical coordinates.  Code-unit values are converted
  94to physical (CGS/SI) unit by multiplying by the normalization constants defined in
  95:mod:`psi_io._units`.  Approximate physical scales are given in parentheses.
  96
  97.. list-table::
  98   :header-rows: 1
  99   :widths: 8 12 30 28 10 10
 100
 101   * - Key
 102     - Symbol
 103     - Physical quantity
 104     - Physical unit (CGS)
 105     - Type
 106     - Mesh code
 107   * - ``vr``
 108     - :math:`v_r`
 109     - Radial velocity
 110     - km s⁻¹ (:data:`~psi_io._units.FN_V` ≈ 481 km s⁻¹)
 111     - vector
 112     - ``0b011``
 113   * - ``vt``
 114     - :math:`v_\theta`
 115     - Co-latitude velocity
 116     - km s⁻¹
 117     - vector
 118     - ``0b101``
 119   * - ``vp``
 120     - :math:`v_\varphi`
 121     - Longitude velocity
 122     - km s⁻¹
 123     - vector
 124     - ``0b110``
 125   * - ``br``
 126     - :math:`B_r`
 127     - Radial magnetic field
 128     - G (:data:`~psi_io._units.FN_B` ≈ 2.2 G)
 129     - vector
 130     - ``0b100``
 131   * - ``bt``
 132     - :math:`B_\theta`
 133     - Co-latitude magnetic field
 134     - G
 135     - vector
 136     - ``0b010``
 137   * - ``bp``
 138     - :math:`B_\varphi`
 139     - Longitude magnetic field
 140     - G
 141     - vector
 142     - ``0b001``
 143   * - ``jr``
 144     - :math:`J_r`
 145     - Radial current density
 146     - A m⁻² (:data:`~psi_io._units.FN_J`)
 147     - vector
 148     - ``0b011``
 149   * - ``jt``
 150     - :math:`J_\theta`
 151     - Co-latitude current density
 152     - A m⁻²
 153     - vector
 154     - ``0b101``
 155   * - ``jp``
 156     - :math:`J_\varphi`
 157     - Longitude current density
 158     - A m⁻²
 159     - vector
 160     - ``0b110``
 161   * - ``t``
 162     - :math:`T`
 163     - Single-fluid temperature
 164     - MK (:data:`~psi_io._units.FN_T` ≈ 28 MK)
 165     - scalar
 166     - ``0b111``
 167   * - ``te``
 168     - :math:`T_e`
 169     - Electron temperature
 170     - MK
 171     - scalar
 172     - ``0b111``
 173   * - ``tp``
 174     - :math:`T_p`
 175     - Proton temperature
 176     - MK
 177     - scalar
 178     - ``0b111``
 179   * - ``rho``
 180     - :math:`\rho`
 181     - Plasma density
 182     - cm⁻³ (:data:`~psi_io._units.FN_N` = 10⁸ cm⁻³)
 183     - scalar
 184     - ``0b111``
 185   * - ``p``
 186     - :math:`p`
 187     - Plasma pressure
 188     - erg cm⁻³ (:data:`~psi_io._units.FN_P`)
 189     - scalar
 190     - ``0b111``
 191   * - ``ep``
 192     - :math:`e^+`
 193     - Forward Alfvén wave energy density
 194     - erg cm⁻³
 195     - scalar
 196     - ``0b111``
 197   * - ``em``
 198     - :math:`e^-`
 199     - Backward Alfvén wave energy density
 200     - erg cm⁻³
 201     - scalar
 202     - ``0b111``
 203   * - ``zp``
 204     - :math:`z^+`
 205     - Outward Elsässer amplitude
 206     - km s⁻¹
 207     - scalar
 208     - ``0b111``
 209   * - ``zm``
 210     - :math:`z^-`
 211     - Inward Elsässer amplitude
 212     - km s⁻¹
 213     - scalar
 214     - ``0b111``
 215   * - ``heat``
 216     - :math:`Q`
 217     - Volumetric heating rate
 218     - erg cm⁻³ s⁻¹ (:data:`~psi_io._units.FN_HEAT`)
 219     - scalar
 220     - ``0b111``
 221
 222POT3D Model Quantities
 223----------------------
 224POT3D solves for the potential magnetic field :math:`\mathbf{B} = -\nabla\Psi` driven
 225by a photospheric radial-field boundary condition.  It outputs three spherical
 226magnetic field components:
 227
 228.. list-table::
 229   :header-rows: 1
 230   :widths: 10 12 35 25 18
 231
 232   * - Key
 233     - Symbol
 234     - Physical quantity
 235     - Physical unit
 236     - Mesh code
 237   * - ``br``
 238     - :math:`B_r`
 239     - Radial magnetic field
 240     - input magnetogram unit (typically G)
 241     - ``0b011``
 242   * - ``bt``
 243     - :math:`B_\theta`
 244     - Co-latitude magnetic field
 245     - input magnetogram unit
 246     - ``0b101``
 247   * - ``bp``
 248     - :math:`B_\varphi`
 249     - Longitude magnetic field
 250     - input magnetogram unit
 251     - ``0b110``
 252
 253.. note::
 254
 255   POT3D mesh codes are the bitwise complement of the corresponding MAS magnetic field
 256   codes: ``POT3D_mesh = 0b111 ^ MAS_mesh``.  Where MAS places each component on the
 257   face through which it is the outward normal (face-centred), POT3D places the same
 258   component on the opposite pair of edges (edge-centred).
 259
 260.. warning::
 261
 262   POT3D does not apply a normalization: values are in whatever physical unit the
 263   input boundary magnetogram was provided in (typically Gauss, but run-dependent).
 264   The code unit :data:`~psi_io._units.POT3D_b` has a scale factor of 1
 265   (dimensionless-unscaled).  Always supply the correct unit explicitly when
 266   converting; ``read(unit='physical')`` without a ``unit`` override will return
 267   dimensionless values.
 268
 269Staggered Grid Overview
 270-----------------------
 271MAS and POT3D solve their equations on a three-dimensional **staggered** (Yee-type)
 272spherical grid :math:`(r, \theta, \varphi)`.  Different physical quantities are
 273located at different positions within each grid cell so that discrete differential
 274operators (curl, divergence) are exactly satisfied.
 275
 276With the convention used in this module, numpy arrays loaded from PSI HDF files have
 277shape :math:`(N_{\phi}, N_{\theta}, N_r)` — longitude index varies slowest, radial index
 278fastest.  Mesh codes therefore assign bits as follows:
 279
 280.. list-table::
 281   :header-rows: 1
 282
 283   * - Bit position
 284     - Numpy axis
 285     - Coordinate
 286   * - Most-significant bit (MSB)
 287     - ``axis = -1`` (last)
 288     - :math:`r` (radial)
 289   * - Middle bit
 290     - ``axis = -2``
 291     - :math:`\theta` (co-latitude)
 292   * - Least-significant bit (LSB)
 293     - ``axis = -3`` (first)
 294     - :math:`\varphi` (longitude)
 295
 296A bit value of ``1`` means the quantity is on the **half mesh** along that axis
 297(displaced half a grid spacing), while ``0`` means it is on the **main mesh**.
 298
 299The resulting grid positions for MAS vector components are:
 300
 301.. list-table::
 302   :header-rows: 1
 303
 304   * - Quantity type
 305     - Mesh code
 306     - :math:`r`
 307     - :math:`\theta`
 308     - :math:`\varphi`
 309     - Grid position
 310   * - ``br``
 311     - ``0b100``
 312     - half
 313     - main
 314     - main
 315     - :math:`r`-face center
 316   * - ``bt``
 317     - ``0b010``
 318     - main
 319     - half
 320     - main
 321     - :math:`\theta`-face center
 322   * - ``bp``
 323     - ``0b001``
 324     - main
 325     - main
 326     - half
 327     - :math:`\varphi`-face center
 328   * - ``vr``, ``jr``
 329     - ``0b011``
 330     - main
 331     - half
 332     - half
 333     - :math:`r`-edge center
 334   * - ``vt``, ``jt``
 335     - ``0b101``
 336     - half
 337     - main
 338     - half
 339     - :math:`\theta`-edge center
 340   * - ``vp``, ``jp``
 341     - ``0b110``
 342     - half
 343     - half
 344     - main
 345     - :math:`\varphi`-edge center
 346   * - Scalars (``t``, ``rho``, ``p``, …)
 347     - ``0b111``
 348     - half
 349     - half
 350     - half
 351     - Cell corner
 352
 353This Yee-type arrangement ensures that :math:`\nabla \cdot \mathbf{B} = 0` is
 354satisfied exactly at the discrete level: each :math:`B` component lives on the face
 355through which it is the outward normal.  The current density components
 356:math:`\mathbf{J} = \nabla \times \mathbf{B}` then live on the corresponding cell
 357edges.  Scalar thermodynamic quantities (density, temperature, pressure) occupy the
 358cell corners, which are equivalent to cell centers on the dual grid.
 359
 360See Also
 361--------
 362
 363:mod:`psi_io._mesh` :
 364    Defines :class:`~psi_io._mesh.Mesh` and the mesh normalization helpers.
 365:mod:`psi_io._units` :
 366    Provides the custom astropy unit referenced by each :class:`Props`.
 367:mod:`psi_io.mhd_io` :
 368    Uses these mappings to auto-configure lazy HDF readers.
 369"""
 370
 371from __future__ import annotations
 372
 373__all__ = [
 374    "get_mas_quantity_properties",
 375    "get_pot3d_quantity_properties",
 376    "get_psi_scale_properties",
 377    "parse_psi_filename_schema",
 378    "extract_quantity_from_filepath",
 379    "extract_sequence_from_filepath"
 380]
 381
 382import re
 383from dataclasses import dataclass
 384from pathlib import Path
 385from types import MappingProxyType
 386from typing import Literal, Optional, Callable
 387
 388import astropy.units as u
 389
 390from psi_io._mesh import _normalize_mesh_code
 391from psi_io._units import MAS_v, MAS_b, MAS_j, MAS_t, MAS_n, MAS_p, MAS_heat, POT3D_b, PSI_rsun, PSI_angle
 392
 393
[docs] 394@dataclass(frozen=True, repr=True) 395class Props: 396 """Immutable property bundle for a single PSI model quantity. 397 398 Associates a quantity name with its human-readable description, physical unit, 399 dimensionality, scalar/vector classification, and staggered-grid mesh code. 400 Instances are frozen (immutable) dataclass instances. 401 402 Parameters 403 ---------- 404 name : str 405 Canonical lower-case quantity identifier (e.g. ``'br'``, ``'vr'``). Matches 406 the filename prefix used in MAS and POT3D HDF output. 407 desc : str 408 Human-readable description of the physical quantity. 409 unit : u.Unit 410 Astropy unit whose scale factor converts one code unit of this quantity to 411 physical unit. For example, :data:`~psi_io._units.MAS_b` ≈ 2.2 Gauss. 412 ndim : int 413 Number of spatial dimensions of the output array (``3`` for MAS/POT3D fields, 414 ``1`` for coordinate scale arrays). 415 scalar : bool 416 ``True`` if the quantity is a scalar field (temperature, density, …); 417 ``False`` if it is a component of a vector field (velocity, magnetic field, …). 418 _mesh : int, optional 419 Integer mesh code encoding the stagger position on the three-dimensional grid. 420 Each binary bit indicates whether the quantity is on the half mesh (``1``) or 421 main mesh (``0``) along one coordinate axis. ``None`` for coordinate scale 422 arrays that carry no stagger information (e.g. the radial scale ``'r'``). 423 424 Attributes 425 ---------- 426 mesh : tuple[Mesh, ...] or None 427 Normalized form of :attr:`_mesh`, expanded to a length-:attr:`ndim` tuple of 428 :class:`~psi_io._mesh.Mesh` members. Returns ``None`` when :attr:`_mesh` is 429 ``None`` (coordinate scale arrays have no stagger). 430 431 Notes 432 ----- 433 The arithmetic dunder methods (``__mul__``, ``__rmul__``, ``__rtruediv__``) 434 delegate to :attr:`unit`, so ``some_value * props`` is equivalent to 435 ``some_value * props.unit`` and returns an :class:`~astropy.units.Quantity`. 436 437 Examples 438 -------- 439 >>> from psi_io._models import Props 440 >>> import astropy.units as u 441 >>> p = Props('br', 'Radial B field', u.Gauss, 3, False, 0b100) 442 >>> str(p) 443 'br' 444 >>> p.ndim, p.scalar 445 (3, False) 446 >>> p.mesh # doctest: +NORMALIZE_WHITESPACE 447 (Mesh.HALF, Mesh.MAIN, Mesh.MAIN) 448 >>> (2.5 * p).unit 449 Unit("G") 450 """ 451 452 name: str 453 desc: str 454 unit: u.Unit 455 ndim: int 456 scalar: bool 457 _mesh: Optional[int] = None 458 459 @property 460 def mesh(self): 461 """Normalized mesh-stagger tuple for this quantity. 462 463 Converts the integer :attr:`_mesh` code to a length-3 tuple of 464 :class:`~psi_io._mesh.Mesh` members via 465 :func:`~psi_io._mesh._normalize_mesh_code`. 466 467 Returns 468 ------- 469 out : tuple[Mesh, Mesh, Mesh] or None 470 One :class:`~psi_io._mesh.Mesh` value per spatial dimension 471 ``(r, theta, phi)`` — in the order they appear in 472 :func:`~psi_io._mesh.main_mesh` processing (most-significant bit first, 473 mapping to the last numpy axis). Returns ``None`` when :attr:`_mesh` is 474 ``None`` (coordinate scale arrays have no stagger). 475 476 Examples 477 -------- 478 >>> from psi_io._models import _MAS_QUANTITY_PROPS_MAPPING 479 >>> from psi_io._mesh import Mesh 480 >>> _MAS_QUANTITY_PROPS_MAPPING['br'].mesh 481 (Mesh.HALF, Mesh.MAIN, Mesh.MAIN) 482 >>> _MAS_QUANTITY_PROPS_MAPPING['t'].mesh 483 (Mesh.HALF, Mesh.HALF, Mesh.HALF) 484 >>> _MAS_QUANTITY_PROPS_MAPPING['vr'].mesh 485 (Mesh.MAIN, Mesh.HALF, Mesh.HALF) 486 """ 487 return _normalize_mesh_code(self._mesh, self.ndim) if self._mesh is not None else None 488 489 def __str__(self) -> str: 490 """Return the quantity name. 491 492 Returns 493 ------- 494 out : str 495 The :attr:`name` field (e.g. ``'br'``, ``'vr'``). 496 497 Examples 498 -------- 499 >>> from psi_io._models import _MAS_QUANTITY_PROPS_MAPPING 500 >>> str(_MAS_QUANTITY_PROPS_MAPPING['br']) 501 'br' 502 """ 503 return self.name 504 505 def __mul__(self, other): 506 """Multiply *other* by this quantity's unit. 507 508 Allows ``props * value`` expressions that return an astropy 509 :class:`~astropy.units.Quantity`. 510 511 Parameters 512 ---------- 513 other : numeric or u.Quantity 514 The value to multiply by :attr:`unit`. 515 516 Returns 517 ------- 518 out : u.Quantity 519 ``other * self.unit``. 520 521 Examples 522 -------- 523 >>> from psi_io._models import _MAS_QUANTITY_PROPS_MAPPING 524 >>> props = _MAS_QUANTITY_PROPS_MAPPING['vr'] 525 >>> (props * 1.0).unit # doctest: +ELLIPSIS 526 Unit(...) 527 """ 528 return other * self.unit 529 530 def __rmul__(self, other): 531 """Right-multiply *other* by this quantity's unit. 532 533 Allows ``value * props`` expressions that return an astropy 534 :class:`~astropy.units.Quantity`. 535 536 Parameters 537 ---------- 538 other : numeric or u.Quantity 539 The value to multiply by :attr:`unit`. 540 541 Returns 542 ------- 543 out : u.Quantity 544 ``other * self.unit``. 545 546 Examples 547 -------- 548 >>> from psi_io._models import _MAS_QUANTITY_PROPS_MAPPING 549 >>> props = _MAS_QUANTITY_PROPS_MAPPING['vr'] 550 >>> (1.0 * props).unit # doctest: +ELLIPSIS 551 Unit(...) 552 """ 553 return other * self.unit 554 555 def __rtruediv__(self, other): 556 """Divide *other* by this quantity's unit. 557 558 Allows ``value / props`` expressions that return an astropy 559 :class:`~astropy.units.Quantity`. 560 561 Parameters 562 ---------- 563 other : numeric or u.Quantity 564 The numerator; divided by :attr:`unit`. 565 566 Returns 567 ------- 568 out : u.Quantity 569 ``other / self.unit``. 570 571 Examples 572 -------- 573 >>> from psi_io._models import _MAS_QUANTITY_PROPS_MAPPING 574 >>> props = _MAS_QUANTITY_PROPS_MAPPING['br'] 575 >>> (1.0 / props).unit # doctest: +ELLIPSIS 576 Unit(...) 577 """ 578 return other / self.unit
579 580 581# ---------------------------------------------------------------- 582# MAS Model 583# ---------------------------------------------------------------- 584 585 586MasQuantities = Literal[ 587 'vr', 'vt', 'vp', 'br', 'bt', 'bp', 'jr', 'jt', 'jp', 588 't', 'te', 'tp', 'rho', 'p', 'ep', 'em', 'zp', 'zm', 'heat' 589] 590"""Literal type alias for the 19 named MAS output quantities. 591 592Each string is the canonical lower-case identifier used as a key in 593:data:`_MAS_QUANTITY_PROPS_MAPPING` and as the filename prefix (e.g. ``br001001.h5``). 594 595Velocity components 596 ``'vr'``, ``'vt'``, ``'vp'`` — radial, co-latitude, and longitude velocity. 597 598Magnetic field components 599 ``'br'``, ``'bt'``, ``'bp'`` — radial, co-latitude, and longitude magnetic field. 600 601Current density components 602 ``'jr'``, ``'jt'``, ``'jp'`` — radial, co-latitude, and longitude current density. 603 604Temperature 605 ``'t'`` — single-fluid temperature; ``'te'`` — electron temperature; 606 ``'tp'`` — proton (ion) temperature (two-temperature model). 607 608Density and pressure 609 ``'rho'`` — mass density; ``'p'`` — plasma pressure. 610 611Alfvén wave quantities 612 ``'ep'``, ``'em'`` — wave energy density for forward- and backward-propagating 613 Alfvén waves (proportional to :math:`|z^+|^2` and :math:`|z^-|^2`); 614 ``'zp'``, ``'zm'`` — Elsässer variable amplitudes 615 :math:`z^\\pm = v \\pm v_A` for forward/backward propagating waves. 616 617Heating 618 ``'heat'`` — local coronal volumetric heating rate. 619""" 620 621_MAS_QUANTITY_PROPS_MAPPING = MappingProxyType({ 622 'vr': Props('vr', 'MAS Velocity (Radial Component)', MAS_v, 3, False, 0b011), 623 'vt': Props('vt', 'MAS Velocity (Theta Component)', MAS_v, 3, False, 0b101), 624 'vp': Props('vp', 'MAS Velocity (Phi Component)', MAS_v, 3, False, 0b110), 625 'br': Props('br', 'MAS Magnetic Field (Radial Component)', MAS_b, 3, False, 0b100), 626 'bt': Props('bt', 'MAS Magnetic Field (Theta Component)', MAS_b, 3, False, 0b010), 627 'bp': Props('bp', 'MAS Magnetic Field (Phi Component)', MAS_b, 3, False, 0b001), 628 'jr': Props('jr', 'MAS Current Density (Radial Component)', MAS_j, 3, False, 0b011), 629 'jt': Props('jt', 'MAS Current Density (Theta Component)', MAS_j, 3, False, 0b101), 630 'jp': Props('jp', 'MAS Current Density (Phi Component)', MAS_j, 3, False, 0b110), 631 't': Props('t', 'MAS Temperature', MAS_t, 3, True, 0b111), 632 'te': Props('te', 'MAS Electron Temperature', MAS_t, 3, True, 0b111), 633 'tp': Props('tp', 'MAS Proton Temperature', MAS_t, 3, True, 0b111), 634 'rho': Props('rho', 'MAS Density', MAS_n, 3, True, 0b111), 635 'p': Props('p', 'MAS Pressure', MAS_p, 3, True, 0b111), 636 'ep': Props('ep', 'MAS Wave Energy Density (Parallel to the Field)', MAS_p, 3, True, 0b111), 637 'em': Props('em', 'MAS Wave Energy Density (Anti-Parallel to the Field)', MAS_p, 3, True, 0b111), 638 'zp': Props('zp', 'MAS Outward Propagating Wave Amplitude', MAS_v, 3, True, 0b111), 639 'zm': Props('zm', 'MAS Inward Propagating Wave Amplitude', MAS_v, 3, True, 0b111), 640 'heat': Props('heat', 'MAS Local Coronal Heating Rate', MAS_heat, 3, True, 0b111), 641}) 642"""Read-only mapping from MAS quantity name to its :class:`Props` descriptor.""" 643
[docs] 644def get_mas_quantity_properties(variable: MasQuantities) -> Props: 645 """Return the :class:`~psi_io._models.Props` descriptor for a MAS quantity. 646 647 Parameters 648 ---------- 649 variable : MasQuantities 650 Case-insensitive MAS quantity name. Valid values: ``'br'``, ``'bt'``, ``'bp'``, 651 ``'vr'``, ``'vt'``, ``'vp'``, ``'jr'``, ``'jt'``, ``'jp'``, ``'t'``, ``'te'``, 652 ``'tp'``, ``'rho'``, ``'p'``, ``'ep'``, ``'em'``, ``'zp'``, ``'zm'``, ``'heat'``. 653 654 Returns 655 ------- 656 out : Props 657 Immutable metadata descriptor for the requested MAS quantity. 658 659 Raises 660 ------ 661 ValueError 662 If *variable* is not a recognized MAS quantity. 663 664 Examples 665 -------- 666 >>> from psi_io.mhd_io import get_mas_quantity_properties 667 >>> props = get_mas_quantity_properties('br') 668 >>> props.desc 669 'Magnetic Field (Radial Component)' 670 >>> get_mas_quantity_properties('BR').name # case-insensitive 671 'br' 672 """ 673 try: 674 return _MAS_QUANTITY_PROPS_MAPPING[variable.lower()] 675 except KeyError: 676 raise ValueError(f"Invalid variable '{variable}'. " 677 f"Valid options are: {', '.join(_MAS_QUANTITY_PROPS_MAPPING.keys())}") from None
678 679 680# ---------------------------------------------------------------- 681# POT3D Model 682# ---------------------------------------------------------------- 683 684 685Pot3dQuantities = Literal['br', 'bt', 'bp',] 686"""Literal type alias for the 3 POT3D magnetic field output quantities. 687 688POT3D is a potential-field solver that outputs only the three spherical components of 689the magnetic field: ``'br'`` (radial), ``'bt'`` (co-latitude), and ``'bp'`` 690(longitude). 691 692.. note:: 693 694 POT3D mesh codes are the bitwise complement of the corresponding MAS magnetic field 695 codes: ``POT3D_mesh = 0b111 ^ MAS_mesh``. Where MAS places each component on the 696 face through which it is the outward normal (face-centred), POT3D places the same 697 component on the opposite pair of edges (edge-centred). 698 699.. warning:: 700 701 POT3D output files carry **no intrinsic physical unit**. The values are stored as 702 dimensionless quantities (unit :data:`~psi_io._units.POT3D_b` = 1) whose physical 703 interpretation depends entirely on the unit of the photospheric boundary 704 magnetogram used to drive the simulation — typically Gauss, but this is not 705 guaranteed. Always supply the correct unit explicitly via the ``unit`` keyword 706 argument of :func:`~psi_io.mhd_io.PsiData`; calling ``read(unit='physical')`` 707 on a reader opened without a ``unit`` override will **not** perform a meaningful 708 conversion. 709""" 710_POT3D_QUANTITY_PROPS_MAPPING = MappingProxyType({ 711 'br': Props('br', 'POT3D Magnetic Field (Radial Component)', POT3D_b, 3, False, 0b011), 712 'bt': Props('bt', 'POT3D Magnetic Field (Theta Component)', POT3D_b, 3, False, 0b101), 713 'bp': Props('bp', 'POT3D Magnetic Field (Phi Component)', POT3D_b, 3, False, 0b110), 714}) 715"""Read-only mapping from POT3D quantity name to its :class:`Props` descriptor.""" 716 717
[docs] 718def get_pot3d_quantity_properties(variable: Pot3dQuantities) -> Props: 719 """Return the :class:`~psi_io._models.Props` descriptor for a POT3D quantity. 720 721 Parameters 722 ---------- 723 variable : Pot3dQuantities 724 Case-insensitive POT3D quantity name. Valid values: ``'br'``, ``'bt'``, 725 ``'bp'``. 726 727 Returns 728 ------- 729 out : Props 730 Immutable descriptor for the requested POT3D magnetic field component. 731 732 Raises 733 ------ 734 ValueError 735 If *variable* is not a recognized POT3D quantity. 736 737 Examples 738 -------- 739 >>> from psi_io.mhd_io import get_pot3d_quantity_properties 740 >>> get_pot3d_quantity_properties('bp').name 741 'bp' 742 """ 743 try: 744 return _POT3D_QUANTITY_PROPS_MAPPING[variable.lower()] 745 except KeyError: 746 raise ValueError(f"Invalid variable '{variable}'. " 747 f"Valid options are: {', '.join(_POT3D_QUANTITY_PROPS_MAPPING.keys())}") from None
748 749 750# ---------------------------------------------------------------- 751# PSI Coordinate Scales 752# ---------------------------------------------------------------- 753 754 755PsiScales = Literal['r', 't', 'p',] 756"""Literal type alias for the three PSI coordinate scale identifiers. 757 758``'r'`` 759 Radial coordinate in unit of solar radii (:data:`~psi_io._units.PSI_rsun`). 760``'t'`` 761 Co-latitude :math:`\\theta` in radians (:data:`~psi_io._units.PSI_angle`). 762``'p'`` 763 Longitude :math:`\\varphi` in radians (:data:`~psi_io._units.PSI_angle`). 764""" 765 766 767_PSI_SCALE_PROPS_MAPPING = MappingProxyType({ 768 'r': Props('r', 'PSI Radial Scale (Solar Radii)', PSI_rsun, 1, True), 769 't': Props('t', 'PSI Theta Scale (Co-Latitude)', PSI_angle, 1, True), 770 'p': Props('p', 'PSI Phi Scale (Longitude)', PSI_angle, 1, True), 771}) 772"""Read-only mapping from coordinate scale label to its :class:`Props` descriptor.""" 773 774
[docs] 775def get_psi_scale_properties(variable: PsiScales) -> Props: 776 """Return the :class:`~psi_io._models.Props` descriptor for a PSI coordinate scale. 777 778 Parameters 779 ---------- 780 variable : PsiScales 781 Coordinate label. The first character is used for lookup, so ``'r'``, 782 ``'radius'``, ``'t'``, ``'theta'``, ``'p'``, and ``'phi'`` are all accepted. 783 784 Returns 785 ------- 786 out : Props 787 Descriptor for the requested coordinate axis. 788 789 Raises 790 ------ 791 ValueError 792 If the first character of *variable* is not ``'r'``, ``'t'``, or ``'p'``. 793 794 Examples 795 -------- 796 >>> from psi_io import get_psi_scale_properties 797 >>> get_psi_scale_properties('r').desc 798 'Radial Scale (Solar Radii)' 799 >>> get_psi_scale_properties('theta').name # uses first character only 800 't' 801 """ 802 try: 803 return _PSI_SCALE_PROPS_MAPPING[variable.lower()[0]] 804 except KeyError: 805 raise ValueError(f"Invalid variable '{variable}'. " 806 f"Valid options are: {', '.join(_PSI_SCALE_PROPS_MAPPING.keys())}") from None
807 808 809ModelType = Literal['mas', 'pot3d'] 810"""Literal type alias for the two recognized PSI model types. 811 812``'mas'`` 813 MAS (Magnetohydrodynamic Algorithm outside a Sphere) plasma model output. 814``'pot3d'`` 815 POT3D potential-field source-surface (PFSS) magnetic field output. 816""" 817 818 819MODEL_TYPE = {'mas', 'pot3d'} 820"""Set of recognized PSI model type strings (``'mas'``, ``'pot3d'``).""" 821 822_PROP_GETTER_MAPPING = MappingProxyType({ 823 'mas': get_mas_quantity_properties, 824 'pot3d': get_pot3d_quantity_properties, 825 'scale': get_psi_scale_properties,}) 826"""Read-only mapping from model/scale label to its :class:`Props` getter function.""" 827 828
[docs] 829def get_model_prop_caller(model: ModelType) -> Callable: 830 """Return the :class:`Props` getter function for the given model type. 831 832 Parameters 833 ---------- 834 model : ModelType 835 Case-insensitive model label. Valid values: ``'mas'``, ``'pot3d'``, 836 ``'scale'``. 837 838 Returns 839 ------- 840 out : Callable 841 The getter function associated with *model* — one of 842 :func:`get_mas_quantity_properties`, :func:`get_pot3d_quantity_properties`, 843 or :func:`get_psi_scale_properties`. 844 845 Raises 846 ------ 847 ValueError 848 If *model* is not a recognized model label. 849 850 Examples 851 -------- 852 >>> from psi_io._models import get_model_prop_caller 853 >>> caller = get_model_prop_caller('mas') 854 >>> caller('br').name 855 'br' 856 """ 857 try: 858 return _PROP_GETTER_MAPPING[model.lower()] 859 except KeyError: 860 raise ValueError(f"Invalid model '{model}'. " 861 f"Valid options are: {', '.join(_PROP_GETTER_MAPPING.keys())}") from None
862 863 864MATCH_QUANTITIES = '|'.join(re.escape(q) for q in sorted(_MAS_QUANTITY_PROPS_MAPPING.keys(), key=len, reverse=True)) 865"""Regex alternation string matching any valid MAS quantity name (case-insensitive). 866 867Quantities are sorted longest-first to avoid partial matches (e.g. ``'heat'`` must be 868tried before ``'h'``). Used in :data:`FILEPATH_SCHEMA` and 869:func:`extract_quantity_from_filepath`. 870""" 871 872FILEPATH_SCHEMA = rf'^({MATCH_QUANTITIES})(\d{{3}}(?:\d{{3}})?)$' 873"""Regex pattern for the strict MAS filename schema ``<quantity><sequence>``. 874 875The stem (filename without extension) must consist of exactly one recognized MAS 876quantity name followed by a 3- or 6-digit decimal sequence number. 877 878Groups: 879 8801. Quantity name (e.g. ``'br'``, ``'heat'``). 8812. Sequence number (e.g. ``'001'``, ``'001001'``). 882 883Used by :func:`parse_psi_filename_schema` 884 885See Also 886-------- 887extract_quantity_from_filepath : 888 A lenient variant that does not require the sequence suffix. 889""" 890 891
[docs] 892def extract_quantity_from_filepath(ifile: Path, default: Optional[str] = None) -> str | None: 893 """Extract the MAS/POT3D quantity name from a filename stem. 894 895 Matches the longest recognized quantity prefix at the start of the stem (before 896 any digit or end-of-string). The match is case-insensitive. 897 898 Parameters 899 ---------- 900 ifile : Path 901 File path whose stem is examined. Only the stem (filename without 902 extension) is inspected. 903 default : str or None, optional 904 Value to return when no quantity prefix is found. Defaults to ``None``. 905 906 Returns 907 ------- 908 out : str or None 909 Lower-case quantity name (e.g. ``'br'``), or *default* if the stem does not 910 begin with a recognized quantity prefix. 911 912 Examples 913 -------- 914 >>> from psi_io._models import extract_quantity_from_filepath 915 >>> from pathlib import Path 916 >>> extract_quantity_from_filepath(Path('br001001.h5')) 917 'br' 918 >>> extract_quantity_from_filepath(Path('heat001.h5')) 919 'heat' 920 >>> extract_quantity_from_filepath(Path('unknown.h5')) is None 921 True 922 >>> extract_quantity_from_filepath(Path('unknown.h5'), default='br') 923 'br' 924 """ 925 match = re.match(rf'^({MATCH_QUANTITIES})(?=[^a-zA-Z]|$)', ifile.stem, re.IGNORECASE) 926 return match.group(1).lower() if match else default
927 928
[docs] 929def extract_sequence_from_filepath(ifile: Path, default: Optional[int] = None) -> int | None: 930 """Extract the sequence number from a filename stem. 931 932 Searches for the first occurrence of a 3- or 6-digit decimal run in the stem. 933 The match is not anchored to a particular position so it works for both the 934 strict MAS schema (``br001001.h5``) and looser naming conventions. 935 936 Parameters 937 ---------- 938 ifile : Path 939 File path whose stem is examined. 940 default : int or None, optional 941 Value to return when no 3- or 6-digit run is found. Defaults to ``None``. 942 943 Returns 944 ------- 945 out : int or None 946 Integer sequence number, or *default* if no match is found. 947 948 Examples 949 -------- 950 >>> from pathlib import Path 951 >>> from psi_io.mhd_io import extract_sequence_from_filepath 952 >>> extract_sequence_from_filepath(Path('br001001.h5')) 953 1001 954 >>> extract_sequence_from_filepath(Path('vr001.h5')) 955 1 956 >>> extract_sequence_from_filepath(Path('nosequence.h5')) is None 957 True 958 """ 959 match = re.search(r'\d{3}(?:\d{3})?', ifile.stem) 960 return int(match.group()) if match else default
961 962
[docs] 963def parse_psi_filename_schema(ifile: Path): 964 """Parse a PSI HDF filename that follows the strict ``<quantity><sequence>`` schema. 965 966 The filename stem must consist of exactly one recognized MAS/POT3D quantity name 967 followed immediately by a 3- or 6-digit sequence number, with no other characters. 968 The match is case-insensitive. 969 970 Parameters 971 ---------- 972 ifile : Path 973 File path to parse. The stem is matched against :data:`FILEPATH_SCHEMA`. 974 975 Returns 976 ------- 977 quantity : str 978 Lower-case quantity name (e.g. ``'br'``). 979 sequence : int 980 Integer sequence number (e.g. ``1001``). 981 982 Raises 983 ------ 984 ValueError 985 If the filename stem does not match the expected schema. 986 987 Examples 988 -------- 989 >>> from pathlib import Path 990 >>> from psi_io._models import parse_psi_filename_schema 991 >>> parse_psi_filename_schema(Path('br001001.h5')) 992 ('br', 1001) 993 >>> parse_psi_filename_schema(Path('heat001.hdf')) 994 ('heat', 1) 995 >>> parse_psi_filename_schema(Path('notvalid.h5')) 996 Traceback (most recent call last): 997 ... 998 ValueError: Filename 'notvalid.h5' does not match expected MAS filename schema: ... 999 """ 1000 matches = re.match(FILEPATH_SCHEMA, ifile.stem, re.IGNORECASE) 1001 if not matches: 1002 raise ValueError(f"Filename '{ifile.name}' does not match expected MAS filename schema: " 1003 f"'<quantity><sequence>'. Valid quantities are: {', '.join(_MAS_QUANTITY_PROPS_MAPPING.keys())}. " 1004 f"Sequence should be a 3 or 6 digit number.") 1005 quantity, sequence = matches.groups() 1006 return quantity, int(sequence)