Quantities: Values and Units¶
The PVL specifications supports the notion that PVL Value Expressions can contain an optional PVL Units Expression that follows the PVL Value. This combination of information: a value followed by a unit can be represented by a single object that we might call a quantity.
There is no fundamental Python object type that represents a value
and the units of that value. However, libraries like astropy
and pint
have implemented “quantity” objects (and managed to
name them both Quantity, but they have slightly different interfaces).
In order to avoid optional dependencies, the pvl
library provides
the pvl.collections.Quantity
class, implemented as a
collections.namedtuple
with a value
and a unit
parameter. However, the unit
parameter is just a string and
so the pvl
quantity objects doesn’t have the super-powers that
the astropy
and pint
quntity objects do.
By default, this means that when PVL text is parsed by pvl.load()
or pvl.loads()
and when a PVL Value followed by a PVL Units
Expression is encountered, a pvl.collections.Quantity
object
will be placed in the returned dict-like.
Likewise when pvl.dump()
or pvl.dumps()
encounters a
pvl.collections.Quantity
its value and units will be serialized
with the right PVL syntax.
However, the pvl
library also supports the use of other quantity
objects.
Getting other quantity objects from PVL text¶
In order to get the parsing side of the pvl
library to return
a particular kind of quantity object when a PVL Value followed by
a PVL Units Expression is found, you must pass the name of that
quantity class to the decoder’s quantity_cls
argument. This
quantity class’s constructor must take two arguments, where the
first will receive the PVL Value (as whatever Python type pvl
determines it to be) and the second will receive the PVL Units
Expression (as a str
).
Examples of how to do this with pvl.load()
or pvl.loads()
are below for astropy
and pint
.
Depending on the PVL text that you are parsing, and the quantity class that you are using, you may get errors if the quantity class can’t accept the PVL Units Expression, or if the value part of the quantity class can’t handle all of the possible types of PVL Values (which can be Simple Values, Sets, or Sequences).
Writing out other quantity objects to PVL text¶
In order to get the encoding side of the pvl
library to write out the
correct kind of PVL text based on some quantity object is more difficult
due to the wide variety of ways that quantity objects are written in 3rd
party libaries. At this time, the pvl
library can properly encode
pvl.collecitons.Quantity
, astropy.units.Quantity
, and
pint.Quantity
objects (or objects that pass an isinstance()
test for those objects). Any other kind of quantity object in the
data structure passed to pvl.dump()
or pvl.dumps()
will
just be encoded as a string.
Other types are possible, but require additions to the encoder in
use. The astropy.units.Quantity
object is already handled
by the pvl
library, but if it wasn’t, this is how you would
enable it. You just need the class name, the name of the
property on the class that yields the value or magnitude (for
astropy.units.Quantity
that is value
), and the property
that yields the units (for astropy.units.Quantity
that is
unit
). With those pieces in hand, we just need to instantiate
an encoder and add the new quantity class and the names of those
properties to it, and then pass it to pvl.dump()
or
pvl.dumps()
as follows:
>>> import pvl
>>> from astropy import units as u
>>> my_label = dict(length=u.Quantity(15, u.m), velocity=u.Quantity(0.5, u.m / u.s))
>>> my_encoder = pvl.PDSLabelEncoder()
>>> my_encoder.add_quantity_cls(u.Quantity, 'value', 'unit')
>>> print(pvl.dumps(my_label, encoder=my_encoder))
LENGTH = 15.0 <m>
VELOCITY = 0.5 <m / s>
END
astropy.units.Quantity¶
The Astropy Project has classes for handing Units and Quantities.
The astropy.units.Quantity
object can be returned in the data
structure returned from pvl.load()
or pvl.loads()
. Here is
an example:
>>> import pvl
>>> pvl_text = "length = 42 <m/s>"
>>> regular = pvl.loads(pvl_text)
>>> print(regular['length'])
Quantity(value=42, units='m/s')
>>> print(type(regular['length']))
<class 'pvl.collections.Quantity'>
>>> from pvl.decoder import OmniDecoder
>>> from astropy import units as u
>>> w_astropy = pvl.loads(pvl_text, decoder=OmniDecoder(quantity_cls=u.Quantity))
>>> print(w_astropy)
PVLModule([
('length', <Quantity 42. m / s>)
])
>>> print(type(w_astropy['length']))
<class 'astropy.units.quantity.Quantity'>
However, in our example file and in other files you may parse, the
units may be in upper case (e.g. KM, M), and by default, astropy will
not recognize the name of these units. It will raise a handy
exception, which, in turn, will be raised as a
pvl.parser.QuantityError
that will look like this:
pvl.parser.QuantityError: 'KM' did not parse as unit: At col
0, KM is not a valid unit. Did you mean klm or km? If this is
meant to be a custom unit, define it with 'u.def_unit'. To have
it recognized inside a file reader or other code, enable it
with 'u.add_enabled_units'. For details, see
http://docs.astropy.org/en/latest/units/combining_and_defining.html
So, in order to parse our file, do this:
>>> import pvl
>>> from pvl.decoder import OmniDecoder
>>> from astropy import units as u
>>> pvl_file = 'tests/data/pds3/units1.lbl'
>>> km_upper = u.def_unit('KM', u.km)
>>> m_upper = u.def_unit('M', u.m)
>>> u.add_enabled_units([km_upper, m_upper])
<astropy.units.core._UnitContext object at ...
>>> label = pvl.load(pvl_file, decoder=OmniDecoder(quantity_cls=u.Quantity))
>>> print(label)
PVLModule([
('PDS_VERSION_ID', 'PDS3')
('MSL:COMMENT', 'THING TEST')
('FLOAT_UNIT', <Quantity 0.414 KM>)
('INT_UNIT', <Quantity 4. M>)
])
>>> print(type(label['FLOAT_UNIT']))
<class 'astropy.units.quantity.Quantity'>
Similarly, astropy.units.Quantity
objects can be encoded to PVL text
by pvl.dump()
or pvl.dumps()
without any particular special handling.
Here is an example:
>>> import pvl
>>> from astropy import units as u
>>> my_label = dict(length=u.Quantity(15, u.m), velocity=u.Quantity(0.5, u.m / u.s))
>>> print(pvl.dumps(my_label))
LENGTH = 15.0 <m>
VELOCITY = 0.5 <m / s>
END
pint.Quantity¶
The Pint library also deals with quantities.
The pint.Quantity
object can also be returned in the data
structure returned from pvl.load()
or pvl.loads()
if you
would prefer to use those objects. Here is an example:
>>> import pvl
>>> pvl_text = "length = 42 <m/s>"
>>> from pvl.decoder import OmniDecoder
>>> import pint
>>> w_pint = pvl.loads(pvl_text, decoder=OmniDecoder(quantity_cls=pint.Quantity))
>>> print(w_pint)
PVLModule([
('length', <Quantity(42, 'meter / second')>)
])
>>> print(type(w_pint['length']))
<class 'pint.quantity.Quantity'>
Just as with astropy.units.Quantity
, pint.Quantity
doesn’t recognize
the upper case units, and will raise an error like this:
pint.errors.UndefinedUnitError: 'KM' is not defined in the unit registry
So, in order to parse our file with uppercase units, you can create
a units definition file to add aliases and units to the pint
‘registry’. When doing this programmatically note that if you define
a registry on-the-fly, you must use the registry’s Quantity to the
quantity_cls
argument:
>>> import pvl
>>> from pvl.decoder import OmniDecoder
>>> import pint
>>> ureg = pint.UnitRegistry()
>>> ureg.define('kilo- = 1000 = K- = k-')
>>> ureg.define('@alias meter = M')
>>> pvl_file = 'tests/data/pds3/units1.lbl'
>>> label = pvl.load(pvl_file, decoder=OmniDecoder(quantity_cls=ureg.Quantity))
>>> print(label)
PVLModule([
('PDS_VERSION_ID', 'PDS3')
('MSL:COMMENT', 'THING TEST')
('FLOAT_UNIT', <Quantity(0.414, 'kilometer')>)
('INT_UNIT', <Quantity(4, 'meter')>)
])
>>> print(type(label['FLOAT_UNIT']))
<class 'pint.quantity.build_quantity_class.<locals>.Quantity'>
Similarly, pint.Quantity
objects can be encoded to PVL text
by pvl.dump()
or pvl.dumps()
:
>>> import pvl
>>> import pint
>>> ureg = pint.UnitRegistry()
>>> dist = 15 * ureg.m
>>> vel = 0.5 * ureg.m / ureg.second
>>> my_label = dict(length=dist, velocity=vel)
>>> print(pvl.dumps(my_label))
LENGTH = 15 <meter>
VELOCITY = 0.5 <meter / second>
END