Source code for psi_io.models

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