# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Handles the "VOUnit" unit format.
"""
import copy
import keyword
import operator
import re
import warnings
from . import core, generic, utils
[docs]class VOUnit(generic.Generic):
"""
The IVOA standard for units used by the VO.
This is an implementation of `Units in the VO 1.0
<http://www.ivoa.net/documents/VOUnits/>`_.
"""
_explicit_custom_unit_regex = re.compile(r"^[YZEPTGMkhdcmunpfazy]?'((?!\d)\w)+'$")
_custom_unit_regex = re.compile(r"^((?!\d)\w)+$")
_custom_units = {}
@staticmethod
def _generate_unit_names():
from astropy import units as u
from astropy.units import required_by_vounit as uvo
names = {}
deprecated_names = set()
bases = [
"A", "C", "D", "F", "G", "H", "Hz", "J", "Jy", "K", "N",
"Ohm", "Pa", "R", "Ry", "S", "T", "V", "W", "Wb", "a",
"adu", "arcmin", "arcsec", "barn", "beam", "bin", "cd",
"chan", "count", "ct", "d", "deg", "eV", "erg", "g", "h",
"lm", "lx", "lyr", "m", "mag", "min", "mol", "pc", "ph",
"photon", "pix", "pixel", "rad", "rad", "s", "solLum",
"solMass", "solRad", "sr", "u", "voxel", "yr",
] # fmt: skip
binary_bases = ["bit", "byte", "B"]
simple_units = ["Angstrom", "angstrom", "AU", "au", "Ba", "dB", "mas"]
si_prefixes = [
"y", "z", "a", "f", "p", "n", "u", "m", "c", "d",
"", "da", "h", "k", "M", "G", "T", "P", "E", "Z", "Y"
] # fmt: skip
binary_prefixes = ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei"]
deprecated_units = {
"a", "angstrom", "Angstrom", "au", "Ba", "barn", "ct",
"erg", "G", "ph", "pix",
} # fmt: skip
def do_defines(bases, prefixes, skips=[]):
for base in bases:
for prefix in prefixes:
key = prefix + base
if key in skips:
continue
if keyword.iskeyword(key):
continue
names[key] = getattr(u if hasattr(u, key) else uvo, key)
if base in deprecated_units:
deprecated_names.add(key)
do_defines(bases, si_prefixes, ["pct", "pcount", "yd"])
do_defines(binary_bases, si_prefixes + binary_prefixes, ["dB", "dbyte"])
do_defines(simple_units, [""])
return names, deprecated_names, []
[docs] @classmethod
def parse(cls, s, debug=False):
if s in ("unknown", "UNKNOWN"):
return None
if s == "":
return core.dimensionless_unscaled
# Check for excess solidi, but exclude fractional exponents (allowed)
if s.count("/") > 1 and s.count("/") - len(re.findall(r"\(\d+/\d+\)", s)) > 1:
raise core.UnitsError(
f"'{s}' contains multiple slashes, which is "
"disallowed by the VOUnit standard."
)
result = cls._do_parse(s, debug=debug)
if hasattr(result, "function_unit"):
raise ValueError("Function units are not yet supported in VOUnit.")
return result
@classmethod
def _get_unit(cls, t):
try:
return super()._get_unit(t)
except ValueError:
if cls._explicit_custom_unit_regex.match(t.value):
return cls._def_custom_unit(t.value)
if cls._custom_unit_regex.match(t.value):
warnings.warn(
f"Unit {t.value!r} not supported by the VOUnit standard. "
+ utils.did_you_mean_units(
t.value,
cls._units,
cls._deprecated_units,
cls._to_decomposed_alternative,
),
core.UnitsWarning,
)
return cls._def_custom_unit(t.value)
raise
@classmethod
def _parse_unit(cls, unit, detailed_exception=True):
if unit not in cls._units:
raise ValueError()
if unit in cls._deprecated_units:
utils.unit_deprecation_warning(
unit, cls._units[unit], "VOUnit", cls._to_decomposed_alternative
)
return cls._units[unit]
@classmethod
def _get_unit_name(cls, unit):
# The da- and d- prefixes are discouraged. This has the
# effect of adding a scale to value in the result.
if isinstance(unit, core.PrefixUnit):
if unit._represents.scale == 10.0:
raise ValueError(
f"In '{unit}': VOUnit can not represent units with the 'da' "
"(deka) prefix"
)
elif unit._represents.scale == 0.1:
raise ValueError(
f"In '{unit}': VOUnit can not represent units with the 'd' "
"(deci) prefix"
)
name = unit.get_format_name("vounit")
if unit in cls._custom_units.values():
return name
if name not in cls._units:
raise ValueError(f"Unit {name!r} is not part of the VOUnit standard")
if name in cls._deprecated_units:
utils.unit_deprecation_warning(
name, unit, "VOUnit", cls._to_decomposed_alternative
)
return name
@classmethod
def _def_custom_unit(cls, unit):
def def_base(name):
if name in cls._custom_units:
return cls._custom_units[name]
if name.startswith("'"):
return core.def_unit(
[name[1:-1], name],
format={"vounit": name},
namespace=cls._custom_units,
)
else:
return core.def_unit(name, namespace=cls._custom_units)
if unit in cls._custom_units:
return cls._custom_units[unit]
for short, full, factor in core.si_prefixes:
for prefix in short:
if unit.startswith(prefix):
base_name = unit[len(prefix) :]
base_unit = def_base(base_name)
return core.PrefixUnit(
[prefix + x for x in base_unit.names],
core.CompositeUnit(
factor, [base_unit], [1], _error_check=False
),
format={"vounit": prefix + base_unit.names[-1]},
namespace=cls._custom_units,
)
return def_base(unit)
@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 or "." in power:
out.append(f"{cls._get_unit_name(base)}({power})")
else:
out.append(f"{cls._get_unit_name(base)}**{power}")
return ".".join(out)
[docs] @classmethod
def to_string(cls, unit):
from astropy.units 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):
if unit.physical_type == "dimensionless" and unit.scale != 1:
raise core.UnitScaleError(
"The VOUnit format is not able to "
"represent scale for dimensionless units. "
f"Multiply your data by {unit.scale:e}."
)
s = ""
if unit.scale != 1:
s += f"{unit.scale:.8g}"
pairs = list(zip(unit.bases, unit.powers))
pairs.sort(key=operator.itemgetter(1), reverse=True)
s += cls._format_unit_list(pairs)
elif isinstance(unit, core.NamedUnit):
s = cls._get_unit_name(unit)
return s
@classmethod
def _to_decomposed_alternative(cls, unit):
from astropy.units import core
try:
s = cls.to_string(unit)
except core.UnitScaleError:
scale = unit.scale
unit = copy.copy(unit)
unit._scale = 1.0
return f"{cls.to_string(unit)} (with data multiplied by {scale})"
return s