Tutorial¶
Este tutorial busca servir de una guia para la creación de clases utilizando uttrs.
DRAFT!
Imports¶
En primer lugar es necesario importar todas las librerias que vamos a utilizar. En general, son solo tres:
attr
(attrs) que es la libreria en la cual se basa uttrs para crear clases con menos boiler plate.astropy.units
La cual contiene todo el marco de utilidades para tratar con unidades físicas/astronómicas.uttr
(uttrs) La librería que corresponde este tutorial.Nota: Este tutorial asume un conocimiento sobre estas librerías, una serie de enlaces de referencias pueden encontrarse al final de la página.
[1]:
import attr
import uttr
import astropy.units as u
The galaxy class¶
La clase que vamos a crear, consiste en una simplicación de la clase Galaxia del proyecot Galaxy-Chop.
Solo tiene 8 atributos y solo los primeros 7 tienen unidades y por lo tanto seran implementadas con la funcion uttr.ib
de la librería. Estos sonx
, y
, z
son las posiciones de las particulas/estrellas en KiloParsecs (kpc
) ; vx
, vy
, vz
las velocidades correspondientes a las particulas (\(Km/s\)); m
su masa en masas solares (\(M_\odot\)). Todos
Finalmente notes
texto libre sobre las galaxias y pueden ser implementadas con la librería attrs estandar
[2]:
@attr.s
class Galaxy:
x = uttr.ib(unit=u.kpc)
y = uttr.ib(unit=u.kpc)
z = uttr.ib(unit=u.kpc)
vx = uttr.ib(unit=u.km / u.s)
vy = uttr.ib(unit=u.km / u.s)
vz = uttr.ib(unit=u.km / u.s)
m = uttr.ib(unit=u.M_sun)
notes = attr.ib(validator=attr.validators.instance_of(str))
Galaxy with default units¶
Finalmente con la clase ya disponible podemos proceder a crear un objeto del tipo Galaxy,
Por cuestiones de simplicidadad, vamos a asumir solo 4 particulas con numeros totalmente arbitrarios en cada atributo.
Parte de la utilidad de usar uttrs, es la capacidad que tiene la libreria para agregar unidades por defecto automáticamente, o validar que la unidad ingresada sea equivalente.
Empecemos con un objeto en el cual todas las unidades se asignan autoḿaticamente
[3]:
gal = Galaxy(
x=[1, 1, 3, 4],
y=[10, 2, 3, 100],
z=[1, 1, 1, 1],
vx=[1000, 1023, 2346, 1334],
vy=[9956, 833, 954, 1024],
vz=[1253, 956, 1054, 3568],
m=[200, 100, 20, 5],
notes="a random galaxy made with random numbers",
)
Si nos fijamos en cualquier atributo de la clase, vamos a ver que todas las unidades se agregaron de manera correcta
[4]:
gal.x
[4]:
[5]:
gal.y
[5]:
[6]:
gal.vx
[6]:
[7]:
gal.m
[7]:
[8]:
gal.notes
[8]:
'a random galaxy made with random numbers'
Galaxy with explicit units¶
Otra alternativa consiste en proveer unidades compatibles con las establecidas, hay que tener en cuenta que estas unidades tienen que ser equivalentes a las propuestas en la creacion de la clase.
Por ejemplo podemos sugerir que la dimension Z
este dada en parsecs, vy
en \(Km/h\) y las masas en \(Kg\)
[9]:
gal = Galaxy(
x=[1, 1, 3, 4],
y=[10, 2, 3, 100],
z=[1000, 1000, 1000, 1000] * u.parsec,
vx=[1000, 1023, 2346, 1334],
vy=[9956, 833, 954, 1024] * (u.km / u.h),
vz=[1253, 956, 1054, 3568],
m=[200, 100, 20, 5] * u.kg,
notes="a random galaxy made with random numbers",
)
Como se nota en el ejemplo, esto funciona perfectamente, y ningun error aparece. Es mas podemos acceder a culquiera de los atributos presentes y todos mantienen las unidades sugeridas o explicitas
[10]:
gal.z # parsecs
[10]:
[11]:
gal.m # kg
[11]:
[12]:
gal.vx # default km/s
[12]:
[13]:
gal.vy # km/h
[13]:
Por otro lado si por error cargamos un valor que tiene una unidad que no es equivalente a la sugerida, se lanza un error de valor (ValueError
)
Para demostrar esto vamos a extender el ejemplo, tratando de asignar a x
un valor expresado en gramos (g
)
[14]:
gal = Galaxy(
x=[1, 1, 3, 4] * u.g,
y=[10, 2, 3, 100],
z=[1000, 1000, 1000, 1000] * u.parsec,
vx=[1000, 1023, 2346, 1334],
vy=[9956, 833, 954, 1024] * (u.km / u.h),
vz=[1253, 956, 1054, 3568],
m=[200, 100, 20, 5] * u.kg,
notes="a random galaxy made with random numbers",
)
---------------------------------------------------------------------------
UnitConversionError Traceback (most recent call last)
~/proyectos/uttrs/src/uttr.py in validate_is_equivalent_unit(self, instance, attribute, value)
135 try:
--> 136 unity.to(self.unit)
137 except u.UnitConversionError:
~/proyectos/uttrs/lib/python3.8/site-packages/astropy/units/quantity.py in to(self, unit, equivalencies)
688 unit = Unit(unit)
--> 689 return self._new_view(self._to_value(unit, equivalencies), unit)
690
~/proyectos/uttrs/lib/python3.8/site-packages/astropy/units/quantity.py in _to_value(self, unit, equivalencies)
659 equivalencies = self._equivalencies
--> 660 return self.unit.to(unit, self.view(np.ndarray),
661 equivalencies=equivalencies)
~/proyectos/uttrs/lib/python3.8/site-packages/astropy/units/core.py in to(self, other, value, equivalencies)
986 else:
--> 987 return self._get_converter(other, equivalencies=equivalencies)(value)
988
~/proyectos/uttrs/lib/python3.8/site-packages/astropy/units/core.py in _get_converter(self, other, equivalencies)
917
--> 918 raise exc
919
~/proyectos/uttrs/lib/python3.8/site-packages/astropy/units/core.py in _get_converter(self, other, equivalencies)
902 try:
--> 903 return self._apply_equivalencies(
904 self, other, self._normalize_equivalencies(equivalencies))
~/proyectos/uttrs/lib/python3.8/site-packages/astropy/units/core.py in _apply_equivalencies(self, unit, other, equivalencies)
885
--> 886 raise UnitConversionError(
887 "{} and {} are not convertible".format(
UnitConversionError: 'g' (mass) and 'kpc' (length) are not convertible
During handling of the above exception, another exception occurred:
ValueError Traceback (most recent call last)
<ipython-input-14-0d84924447d4> in <module>
----> 1 gal = Galaxy(
2 x=[1, 1, 3, 4] * u.g,
3 y=[10, 2, 3, 100],
4 z=[1000, 1000, 1000, 1000] * u.parsec,
5 vx=[1000, 1023, 2346, 1334],
<attrs generated init __main__.Galaxy> in __init__(self, x, y, z, vx, vy, vz, m, notes)
9 self.notes = notes
10 if _config._run_validators is True:
---> 11 __attr_validator_x(self, __attr_x, self.x)
12 __attr_validator_y(self, __attr_y, self.y)
13 __attr_validator_z(self, __attr_z, self.z)
~/proyectos/uttrs/lib/python3.8/site-packages/attr/_make.py in __call__(self, inst, attr, value)
2721 def __call__(self, inst, attr, value):
2722 for v in self._validators:
-> 2723 v(inst, attr, value)
2724
2725
~/proyectos/uttrs/src/uttr.py in validate_is_equivalent_unit(self, instance, attribute, value)
137 except u.UnitConversionError:
138 unit, aname, ufound = self.unit, attribute.name, value.unit
--> 139 raise ValueError(
140 f"Unit of attribute '{aname}' must be equivalent to '{unit}'."
141 f" Found '{ufound}'."
ValueError: Unit of attribute 'x' must be equivalent to 'kpc'. Found 'g'.
Automatic cohersion of units: Array Accessor¶
El mayor poder de uttrs es la capacidad de transformar de manera sencilla todas las unidades a numpy.ndarray
planos, utilizando las unidades por defecto.
Para esto se provee de una funcion uttr.array_accessor()
la cual permite acceder a los atributos definidos por uttrs de manera uniforme en una estructura de datos mas veloz que las que possen unidad.
Para agregar esta característica, se debe agregar un atributo extra a la clase que se iguale a uttr.array_accessor()
. Se propone utilizar el nombre arr_
Extendiendo el ejemplo anterior
[15]:
@attr.s
class Galaxy:
x = uttr.ib(unit=u.kpc)
y = uttr.ib(unit=u.kpc)
z = uttr.ib(unit=u.kpc)
vx = uttr.ib(unit=u.km / u.s)
vy = uttr.ib(unit=u.km / u.s)
vz = uttr.ib(unit=u.km / u.s)
m = uttr.ib(unit=u.M_sun)
notes = attr.ib(validator=attr.validators.instance_of(str))
arr_ = uttr.array_accessor() # el accessor
Ahora volvemos a instanciar la clase con algunos parámetros con unidades custom
[16]:
gal = Galaxy(
x=[1, 1, 3, 4],
y=[10, 2, 3, 100],
z=[1000, 1000, 1000, 1000] * u.parsec,
vx=[1000, 1023, 2346, 1334],
vy=[9956, 833, 954, 1024] * (u.km / u.h),
vz=[1253, 956, 1054, 3568],
m=[200, 100, 20, 5] * u.kg,
notes="a random galaxy made with random numbers",
)
ahora si accedemos a z
a travez de arr_
, uttrs se encargara de convertir los parsecs en kiloparsecs y luego convertirlo en un numpy array
[17]:
gal.arr_.z
[17]:
array([1., 1., 1., 1.])
Mientras que z
mantiene sus unidades originales
[18]:
gal.arr_.z
[18]:
array([1., 1., 1., 1.])
Lo mismo si accedemos a vy
y m
[19]:
gal.arr_.m
[19]:
array([1.00582884e-28, 5.02914422e-29, 1.00582884e-29, 2.51457211e-30])
[20]:
gal.arr_.vy
[20]:
array([2.76555556, 0.23138889, 0.265 , 0.28444444])
Tratar de acceder a un atributo privado o que no sea un uttr.ib
, lanza un error del tipo AttributeError
[21]:
gal.arr_.notes
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-21-92911293967b> in <module>
----> 1 gal.arr_.notes
~/proyectos/uttrs/src/uttr.py in __getattr__(self, a)
278 return arr
279
--> 280 raise AttributeError(f"No uttr.Attribute '{a}'")
281
282
AttributeError: No uttr.Attribute 'notes'
El uso de array_accessor
¶
Es conocido que las unidades de Astropy son lentas cuando los calculos son complejos.
Para evitar esto, los desarrolladores optan por unificar las unidades y luego convertir los valores a arrays de numpy para operarlos mas rapidamente; y al final se vuelve a asignas las unidades.
Para evitar esto, array_accesor
realiza toda esta trasnformacion transparentemente para el usuario, evitando la necesidad de replicar informacion sobre las unidades.
Por ejemplo, si quisieramos programar un codigo que genere un nuevo objeto galaxia con una sola particula, promedio de las demas, el código seria el siguiente:
[22]:
@attr.s
class Galaxy:
x = uttr.ib(unit=u.kpc)
y = uttr.ib(unit=u.kpc)
z = uttr.ib(unit=u.kpc)
vx = uttr.ib(unit=u.km / u.s)
vy = uttr.ib(unit=u.km / u.s)
vz = uttr.ib(unit=u.km / u.s)
m = uttr.ib(unit=u.M_sun)
notes = attr.ib(validator=attr.validators.instance_of(str))
arr_ = uttr.array_accessor() # el accessor
def mean(self):
x = np.mean(self.arr_.x)
y = np.mean(self.arr_.y)
z = np.mean(self.arr_.z)
vx = np.mean(self.arr_.vx)
vy = np.mean(self.arr_.vy)
vz = np.mean(self.arr_.vz)
m = np.mean(self.arr_.m)
return Galaxy(
x=x, y=y, z=z, vx=vx, vy=vy, vz=vz, m=m, notes=self.notes
)
Ahora podemos crear una galaxia de 1 millon de elementos aleatorios y calcular la galaxia “media”
[23]:
import numpy as np
# fijamos la semilla random
random = np.random.default_rng(seed=42)
size = 1_000_000
gal = Galaxy(
x=random.random(size=size),
y=random.random(size=size),
z=random.random(size=size) * u.parsec,
vx=random.random(size=size),
vy=random.random(size=size),
vz=random.random(size=size) * (u.km / u.h),
m=random.random(size=size) * u.kg,
notes="a random galaxy made with random numbers",
)
[24]:
gal.mean()
[24]:
Galaxy(x=<Quantity 0.50002648 kpc>, y=<Quantity 0.49981983 kpc>, z=<Quantity 0.00049982 kpc>, vx=<Quantity 0.49976787 km / s>, vy=<Quantity 0.50029902 km / s>, vz=<Quantity 0.00013897 km / s>, m=<Quantity 2.5120227e-31 solMass>, notes='a random galaxy made with random numbers')
Por completitud se ejemplifica a continuacion como seria el miemos codigo de mean
pero sin usar array_accessor
[25]:
@attr.s
class Galaxy:
x = uttr.ib(unit=u.kpc)
y = uttr.ib(unit=u.kpc)
z = uttr.ib(unit=u.kpc)
vx = uttr.ib(unit=u.km / u.s)
vy = uttr.ib(unit=u.km / u.s)
vz = uttr.ib(unit=u.km / u.s)
m = uttr.ib(unit=u.M_sun)
notes = attr.ib(validator=attr.validators.instance_of(str))
arr_ = uttr.array_accessor() # el accessor
def mean(self):
x = np.mean(self.x.to_value(u.kpc))
y = np.mean(self.y.to_value(u.kpc))
z = np.mean(self.z.to_value(u.kpc))
vx = np.mean(self.vx.to_value(u.km / u.s))
vy = np.mean(self.vy.to_value(u.km / u.s))
vz = np.mean(self.vz.to_value(u.km / u.s))
m = np.mean(self.m.to_value(u.M_sun))
return Galaxy(
x=x, y=y, z=z, vx=vx, vy=vy, vz=vz, m=m, notes=self.notes
)
[26]:
import datetime as dt
dt.datetime.now()
[26]:
datetime.datetime(2020, 11, 21, 20, 52, 24, 347474)
[ ]: