Source code for astropy.units.format.ogip

# Licensed under a 3-clause BSD style license - see LICNSE.rst

# This module includes files automatically generated from ply (these end in
# _lextab.py and _parsetab.py). To generate these files, remove them from this
# folder, then build astropy and run the tests in-place:
#
#   python setup.py build_ext --inplace
#   pytest astropy/units
#
# You can then commit the changes to the re-generated _lextab.py and
# _parsetab.py files.

"""
Handles units in `Office of Guest Investigator Programs (OGIP)
FITS files
<https://heasarc.gsfc.nasa.gov/docs/heasarc/ofwg/docs/general/ogip_93_001/>`__.
"""

import math
import warnings
from fractions import Fraction
from typing import ClassVar, Literal

import numpy as np

from ply.lex import Lexer
from astropy.units.core import CompositeUnit, UnitBase
from astropy.units.enums import DeprecatedUnitAction
from astropy.units.errors import UnitParserWarning, UnitsWarning
from astropy.units.typing import UnitScale
from astropy.utils import classproperty, parsing
from astropy.utils.parsing import ThreadSafeParser

from .base import Base, _ParsingFormatMixin


[docs] class OGIP(Base, _ParsingFormatMixin): """ Support the units in `Office of Guest Investigator Programs (OGIP) FITS files <https://heasarc.gsfc.nasa.gov/docs/heasarc/ofwg/docs/general/ogip_93_001/>`__. """ _tokens: ClassVar[tuple[str, ...]] = ( "DIVISION", "OPEN_PAREN", "CLOSE_PAREN", "WHITESPACE", "POWER", "STAR", "FLOAT", "LIT10", "INT", "UNKNOWN", "FUNCNAME", "UNIT", ) _deprecated_units: ClassVar[frozenset[str]] = frozenset(("Crab", "mCrab")) @classproperty(lazy=True) def _units(cls) -> dict[str, UnitBase]: from astropy import units as u names = {"as": u.attosecond} for non_prefixed_unit in [ "angstrom", "arcmin", "arcsec", "AU", "barn", "bin", "byte", "chan", "count", "d", "deg", "erg", "G", "h", "lyr", "mag", "min", "photon", "pixel", "voxel", "yr", ]: # fmt: skip names[non_prefixed_unit] = getattr(u, non_prefixed_unit) bases = [ "A", "C", "cd", "eV", "F", "g", "H", "Hz", "J", "Jy", "K", "lm", "lx", "m", "mol", "N", "ohm", "Pa", "pc", "rad", "s", "S", "sr", "T", "V", "W", "Wb", ] # fmt: skip prefixes = [ "y", "z", "a", "f", "p", "n", "u", "m", "c", "d", "", "da", "h", "k", "M", "G", "T", "P", "E", "Z", "Y", ] # fmt: skip for name in (prefix + base for base in bases for prefix in prefixes): if name not in names: names[name] = getattr(u, name) # Create a separate, disconnected unit for the special case of # Crab and mCrab, since OGIP doesn't define their quantities. names["Crab"] = u.def_unit(["Crab"], prefixes=False, doc="Crab (X-ray flux)") names["mCrab"] = u.Unit(10**-3 * names["Crab"]) return names @classproperty(lazy=True) def _lexer(cls) -> Lexer: tokens = cls._tokens t_DIVISION = "[ \t]*/[ \t]*" t_OPEN_PAREN = r"\(" t_CLOSE_PAREN = r"\)" t_WHITESPACE = "[ \t]+" t_POWER = r"\*\*" t_STAR = r"\*" # NOTE THE ORDERING OF THESE RULES IS IMPORTANT!! # Regular expression rules for simple tokens def t_FLOAT(t): r"[+-]?((((\d+\.?\d*)|(\.\d+))([eE][+-]?\d+))|(((\d+\.\d*)|(\.\d+))([eE][+-]?\d+)?))" t.value = float(t.value) return t def t_INT(t): r"[+-]?\d+" t.value = int(t.value) return t def t_LIT10(t): r"10" return 10 def t_UNKNOWN(t): r"[Uu][Nn][Kk][Nn][Oo][Ww][Nn]" return None def t_FUNCNAME(t): r"((sqrt)|(ln)|(exp)|(log)|(sin)|(cos)|(tan)|(asin)|(acos)|(atan)|(sinh)|(cosh)|(tanh))(?=\ *\()" return t def t_UNIT(t): r"[a-zA-Z][a-zA-Z_]*" t.value = cls._get_unit(t) return t # Don't ignore whitespace t_ignore = "" # Error handling rule def t_error(t): raise ValueError(f"Invalid character at col {t.lexpos}") return parsing.lex(lextab="ogip_lextab", package="astropy/units") @classproperty(lazy=True) def _parser(cls) -> ThreadSafeParser: """ The grammar here is based on the description in the `Specification of Physical Units within OGIP FITS files <https://heasarc.gsfc.nasa.gov/docs/heasarc/ofwg/docs/general/ogip_93_001/>`__, which is not terribly precise. The exact grammar is here is based on the YACC grammar in the `unity library <https://bitbucket.org/nxg/unity/>`_. """ tokens = cls._tokens def p_main(p): """ main : UNKNOWN | complete_expression | scale_factor complete_expression | scale_factor WHITESPACE complete_expression """ match p[1:]: case (factor, unit) | (factor, _, unit): p[0] = CompositeUnit(factor * unit.scale, unit.bases, unit.powers) case _: p[0] = p[1] def p_complete_expression(p): """ complete_expression : unit_expression | product_of_units | division_of_units """ # product_of_units is not in unit_expression for performance # division_of_units is separate to enforce the correct order of operations p[0] = p[1] def p_product_of_units(p): """ product_of_units : complete_expression product unit_expression """ p[0] = p[1] * p[3] def p_division_of_units(p): """ division_of_units : DIVISION unit_expression | complete_expression DIVISION unit_expression """ match p[1:]: case _, unit: p[0] = unit**-1 case num, _, denom: p[0] = num / denom def p_unit_expression(p): """ unit_expression : UNIT | function | UNIT POWER numeric_power | UNIT OPEN_PAREN complete_expression CLOSE_PAREN | OPEN_PAREN complete_expression CLOSE_PAREN | UNIT OPEN_PAREN complete_expression CLOSE_PAREN POWER numeric_power | OPEN_PAREN complete_expression CLOSE_PAREN POWER numeric_power """ bad_multiplication_message = ( "if '{0}{1}' was meant to be a multiplication, " "it should have been written as '{0} {1}'." ) match p[1:]: case factor, _, unit, _, _, power: warnings.warn( bad_multiplication_message.format(factor, f"({unit})**{power}"), UnitParserWarning, ) p[0] = factor * unit**power case (_, unit, _, _, power) | (unit, "**", power): p[0] = unit**power case left, _, right, _: warnings.warn( bad_multiplication_message.format(left, f"({right})"), UnitParserWarning, ) p[0] = left * right case _, unit, _: p[0] = unit case _: p[0] = p[1] def p_function(p): """ function : FUNCNAME OPEN_PAREN complete_expression CLOSE_PAREN | FUNCNAME OPEN_PAREN complete_expression CLOSE_PAREN POWER numeric_power """ match p[1:]: case "sqrt", _, unit, _: p[0] = unit**0.5 case "sqrt", _, unit, _, _, numeric_power: p[0] = unit ** (0.5 * numeric_power) case func, *_: raise ValueError( f"The function '{func}' is valid in OGIP, but not understood " "by astropy.units." ) def p_scale_factor(p): """ scale_factor : LIT10 POWER numeric_power | LIT10 | number | number POWER numeric_power """ if len(p) == 4: p[0] = 10 ** p[3] else: p[0] = p[1] # Can't use np.log10 here, because p[0] may be a Python long. if math.log10(p[0]) % 1.0 != 0.0: warnings.warn( f"'{p[0]}' scale should be a power of 10 in OGIP format", UnitsWarning, ) def p_product(p): """ product : WHITESPACE | STAR | WHITESPACE STAR | WHITESPACE STAR WHITESPACE | STAR WHITESPACE """ def p_numeric_power(p): """ numeric_power : number | OPEN_PAREN number CLOSE_PAREN | OPEN_PAREN INT DIVISION INT CLOSE_PAREN """ if len(p) == 6: p[0] = Fraction(int(p[2]), int(p[4])) elif len(p) == 4: p[0] = p[2] else: p[0] = p[1] if p[1] < 0: warnings.warn( UnitParserWarning( "negative exponents must be enclosed in parenthesis. " f"Expected '**({p[1]})' instead of '**{p[1]}'." ) ) def p_number(p): """ number : INT | FLOAT """ p[0] = p[1] def p_error(p): raise ValueError() return parsing.yacc(tabmodule="ogip_parsetab", package="astropy/units")
[docs] @classmethod def parse(cls, s: str, debug: bool = False) -> UnitBase: return cls._do_parse(s.strip(), debug)
@classmethod def _format_superscript(cls, number: str) -> str: return f"**({number})" if "/" in number else f"**{number}"
[docs] @classmethod def to_string( cls, unit: UnitBase, fraction: bool | Literal["inline", "multiline"] = "inline", deprecations: DeprecatedUnitAction = DeprecatedUnitAction.WARN, ) -> str: # Remove units that aren't known to the format unit = cls._decompose_to_known_units(unit, deprecations) if isinstance(unit, CompositeUnit): # Can't use np.log10 here, because p[0] may be a Python long. if math.log10(unit.scale) % 1.0 != 0.0: warnings.warn( f"'{unit.scale}' scale should be a power of 10 in OGIP format", UnitsWarning, ) return super().to_string(unit, fraction=fraction)
[docs] @classmethod def format_exponential_notation( cls, val: UnitScale | np.number, format_spec: str = "g" ) -> str: return format(val, format_spec)