1r"""Physical constants and normalization factors for PSI MAS model output.
2
3The MAS (Magnetohydrodynamic Algorithm outside a Sphere) code solves the resistive
4MHD equations in *dimensionless* form. Every quantity stored in an HDF output file is
5a dimensionless ratio
6
7.. math::
8
9 q_\text{code} = \frac{q_\text{phys}}{q_0}
10
11where :math:`q_0` is the characteristic scale for that quantity. This module
12collects all reference scales so that code-unit values can be converted to physical
13(CGS or SI) units via :mod:`astropy.units`.
14
15Normalization Scheme
16--------------------
17MAS adopts a normalization anchored to two observable solar parameters and one
18reference number density:
19
20.. list-table::
21 :header-rows: 1
22
23 * - Symbol
24 - Quantity
25 - Value
26 * - :data:`FNORML` = :data:`RSUN`
27 - Characteristic length
28 - :math:`6.96 \times 10^{10}` cm
29 * - :data:`G0PHYS`
30 - Solar surface gravity
31 - :math:`2.74 \times 10^4` cm s⁻²
32 * - :data:`FN0PHYS`
33 - Reference number density
34 - :math:`10^8` cm⁻³
35
36From these three anchors, the characteristic **time** is fixed by requiring the
37dimensionless surface gravity :math:`G_0^\text{norm} = 0.823`:
38
39.. math::
40
41 T_0 = \sqrt{\frac{G_0^\text{norm}\, R_\odot}{g_0}} \approx 1446 \;\text{s}
42
43All other normalization factors follow from dimensional analysis:
44
45.. list-table::
46 :header-rows: 1
47
48 * - Symbol
49 - Quantity
50 - Formula
51 * - :data:`FN_V`
52 - Velocity
53 - :math:`V_0 = L_0 / T_0 \approx 481` km s⁻¹
54 * - :data:`FN_RHO`
55 - Mass density
56 - :math:`\rho_0 = m_p n_0`
57 * - :data:`FN_T`
58 - Temperature
59 - :math:`T_0^\text{temp} = m_p V_0^2 / k_B \approx 28` MK
60 * - :data:`FN_P`
61 - Pressure
62 - :math:`P_0 = \rho_0 V_0^2`
63 * - :data:`FN_B`
64 - Magnetic field
65 - :math:`B_0 = V_0 \sqrt{\mu_0 \rho_0} \approx 2.2` G
66 * - :data:`FN_J`
67 - Current density (SI)
68 - :math:`J_0 = B_0 / (\mu_0 L_0)` A m⁻²
69 * - :data:`FN_HEAT`
70 - Volumetric heating rate
71 - :math:`Q_0 = P_0 / T_0` erg cm⁻³ s⁻¹
72
73Electromagnetic Units
74---------------------
75MAS is formulated in Gaussian CGS, where Ampère's law reads
76:math:`\mathbf{J} = (c/4\pi)\,\nabla \times \mathbf{B}`. Because current density
77in Gaussian CGS (statampere cm⁻²) is unfamiliar to most users, this module provides
78both SI (:data:`FN_J`, :data:`FN_E`) and Gaussian CGS (:data:`FN_J_CGS`,
79:data:`FN_E_CGS`) normalizations for these quantities.
80
81Magnetic field is expressed in Gauss throughout, since that system is unambiguous and
82Gauss is the conventional unit for coronal magnetic field strengths.
83
84Custom Astropy Units
85--------------------
86Each PSI quantity has a dedicated :class:`~astropy.units.UnitBase` registered at
87module import time. These units carry the conversion factor from code units to
88physical units so that astropy can chain conversions automatically — for example,
89a quantity returned by :mod:`psi_io.mhd_io` in ``MAS_b`` units can be converted
90with a single ``.to(u.Gauss)`` call.
91
92Units are registered both under a *family name* (e.g. ``MAS_b``) and under every
93individual quantity name in that family (e.g. ``MAS_br``, ``MAS_bt``, ``MAS_bp``),
94so that the unit embedded in an :class:`~astropy.units.Quantity` always reflects
95the specific field component.
96
97.. list-table:: Registered PSI Custom Units
98 :header-rows: 1
99 :widths: 12 24 16 48
100
101 * - Unit object
102 - Physical equivalent
103 - Module constant
104 - Registered string names
105 * - :data:`MAS_b`
106 - :math:`\approx 2.2` G
107 - :data:`FN_B`
108 - ``MAS_b``, ``MAS_br``, ``MAS_bt``, ``MAS_bp``
109 * - :data:`MAS_v`
110 - :math:`\approx 481` km s⁻¹
111 - :data:`FN_V`
112 - ``MAS_v``, ``MAS_vr``, ``MAS_vt``, ``MAS_vp``, ``MAS_zp``, ``MAS_zm``
113 * - :data:`MAS_j`
114 - :math:`J_0` A m⁻²
115 - :data:`FN_J`
116 - ``MAS_j``, ``MAS_jr``, ``MAS_jt``, ``MAS_jp``
117 * - :data:`MAS_t`
118 - :math:`\approx 28` MK
119 - :data:`FN_T`
120 - ``MAS_t``, ``MAS_te``, ``MAS_tp``
121 * - :data:`MAS_n`
122 - :math:`10^8` cm⁻³
123 - :data:`FN_N`
124 - ``MAS_n``, ``MAS_rho``
125 * - :data:`MAS_p`
126 - :math:`P_0` erg cm⁻³
127 - :data:`FN_P`
128 - ``MAS_p``, ``MAS_ep``, ``MAS_em``
129 * - :data:`MAS_heat`
130 - :math:`Q_0` erg cm⁻³ s⁻¹
131 - :data:`FN_HEAT`
132 - ``MAS_heat``
133 * - :data:`POT3D_b`
134 - 1 (dimensionless)
135 - —
136 - ``POT3D_b``, ``POT3D_br``, ``POT3D_bt``, ``POT3D_bp``
137 * - :data:`PSI_rsun`
138 - :math:`6.96 \times 10^{10}` cm
139 - :data:`RSUN`
140 - ``PSI_rsun``, ``PSI_radius``, ``PSI_r``
141 * - :data:`PSI_angle`
142 - 1 rad
143 - —
144 - ``PSI_angle``, ``PSI_t``, ``PSI_p``, ``PSI_theta``, ``PSI_phi``
145
146See Also
147--------
148:mod:`astropy.units` :
149 The underlying unit system used for normalization and conversion.
150:mod:`psi_io._models` :
151 Maps each MAS quantity name to its :data:`FN_*` normalization.
152:mod:`psi_io.mhd_io` :
153 Lazy readers that apply these normalizations on property access.
154"""
155
156from __future__ import annotations
157
158__all__ = [
159 "FAVORED_UNITS",
160 "compose_mas_units",
161 "decompose_mas_units",
162 "get_helium_fractions",
163 "MAS_b",
164 "MAS_v",
165 "MAS_j",
166 "MAS_t",
167 "MAS_n",
168 "MAS_p",
169 "MAS_heat",
170 "POT3D_b",
171 "PSI_rsun",
172 "PSI_angle"
173]
174
175import astropy.units as u
176import numpy as np
177
178
179FAVORED_UNITS = (u.erg, u.cm, u.s, u.K, u.Gauss, u.g, u.rad)
180"""Tuple of CGS units preferred when composing or decomposing MAS quantities.
181
182Used by :func:`compose_mas_units` and :func:`decompose_mas_units` to express
183results in the most physically intuitive CGS basis: erg, cm, s, K, Gauss, g, rad.
184Gauss is included explicitly because :func:`astropy.units.compose` would otherwise
185resolve magnetic field to mixed electromagnetic CGS units.
186
187A tuple (not a set) is used so that Sphinx's autodoc renderer does not call
188``sorted()`` on the elements — astropy unit raise :exc:`~astropy.units.UnitConversionError`
189when compared across incompatible physical dimensions (e.g. ``g`` vs ``K``), and
190Sphinx 9+ does not catch that exception type.
191"""
192
193
[docs]
194def compose_mas_units(quantity: u.Quantity) -> u.Quantity:
195 """Express a quantity in the preferred MAS CGS units basis.
196
197 Calls :meth:`astropy.units.UnitBase.compose` with :data:`FAVORED_UNITS` as the
198 allowed unit set. The first (simplest) composed unit that astropy returns is used.
199
200 Parameters
201 ----------
202 quantity : u.Quantity
203 Input quantity in any unit system.
204
205 Returns
206 -------
207 out : u.Quantity
208 Equivalent quantity expressed in the MAS-preferred CGS units basis
209 (erg, cm, s, K, Gauss, g, rad).
210
211 See Also
212 --------
213 decompose_mas_units : Decomposes to CGS *base* unit rather than composing.
214
215 Examples
216 --------
217 >>> import astropy.units as u
218 >>> from psi_io._units import compose_mas_units, FN_P
219 >>> compose_mas_units(1.0 * u.Pa) # doctest: +ELLIPSIS
220 <Quantity ... erg / cm3>
221 """
222 return quantity.to(quantity.unit.compose(units=FAVORED_UNITS)[0])
223
224
[docs]
225def decompose_mas_units(quantity: u.Quantity) -> u.Quantity:
226 """Decompose a quantity into the preferred MAS CGS base unit.
227
228 Calls :meth:`astropy.units.Quantity.decompose` with :data:`FAVORED_UNITS` as the
229 allowed bases. Unlike :func:`compose_mas_units`, this always expands compound
230 unit (e.g. Joule → erg) without attempting to find a compact composed form.
231
232 Parameters
233 ----------
234 quantity : u.Quantity
235 Input quantity in any unit system.
236
237 Returns
238 -------
239 out : u.Quantity
240 Equivalent quantity with all units decomposed into the MAS-preferred CGS
241 bases.
242
243 See Also
244 --------
245 compose_mas_units : Composes to a simpler combined CGS units where possible.
246
247 Examples
248 --------
249 >>> import astropy.units as u
250 >>> from psi_io._units import decompose_mas_units
251 >>> decompose_mas_units(1.0 * u.J) # doctest: +ELLIPSIS
252 <Quantity 1.e+07 erg>
253 """
254 return quantity.decompose(bases=list(FAVORED_UNITS))
255
256
257# =============================================================================
258# Physical constants
259# =============================================================================
260
261PI = 3.1415926535897932
262"""High-precision value of π.
263
264Stored as a module-level float to avoid repeated calls to :func:`math.pi` or
265:data:`numpy.pi` in constant expressions evaluated at import time.
266"""
267
268RSUN = 6.96e10 * u.cm
269"""Solar radius in centimeters.
270
271The canonical value :math:`R_\\odot = 6.96 \\times 10^{10}` cm used throughout PSI
272models as the characteristic length scale. Also aliased as :data:`FN_LENGTH` and
273:data:`FNORML`.
274"""
275
276G0PHYS = 0.274e5 * u.cm / u.s**2
277"""Solar surface gravitational acceleration in CGS.
278
279:math:`g_0 = 2.74 \\times 10^4` cm s⁻², the standard solar surface gravity.
280Together with :data:`RSUN` and :data:`G0NORM`, this fixes the MAS time normalization
281:data:`FNORMT`.
282"""
283
284FN0PHYS = 1e8 / u.cm**3
285"""Reference number density of the coronal base in CGS.
286
287:math:`n_0 = 10^8` cm⁻³, a representative electron (or proton) number density at
288:math:`r = 1\\,R_\\odot` for a quiescent solar corona. Used together with
289:data:`FMP` to set the mass-density normalization :data:`FN_RHO`.
290"""
291
292FMP = 1.6726e-24 * u.g
293"""Proton mass in CGS.
294
295:math:`m_p = 1.6726 \\times 10^{-24}` g. The MAS density normalization assumes a
296fully ionized hydrogen plasma, so mass density is proportional to number density
297through :math:`\\rho_0 = m_p n_0`.
298"""
299
300BOLTZ = 1.3807e-16 * u.erg / u.K
301"""Boltzmann constant in CGS.
302
303:math:`k_B = 1.3807 \\times 10^{-16}` erg K⁻¹. Used to derive the temperature
304normalization :data:`FN_T` from the kinetic pressure relation.
305"""
306
307FKSPITZ = 9e-7 # erg cm^-1 s^-1 K^(-7/2)
308"""Spitzer parallel thermal conductivity prefactor in CGS.
309
310The Spitzer heat flux along the magnetic field is
311
312.. math::
313
314 \\mathbf{q} = -\\kappa_0 T^{5/2}\\, \\hat{\\mathbf{b}}\\hat{\\mathbf{b}} \\cdot \\nabla T
315
316where :math:`\\kappa_0 = 9 \\times 10^{-7}` erg cm⁻¹ s⁻¹ K⁻⁷/² is the Spitzer
317conductivity coefficient for a fully ionized hydrogen plasma. This constant is stored
318as a plain :class:`float` (not an astropy :class:`~astropy.units.Quantity`) because it
319is used directly in normalized code expressions; the associated physical unit are
320erg cm⁻¹ s⁻¹ K⁻⁷/².
321
322See :data:`FN_KAPPA` for the conductivity normalization that converts the dimensionless
323code conductivity back to physical unit.
324"""
325
326
327# =============================================================================
328# Convenience unit containers
329# =============================================================================
330
331FLUX_UNIT = u.erg / (u.cm**2 * u.s)
332"""Astropy unit for surface energy flux: erg cm⁻² s⁻¹."""
333
334VOLUMETRIC_RATE_UNIT = u.erg / (u.cm**3 * u.s)
335"""Astropy unit for volumetric energy deposition rate: erg cm⁻³ s⁻¹."""
336
337
338# =============================================================================
339# Normalization anchors
340# =============================================================================
341
342G0NORM = 0.823
343"""Dimensionless surface gravity in MAS code unit.
344
345MAS defines a dimensionless gravitational acceleration profile
346
347.. math::
348
349 g_\\text{code}(r) = \\frac{G_0^\\text{norm}}{r^2}
350
351so that the surface value :math:`g_\\text{code}(1) = G_0^\\text{norm} = 0.823`.
352The physical surface gravity is then :math:`g_0 = G_0^\\text{norm}\\,L_0/T_0^2`,
353which gives
354
355.. math::
356
357 T_0 = \\sqrt{\\frac{G_0^\\text{norm}\\,L_0}{g_0}} \\approx 1446\\;\\text{s}.
358
359The value 0.823 is a calibration choice made when deriving :data:`FNORMT`; it is not
360a universal solar constant.
361"""
362
363FNORML = RSUN
364"""Characteristic length scale for MAS: the solar radius.
365
366:math:`L_0 = R_\\odot = 6.96 \\times 10^{10}` cm. All spatial coordinates in MAS
367output are in unit of :math:`L_0`. Aliased as :data:`FN_LENGTH`.
368"""
369
370
371# =============================================================================
372# Derived normalization factors
373# =============================================================================
374
375FNORMT = np.sqrt(G0NORM * FNORML / G0PHYS)
376"""Characteristic time scale for MAS.
377
378Derived from the dimensionless surface gravity :data:`G0NORM`, the characteristic
379length :data:`FNORML`, and the physical surface gravity :data:`G0PHYS`:
380
381.. math::
382
383 T_0 = \\sqrt{\\frac{G_0^\\text{norm}\\,R_\\odot}{g_0}}
384 \\approx 1446\\;\\text{s} \\approx 24\\;\\text{min}.
385
386Aliased as :data:`FN_TIME`.
387"""
388
389FNORMM = FN0PHYS * FMP * np.power(FNORML, 3)
390"""Characteristic mass scale for MAS.
391
392:math:`M_0 = n_0\\,m_p\\,L_0^3 = \\rho_0\\,L_0^3`. Represents the total mass
393contained in a cube of side :math:`L_0 = R_\\odot` at the reference density.
394"""
395
396FN_N = FN0PHYS
397"""Number density normalization: :math:`n_0 = 10^8` cm⁻³.
398
399Code-unit number density 1 corresponds to :math:`10^8` particles cm⁻³.
400Note that MAS stores *mass* density (dimensionless :math:`\\rho`), related to the
401number density by :math:`\\rho_\\text{code} = (m_p / n_0) \\cdot n`. See
402:data:`FN_RHO` for the mass-density normalization.
403"""
404
405FN_RHO = FMP * FN0PHYS
406"""Mass density normalization for MAS.
407
408:math:`\\rho_0 = m_p\\,n_0 \\approx 1.67 \\times 10^{-16}` g cm⁻³, assuming a
409fully ionized hydrogen plasma where the mass per particle is the proton mass.
410"""
411
412FN_T = ((FMP * FNORML**2 / FNORMT**2).to(u.erg) / BOLTZ).to(u.MK)
413"""Temperature normalization for MAS.
414
415Derived from the condition that the ideal-gas pressure :math:`P = n\\,k_B\\,T` equals
416the dynamic pressure :math:`\\rho_0\\,V_0^2` at code-unit temperature 1:
417
418.. math::
419
420 T_0 = \\frac{m_p\\,V_0^2}{k_B} = \\frac{m_p\\,L_0^2}{k_B\\,T_0^2}
421 \\approx 28\\;\\text{MK}.
422
423Code-unit temperatures of order unity therefore correspond to tens of megakelvin,
424consistent with hot coronal plasma.
425"""
426
427FN_V = (FNORML / FNORMT).to(u.km / u.s)
428"""Velocity normalization for MAS.
429
430:math:`V_0 = L_0 / T_0 \\approx 481` km s⁻¹. This is comparable to the fast solar
431wind speed, making code-unit velocities of order unity physically natural for
432heliospheric modelling.
433"""
434
435# Magnetic field normalization requires a CGS–SI bridge.
436# MAS is formulated in Gaussian CGS but astropy's CGS electromagnetic unit are
437# incomplete. The SI μ₀ is used here as a conversion bridge: in Gaussian CGS the
438# Alfvénic normalization B₀ = V₀ √(4πρ₀) is algebraically equivalent to the SI
439# expression B₀ = V₀ √(μ₀ ρ₀) once the result is converted to Gauss.
440MU0 = 4 * PI * 1e-7 * u.N / u.A**2
441"""Permeability of free space (SI).
442
443:math:`\\mu_0 = 4\\pi \\times 10^{-7}` N A⁻², used here as an algebraic bridge to
444compute the magnetic field normalization :data:`FN_B` and current density
445normalization :data:`FN_J` in SI, which are then converted to the required CGS or
446SI output unit.
447
448In Gaussian CGS, the Alfvénic magnetic normalization is
449:math:`B_0 = V_0\\sqrt{4\\pi\\rho_0}`, which is numerically equivalent to the SI
450expression :math:`B_0 = V_0\\sqrt{\\mu_0\\rho_0}` when both results are expressed in
451Gauss.
452"""
453
454FN_B = (np.sqrt(MU0 * FN_RHO) * FNORML / FNORMT).to(u.Gauss)
455"""Magnetic field normalization for MAS, in Gauss.
456
457The code B field is normalized so that the Alfvén speed equals the reference velocity
458:data:`FN_V` when :math:`B_\\text{code} = 1`:
459
460.. math::
461
462 B_0 = V_0\\sqrt{\\mu_0\\,\\rho_0} \\approx 2.2\\;\\text{G}.
463
464This is on the order of typical large-scale coronal magnetic field strengths.
465"""
466
467FN_J = (FN_B.to(u.Tesla) / FNORML.to(u.m) / MU0).to(u.ampere / u.m**2)
468"""Current density normalization for MAS, in SI (A m⁻²).
469
470From Ampère's law :math:`\\mathbf{J} = \\mu_0^{-1}\\nabla\\times\\mathbf{B}` (SI):
471
472.. math::
473
474 J_0 = \\frac{B_0}{\\mu_0\\,L_0}.
475
476SI unit are used here because current density in Gaussian CGS (statampere cm⁻²) is
477rarely used in practice. See :data:`FN_J_CGS` for the Gaussian equivalent.
478"""
479
480FN_E = (FN_B.to(u.Tesla) * FN_V.to(u.m / u.s)).to(u.Volt / u.m)
481"""Electric field normalization for MAS, in SI (V m⁻¹).
482
483In ideal MHD the electric field is :math:`\\mathbf{E} = -\\mathbf{v}\\times\\mathbf{B}`,
484so the natural normalization is
485
486.. math::
487
488 E_0 = V_0\\,B_0.
489
490SI unit are used for the same reasons as :data:`FN_J`. See :data:`FN_E_CGS` for the
491Gaussian equivalent.
492"""
493
494# ---------------------------------------------------------------------------
495# Gaussian CGS conversions for J and E
496# ---------------------------------------------------------------------------
497C_CGS = 2.99792458e10
498"""Speed of light in CGS: :math:`c = 2.998 \\times 10^{10}` cm s⁻¹.
499
500Used to convert between SI and Gaussian electromagnetic unit.
501"""
502
503STATAMP_TO_AMPERE = 10 / C_CGS
504"""Conversion factor from statampere to ampere.
505
506In Gaussian CGS, charge is measured in statcoulombs (esu). One statampere
507(statcoulomb per second) equals :math:`10/c` amperes, where :math:`c` is the speed
508of light in cm s⁻¹:
509
510.. math::
511
512 1\\;\\text{statA} = \\frac{10}{c}\\;\\text{A} \\approx 3.336 \\times 10^{-10}\\;\\text{A}.
513"""
514
515FN_J_CGS = FN_J.value / (1e4 * STATAMP_TO_AMPERE) * u.statA / u.cm**2
516"""Current density normalization for MAS, in Gaussian CGS (statA cm⁻²).
517
518Equivalent to :data:`FN_J` expressed in Gaussian electromagnetic unit. The
519conversion from SI to Gaussian CGS is:
520
521.. math::
522
523 1\\;\\text{A m}^{-2} = \\frac{1}{10^4 \\times (10/c)}\\;\\text{statA cm}^{-2}
524
525where :math:`c` is :data:`C_CGS`.
526"""
527
528STATVOLT_TO_VOLT = C_CGS * 1e-8
529"""Conversion factor from statvolt to volt.
530
531One statvolt (erg per statcoulomb) equals :math:`c \\times 10^{-8}` volts, where
532:math:`c` is the speed of light in cm s⁻¹:
533
534.. math::
535
536 1\\;\\text{statV} = c \\times 10^{-8}\\;\\text{V} \\approx 299.8\\;\\text{V}.
537"""
538
539FN_E_CGS = FN_E.value / (1e2 * STATVOLT_TO_VOLT) * u.erg / u.statC / u.cm
540"""Electric field normalization for MAS, in Gaussian CGS (statV cm⁻¹).
541
542Equivalent to :data:`FN_E` expressed in Gaussian electromagnetic unit (statvolt per
543centimeter, where 1 statvolt = 1 erg / statcoulomb). The conversion from SI to
544Gaussian CGS is:
545
546.. math::
547
548 1\\;\\text{V m}^{-1} = \\frac{1}{10^2 \\times c \\times 10^{-8}}\\;\\text{statV cm}^{-1}
549
550where :math:`c` is :data:`C_CGS`.
551"""
552
553# ---------------------------------------------------------------------------
554# Thermodynamic and wave-energy normalizations
555# ---------------------------------------------------------------------------
556
557FN_P = compose_mas_units(FMP * FN0PHYS * FNORML**2 / FNORMT**2)
558"""Pressure normalization for MAS.
559
560The dynamic pressure :math:`P_0 = \\rho_0\\,V_0^2 = m_p\\,n_0\\,(L_0/T_0)^2`,
561expressed in the preferred MAS CGS basis (erg cm⁻³). Code-unit pressure 1 maps to
562this value.
563"""
564
565FN_QRAD = compose_mas_units(FMP * np.power(FNORML, 2) / (FN0PHYS * np.power(FNORMT, 3)))
566"""Radiative loss function normalization for MAS.
567
568The radiative loss per unit volume in the energy equation takes the form
569:math:`Q_\\text{rad} = n_e\\,n_p\\,\\Lambda(T)`, where :math:`\\Lambda(T)` is the
570optically thin loss function (erg cm³ s⁻¹). In MAS code units, the dimensionless
571loss function :math:`\\Lambda_\\text{code}` satisfies
572
573.. math::
574
575 \\Lambda_\\text{phys} = \\Lambda_\\text{code} \\times
576 \\frac{m_p\\,L_0^2}{n_0\\,T_0^3}.
577
578This normalization ensures the radiative source term enters the dimensionless energy
579equation with a coefficient of order unity for typical coronal conditions.
580"""
581
582FN_KAPPA = compose_mas_units(BOLTZ * FN0PHYS * np.power(FNORML, 2) / FNORMT)
583"""Thermal conductivity normalization for MAS.
584
585The Spitzer parallel conductivity :math:`\\kappa = \\kappa_0 T^{5/2}` (with
586:data:`FKSPITZ` = :math:`\\kappa_0`) has unit of erg cm⁻¹ s⁻¹ K⁻¹. In MAS code
587unit the dimensionless conductivity :math:`\\kappa_\\text{code}` satisfies
588
589.. math::
590
591 \\kappa_\\text{phys} = \\kappa_\\text{code} \\times
592 \\frac{k_B\\,n_0\\,L_0^2}{T_0}.
593
594The dimensionless Spitzer coefficient stored in the code is therefore
595
596.. math::
597
598 F_\\kappa^\\text{code} = \\kappa_0
599 \\frac{T_0^{5/2}}{k_B\\,n_0\\,L_0^2/T_0\\cdot T_0^{5/2}}
600 = \\frac{\\kappa_0\\,T_0^{5/2}}{F_\\kappa}.
601"""
602
603FN_FLUX = compose_mas_units(FN_P * FN_V).to(FLUX_UNIT)
604"""Surface energy flux normalization for MAS.
605
606:math:`F_0 = P_0\\,V_0` in erg cm⁻² s⁻¹. This scale governs the Poynting flux,
607enthalpy flux, and wave-energy flux that appear in the energy equation boundary
608conditions.
609"""
610
611FN_HEAT = compose_mas_units(FN_P / FNORMT).to(VOLUMETRIC_RATE_UNIT)
612"""Volumetric heating rate normalization for MAS.
613
614:math:`Q_0 = P_0 / T_0` in erg cm⁻³ s⁻¹. Code-unit heating rate 1 corresponds to
615the reference dynamic pressure dissipated over one characteristic time. This sets
616the scale for empirical coronal heating prescriptions and wave-turbulence heating in
617MAS.
618"""
619
620FN_JB = compose_mas_units(FN_P / FNORML)
621"""Lorentz force density (and pressure gradient) normalization for MAS.
622
623:math:`(\\mathbf{J}\\times\\mathbf{B})_0 = P_0 / L_0` in erg cm⁻⁴ (equivalent to
624dyne cm⁻³). Both the magnetic Lorentz force and the plasma pressure gradient appear
625in the MAS momentum equation with this normalization, ensuring they enter on equal
626footing.
627"""
628
629# ---------------------------------------------------------------------------
630# Total energy scales
631# ---------------------------------------------------------------------------
632
633W = FN_P * FNORML**3
634"""Total (integrated) energy normalization for MAS.
635
636:math:`W_0 = P_0\\,L_0^3` in erg. The total magnetic, kinetic, or thermal energy
637integrated over a domain of volume :math:`L_0^3` at the reference pressure. Used
638when computing globally integrated energy budgets.
639"""
640
641K = W
642"""Kinetic energy normalization for MAS.
643
644Alias for :data:`W`. The kinetic and pressure-volume energy scales are identical
645since :math:`K_0 = \\tfrac{1}{2}\\rho_0 V_0^2 L_0^3 = \\tfrac{1}{2} P_0 L_0^3`;
646the factor of one-half is absorbed into the code-unit kinetic energy.
647"""
648
649# ---------------------------------------------------------------------------
650# Aliases and corotating frame
651# ---------------------------------------------------------------------------
652
653FN_LENGTH = FNORML
654"""Alias for :data:`FNORML`: the characteristic length scale :math:`L_0 = R_\\odot`."""
655
656FN_TIME = FNORMT
657"""Alias for :data:`FNORMT`: the characteristic time scale :math:`T_0 \\approx 1446` s."""
658
659OMEGA_COROTATE = 0.004144 * u.rad / FN_TIME
660"""Default angular velocity of the MAS corotating frame, in physical unit.
661
662MAS can be run in a frame rotating with the Sun. The dimensionless code rotation
663rate 0.004144 rad per time unit converts to
664
665.. math::
666
667 \\Omega_\\text{corotate} = \\frac{0.004144}{T_0} \\approx 2.87 \\times 10^{-6}\\;\\text{rad s}^{-1},
668
669which matches the solar sidereal rotation rate (period ≈ 25.4 days).
670"""
671
672
673# =============================================================================
674# Helium abundance corrections
675# =============================================================================
676
[docs]
677def get_helium_fractions(he_frac: float) -> dict[str, float]:
678 """Compute fractional abundance multipliers for a hydrogen–helium plasma.
679
680 MAS models the solar corona as a gas of electrons (e), protons (p), and alpha
681 particles (a). Given the fractional helium abundance :math:`f = n_a / n_e`, this
682 function returns dimensionless multipliers that relate the MAS code-unit density
683 and pressure to the individual species number densities.
684
685 The multipliers are derived from two constraints:
686
687 1. **Charge neutrality**: :math:`n_e = n_p + 2 n_a`, giving
688 :math:`n_p/n_e = 1/(1+2f)` and :math:`n_a/n_e = f/(1+2f)`.
689 2. **Alpha particle properties**: mass :math:`4 m_p`, charge :math:`2e`.
690
691 Parameters
692 ----------
693 he_frac : float
694 Helium fraction :math:`f = n_a / n_e`, the ratio of alpha-particle number
695 density to electron number density. Typical coronal value: ``0.05``.
696
697 Returns
698 -------
699 fractions : dict[str, float]
700 Dictionary with the following keys:
701
702 ``'he_rho'``
703 Mass-density multiplier. The code mass density :math:`\\rho_\\text{code}`
704 is related to the electron number density by
705 :math:`\\rho_\\text{code} = n_e \\cdot` ``he_rho``, where
706
707 .. math::
708
709 \\texttt{he\\_rho} = \\frac{n_p + 4 n_a}{n_e} = \\frac{1 + 4f}{1 + 2f}.
710
711 ``'he_p'``
712 Total pressure multiplier for a single-temperature plasma. For an ideal
713 gas the total particle pressure is :math:`p = (n_e + n_p + n_a)\\,k_B T`,
714 so
715
716 .. math::
717
718 \\texttt{he\\_p} = \\frac{n_e + n_p + n_a}{n_e} = \\frac{2 + 3f}{1 + 2f}.
719
720 ``'he_np'``
721 Proton fraction :math:`n_p / n_e = 1/(1 + 2f)`. Needed for radiative
722 loss rates that scale as :math:`n_e n_p`.
723
724 ``'he_p_e'``
725 Electron pressure multiplier (equal to 1.0). In the two-temperature
726 model the electron partial pressure is :math:`p_e = n_e k_B T_e`, so
727 the multiplier relative to the electron density is always unity.
728
729 ``'he_p_p'``
730 Ion pressure multiplier. The combined proton + alpha pressure is
731 :math:`p_i = (n_p + n_a)\\,k_B T_i`, giving
732
733 .. math::
734
735 \\texttt{he\\_p\\_p} = \\frac{n_p + n_a}{n_e} = \\frac{1 + f}{1 + 2f}.
736
737 Notes
738 -----
739 These multipliers are *not* stored as module-level constants because they depend on
740 ``he_frac``, which varies between simulations. The caller is responsible for
741 passing the correct helium fraction from the model metadata.
742
743 Examples
744 --------
745 >>> from psi_io._units import get_helium_fractions
746 >>> fracs = get_helium_fractions(0.0) # pure hydrogen plasma
747 >>> fracs['he_rho']
748 1.0
749 >>> fracs['he_p']
750 2.0
751 >>> fracs = get_helium_fractions(0.05) # 5 % helium by electron fraction
752 >>> round(fracs['he_rho'], 4)
753 1.1818
754 """
755 # Mass Density Multiplier: A = (np + 4*na)/ne
756 # --> rho_mas = ne_mas*he_rho
757 he_rho = (1 + 4 * he_frac) / (1 + 2 * he_frac)
758
759 # Total number of particles: n = ne + np + na
760 # --> for 1T: p_mas = ne_mas*T_mas*he_p = rho_mas*T_mas*he_p/he_rho
761 he_p = (2 + 3 * he_frac) / (1 + 2 * he_frac)
762
763 # Number of protons (used by radloss, which needs ne*np)
764 he_np = 1 / (1 + 2 * he_frac)
765
766 # Electron pressure multiplier (used in 2T: Pe = ne*kB*Te → multiplier = 1)
767 he_p_e = 1.0
768
769 # Ion pressure multiplier (protons + alphas, used in 2T: Pi = (np+na)*kB*Ti)
770 he_p_p = (1 + he_frac) / (1 + 2 * he_frac)
771
772 return {
773 'he_rho': he_rho,
774 'he_p': he_p,
775 'he_np': he_np,
776 'he_p_e': he_p_e,
777 'he_p_p': he_p_p,
778 }
779
780
781# =============================================================================
782# Custom astropy unit definitions for MAS and POT3D code quantities
783# =============================================================================
784
785MAS_b = u.def_unit(
786 [f"MAS_{q}" for q in ("b", "br", "bt", "bp")],
787 FN_B,
788 doc="PSI's MAS magnetic field normalization unit.",
789 format={"latex": r"B_\mathrm{MAS}"},
790)
791"""Custom astropy unit for MAS magnetic field quantities.
792
793One ``MAS_b`` equals :data:`FN_B` ≈ 2.2 Gauss. Registered for the quantity names
794``MAS_b``, ``MAS_br``, ``MAS_bt``, and ``MAS_bp`` so that astropy can automatically
795convert code-unit field values to Gauss or Tesla.
796"""
797
798MAS_v = u.def_unit(
799 [f"MAS_{q}" for q in ("v", "vr", "vt", "vp", "zp", "zm")],
800 FN_V,
801 doc="PSI's MAS velocity normalization unit.",
802 format={"latex": r"v_\mathrm{MAS}"},
803)
804"""Custom astropy unit for MAS velocity quantities.
805
806One ``MAS_v`` equals :data:`FN_V` ≈ 481 km s⁻¹. Registered for velocity components
807(``vr``, ``vt``, ``vp``) and Elsässer wave variables (``zp``, ``zm``).
808"""
809
810MAS_j = u.def_unit(
811 [f"MAS_{q}" for q in ("j", "jr", "jt", "jp")],
812 FN_J,
813 doc="PSI's MAS current density normalization unit.",
814 format={"latex": r"J_\mathrm{MAS}"},
815)
816"""Custom astropy unit for MAS current density quantities.
817
818One ``MAS_j`` equals :data:`FN_J` in A m⁻² (SI). Registered for the vector
819components ``jr``, ``jt``, ``jp``.
820"""
821
822MAS_t = u.def_unit(
823 [f"MAS_{q}" for q in ("t", "te", "tp")],
824 FN_T,
825 doc="PSI's MAS temperature normalization unit.",
826 format={"latex": r"T_\mathrm{MAS}"},
827)
828"""Custom astropy unit for MAS temperature quantities.
829
830One ``MAS_t`` equals :data:`FN_T` ≈ 28 MK. Registered for the single-temperature
831(``t``), electron-temperature (``te``), and proton-temperature (``tp``) variables.
832"""
833
834MAS_n = u.def_unit(
835 [f"MAS_{q}" for q in ("n", "rho")],
836 FN_N,
837 doc="PSI's MAS (number) density normalization unit.",
838 format={"latex": r"n_\mathrm{MAS}"},
839)
840"""Custom astropy unit for MAS number density and mass density.
841
842One ``MAS_n`` equals :data:`FN_N` = 10⁸ cm⁻³. Registered for both the number
843density (``n``) and the code mass-density (``rho``) variables; callers should divide
844``rho`` by :data:`FMP` to obtain a physical number density in cm⁻³.
845"""
846
847MAS_p = u.def_unit(
848 [f"MAS_{q}" for q in ("p", "ep", "em")],
849 FN_P,
850 doc="PSI's MAS pressure normalization unit.",
851 format={"latex": r"p_\mathrm{MAS}"},
852)
853"""Custom astropy unit for MAS pressure and wave-energy quantities.
854
855One ``MAS_p`` equals :data:`FN_P` in erg cm⁻³. Registered for the thermal pressure
856(``p``), Elsässer energy densities (``ep``, ``em``), and Alfvén wave pressure
857variables.
858"""
859
860MAS_heat = u.def_unit(
861 [f"MAS_{q}" for q in ("heat",)],
862 FN_HEAT,
863 doc="PSI's MAS volumetric heating rate normalization unit.",
864 format={"latex": r"heat_\mathrm{MAS}"},
865)
866"""Custom astropy unit for MAS volumetric heating rate.
867
868One ``MAS_heat`` equals :data:`FN_HEAT` in erg cm⁻³ s⁻¹.
869"""
870
871POT3D_b = u.def_unit(
872 [f"POT3D_{q}" for q in ("b", "br", "bt", "bp")],
873 1 * u.dimensionless_unscaled,
874 doc="PSI's POT3D magnetic field normalization unit.",
875 format={"latex": r"B_\mathrm{POT3D}"},
876)
877"""Custom astropy unit for POT3D magnetic field quantities.
878
879One ``POT3D_b`` equals 1 (dimensionless). POT3D is a potential-field solver driven
880by photospheric magnetogram boundary conditions; its output magnetic field is already
881expressed in the same physical unit as the input map (typically Gauss), so no
882additional conversion factor is applied.
883"""
884
885PSI_rsun = u.def_unit(
886 [f"PSI_{q}" for q in ("rsun", "radius", "r")],
887 RSUN,
888 doc="PSI's solar radius normalization unit.",
889 format={"latex": r"R_\odot"},
890)
891"""Custom astropy unit for PSI radial coordinate grids.
892
893One ``PSI_rsun`` equals :data:`RSUN` = 6.96 × 10¹⁰ cm. Radial coordinate arrays
894in MAS and POT3D HDF files are stored in unit of solar radii; this unit allows
895astropy to convert them to cm, km, or AU automatically.
896"""
897
898PSI_angle = u.def_unit(
899 [f"PSI_{q}" for q in ("angle", "t", "p", "theta", "phi")],
900 1 * u.rad,
901 doc="PSI's long-lat angle unit.",
902 format={"latex": r"R_\odot"},
903)
904"""Custom astropy unit for PSI angular coordinate grids.
905
906One ``PSI_angle`` equals 1 radian. Colatitude (θ) and longitude (φ) coordinate
907arrays in MAS and POT3D HDF files are stored in radians; this unit allows astropy to
908convert them to degrees where needed.
909"""
910
911
912u.add_enabled_units([MAS_b, MAS_v, MAS_j, MAS_t, MAS_n, MAS_p, MAS_heat, POT3D_b, PSI_rsun, PSI_angle])