astropy:docs

Source code for astropy.units.format.ogip

# -*- coding: utf-8 -*-
# Licensed under a 3-clause BSD style license - see LICNSE.rst

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

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

from ...extern import six

import keyword
import math
import os
import warnings

from . import generic
from . import utils
from ...utils.compat.fractions import Fraction


[docs]class OGIP(generic.Generic): """ Support the units in `Office of Guest Investigator Programs (OGIP) FITS files <http://heasarc.gsfc.nasa.gov/docs/heasarc/ofwg/docs/general/ogip_93_001/>`__. """ def __init__(self): # Build this on the class, so it only gets generated once. if not '_units' in OGIP.__dict__: OGIP._units, OGIP._deprecated_units, OGIP._functions = self._generate_unit_names() if '_parser' not in OGIP.__dict__: OGIP._parser, OGIP._lexer = self._make_parser() @staticmethod def _generate_unit_names(): from ... import units as u names = {} deprecated_names = set() 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' ] deprecated_bases = [] prefixes = [ 'y', 'z', 'a', 'f', 'p', 'n', 'u', 'm', 'c', 'd', '', 'da', 'h', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y' ] for base in bases + deprecated_bases: for prefix in prefixes: key = prefix + base if keyword.iskeyword(key): continue names[key] = getattr(u, key) for base in deprecated_bases: for prefix in prefixes: deprecated_names.add(prefix + base) simple_units = [ 'angstrom', 'arcmin', 'arcsec', 'AU', 'barn', 'bin', 'byte', 'chan', 'count', 'day', 'deg', 'erg', 'G', 'h', 'lyr', 'mag', 'min', 'photon', 'pixel', 'voxel', 'yr' ] for unit in simple_units: names[unit] = getattr(u, unit) # Create a separate, disconnected unit for the special case of # Crab and mCrab, since OGIP doesn't define their quantities. Crab = u.def_unit(['Crab'], prefixes=False, doc='Crab (X-ray flux)') mCrab = u.Unit(10 ** -3 * Crab) names['Crab'] = Crab names['mCrab'] = mCrab deprecated_units = ['Crab', 'mCrab'] for unit in deprecated_units: deprecated_names.add(unit) # Define the function names, so we can parse them, even though # we can't use any of them (other than sqrt) meaningfully for # now. functions = [ 'log', 'ln', 'exp', 'sqrt', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'sinh', 'cosh', 'tanh' ] for name in functions: names[name] = name return names, deprecated_names, functions @classmethod def _make_parser(cls): """ The grammar here is based on the description in the `Specification of Physical Units within OGIP FITS files <http://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/>`_. """ from ...extern.ply import lex, yacc tokens = ( 'DIVISION', 'OPEN_PAREN', 'CLOSE_PAREN', 'WHITESPACE', 'STARSTAR', 'STAR', 'SIGN', 'UFLOAT', 'LIT10', 'UINT', 'UNKNOWN', 'UNIT' ) t_DIVISION = r'/' t_OPEN_PAREN = r'\(' t_CLOSE_PAREN = r'\)' t_WHITESPACE = '[ \t]+' t_STARSTAR = r'\*\*' t_STAR = r'\*' # NOTE THE ORDERING OF THESE RULES IS IMPORTANT!! # Regular expression rules for simple tokens def t_UFLOAT(t): r'(((\d+\.?\d*)|(\.\d+))([eE][+-]?\d+))|(((\d+\.\d*)|(\.\d+))([eE][+-]?\d+)?)' t.value = float(t.value) return t def t_UINT(t): r'\d+' t.value = int(t.value) return t def t_SIGN(t): r'[+-](?=\d)' t.value = float(t.value + '1') return t def t_X(t): # multiplication for factor in front of unit r'[x×]' 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_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( "Invalid character at col {0}".format(t.lexpos)) try: from . import ogip_lextab lexer = lex.lex(optimize=True, lextab=ogip_lextab) except ImportError: lexer = lex.lex(optimize=True, lextab='ogip_lextab', outputdir=os.path.dirname(__file__)) def p_main(p): ''' main : UNKNOWN | complete_expression | scale_factor complete_expression | scale_factor WHITESPACE complete_expression ''' if len(p) == 4: p[0] = p[1] * p[3] elif len(p) == 3: p[0] = p[1] * p[2] else: p[0] = p[1] def p_complete_expression(p): ''' complete_expression : product_of_units ''' p[0] = p[1] def p_product_of_units(p): ''' product_of_units : unit_expression | division unit_expression | product_of_units product unit_expression | product_of_units division unit_expression ''' if len(p) == 4: if p[2] == 'DIVISION': p[0] = p[1] / p[3] else: p[0] = p[1] * p[3] elif len(p) == 3: p[0] = p[2] ** -1 else: p[0] = p[1] def p_unit_expression(p): ''' unit_expression : unit | 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 ''' if p[1] in cls._functions and p[1] != 'sqrt': raise ValueError( "The function '{0}' is valid in OGIP, but not understood " "by astropy.units.".format( p[1])) if len(p) == 7: if p[1] == 'sqrt': p[0] = p[1] * p[3] ** (0.5 * p[6]) else: p[0] = p[1] * p[3] ** p[6] elif len(p) == 6: p[0] = p[2] ** p[5] elif len(p) == 5: if p[1] == 'sqrt': p[0] = p[3] ** 0.5 else: p[0] = p[1] * p[3] elif len(p) == 4: p[0] = p[2] else: p[0] = p[1] def p_scale_factor(p): ''' scale_factor : LIT10 power numeric_power | LIT10 | signed_float | signed_float power numeric_power | signed_int 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: from ..core import UnitsWarning warnings.warn( "'{0}' scale should be a power of 10 in " "OGIP format".format(p[0]), UnitsWarning) def p_division(p): ''' division : DIVISION | WHITESPACE DIVISION | WHITESPACE DIVISION WHITESPACE | DIVISION WHITESPACE ''' p[0] = 'DIVISION' def p_product(p): ''' product : WHITESPACE | STAR | WHITESPACE STAR | WHITESPACE STAR WHITESPACE | STAR WHITESPACE ''' p[0] = 'PRODUCT' def p_power(p): ''' power : STARSTAR ''' p[0] = 'POWER' def p_unit(p): ''' unit : UNIT | UNIT power numeric_power ''' if len(p) == 4: p[0] = p[1] ** p[3] else: p[0] = p[1] def p_numeric_power(p): ''' numeric_power : UINT | signed_float | OPEN_PAREN signed_int CLOSE_PAREN | OPEN_PAREN signed_float CLOSE_PAREN | OPEN_PAREN signed_float division UINT 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] def p_sign(p): ''' sign : SIGN | ''' if len(p) == 2: p[0] = p[1] else: p[0] = 1.0 def p_signed_int(p): ''' signed_int : SIGN UINT ''' p[0] = p[1] * p[2] def p_signed_float(p): ''' signed_float : sign UINT | sign UFLOAT ''' p[0] = p[1] * p[2] def p_error(p): raise ValueError() try: from . import ogip_parsetab parser = yacc.yacc(debug=False, tabmodule=ogip_parsetab, write_tables=False) except ImportError: parser = yacc.yacc(debug=False, tabmodule='ogip_parsetab', outputdir=os.path.dirname(__file__)) return parser, lexer @classmethod def _get_unit(cls, t): try: return cls._parse_unit(t.value) except ValueError as e: raise ValueError( "At col {0}, '{1}': {2}".format( t.lexpos, t.value, six.text_type(e))) @classmethod def _validate_unit(cls, unit, detailed_exception=True): if unit not in cls._units: if detailed_exception: raise ValueError( "Unit '{0}' not supported by the OGIP " "standard. {1}".format( unit, utils.did_you_mean_units( unit, cls._units, cls._deprecated_units, cls._to_decomposed_alternative))) else: raise ValueError() if unit in cls._deprecated_units: utils.unit_deprecation_warning( unit, cls._units[unit], 'OGIP', cls._to_decomposed_alternative) @classmethod def _parse_unit(cls, unit, detailed_exception=True): cls._validate_unit(unit, detailed_exception=detailed_exception) return cls._units[unit]
[docs] def parse(self, s, debug=False): s = s.strip() try: # This is a short circuit for the case where the string is # just a single unit name return self._parse_unit(s, detailed_exception=False) except ValueError: from ..core import Unit try: return Unit( self._parser.parse(s, lexer=self._lexer, debug=debug)) except ValueError as e: if six.text_type(e): raise else: raise ValueError( "Syntax error parsing unit '{0}'".format(s))
@classmethod def _get_unit_name(cls, unit): name = unit.get_format_name('ogip') cls._validate_unit(name) return name @classmethod def _format_unit_list(cls, units): out = [] units.sort(key=lambda x: cls._get_unit_name(x[0]).lower()) for base, power in units: if power == 1: out.append(cls._get_unit_name(base)) else: power = utils.format_power(power) if '/' in power: out.append('{0}**({1})'.format( cls._get_unit_name(base), power)) else: out.append('{0}**{1}'.format( cls._get_unit_name(base), power)) return ' '.join(out) @classmethod
[docs] def to_string(cls, unit): from .. import core # Remove units that aren't known to the format unit = utils.decompose_to_known_units(unit, cls._get_unit_name) if isinstance(unit, core.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( "'{0}' scale should be a power of 10 in " "OGIP format".format( unit.scale), core.UnitsWarning) return generic._to_string(cls, unit)
@classmethod def _to_decomposed_alternative(cls, unit): from .. import core # Remove units that aren't known to the format unit = utils.decompose_to_known_units(unit, cls._get_unit_name) if isinstance(unit, core.CompositeUnit): # Can't use np.log10 here, because p[0] may be a Python long. if math.log10(unit.scale) % 1.0 != 0.0: scale = unit.scale unit = copy.copy(unit) unit._scale = 1.0 return '{0} (with data multiplied by {1})'.format( generic._to_string(cls, unit), scale) return generic._to_string(unit)

Page Contents