{ "cells": [ { "cell_type": "markdown", "source": [ "# Tutorial\n", "\n", "This tutorial is intended to serve as a guide on how to create classes using *uttrs*\n", "\n", "\n", "## Interactive Version\n", "\n", "Launch Binder for an interactive version of this tutorial!\n", "\n", "[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/quatrope/uttrs/HEAD?filepath=%2Fdocs%2Fsource%2Ftutorial.ipynb)\n", "\n", "## Imports\n", "\n", "Let's first import all necessary libraries at the top. You will generally need just three:\n", "\n", "- `attr` (*attrs*) is the library uttrs is based on. Attrs creates classes with less boilerplate code.\n", "- `astropy.units` is a library that contains all the machinery to deal with astronomical and physical units.\n", "- `uttr` (*uttrs*) is the library we will explore on this tutorial.\n", "\n", "\n", "> Note: This tutorial assumes knowledge on the aforementioned libraries.\n", "Please refer to the reference links at the end of this notebook for more information." ], "metadata": {} }, { "cell_type": "code", "execution_count": 1, "source": [ "import attr # to use the .validators module\n", "import uttr\n", "import astropy.units as u" ], "outputs": [], "metadata": {} }, { "cell_type": "markdown", "source": [ "### The Galaxy Class\n", "\n", "We will create a stripped-down version of the Galaxy class from the [Galaxy-Chop](https://github.com/vcristiani/galaxy-chop) project.\n", "\n", "It will have only 8 attributes. The first 7 will have units attached and will be implemented with `uttr.ib`.\n", "These are:\n", "\n", "* `x`, `y`, `z`: The postions of the particles (typically stars) from the center of the galaxy measured in KiloParsecs ($kpc$).\n", "* `vx`, `vy`, `vz`: The relative velocity components of the particles measured in $km/s$.\n", "* `m`: Masses of the particles in units of solar masses ($M_\\odot$).\n", "\n", "The last attribute `notes` is a description text about the galaxy and can be implemented with the standar *attrs* library." ], "metadata": {} }, { "cell_type": "code", "execution_count": 2, "source": [ "@uttr.s\n", "class Galaxy:\n", " x = uttr.ib(unit=u.kpc)\n", " y = uttr.ib(unit=u.kpc)\n", " z = uttr.ib(unit=u.kpc)\n", "\n", " vx = uttr.ib(unit=u.km / u.s)\n", " vy = uttr.ib(unit=u.km / u.s)\n", " vz = uttr.ib(unit=u.km / u.s)\n", "\n", " m = uttr.ib(unit=u.M_sun)\n", "\n", " notes = attr.ib(validator=attr.validators.instance_of(str))" ], "outputs": [], "metadata": {} }, { "cell_type": "markdown", "source": [ "### Galaxy with Default Units\n", "\n", "Now that we created our class, we can go ahead and create an object of type *Galaxy*.\n", "\n", "To keep it simple, let's assume only 4 particles with totally arbitrary numbers on each attribute.\n", "\n", "Part of *uttrs* power is its ability to assign default units when not provided, or to validate that the input unit is physically compatible with the given default.\n", "\n", "Let's see first an example in which all units are assigned automatically." ], "metadata": {} }, { "cell_type": "code", "execution_count": 3, "source": [ "gal = Galaxy(\n", " x=[1, 1, 3, 4],\n", " y=[10, 2, 3, 100],\n", " z=[1, 1, 1, 1],\n", " vx=[1000, 1023, 2346, 1334],\n", " vy=[9956, 833, 954, 1024],\n", " vz=[1253, 956, 1054, 3568],\n", " m=[200, 100, 20, 5],\n", " notes=\"A random galaxy with arbitrary numbers.\",\n", ")" ], "outputs": [], "metadata": {} }, { "cell_type": "markdown", "source": [ "Let's verify that all attributes of the class were given the correct units." ], "metadata": {} }, { "cell_type": "code", "execution_count": 4, "source": [ "gal.x" ], "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ "" ], "text/latex": [ "$[1,~1,~3,~4] \\; \\mathrm{kpc}$" ] }, "metadata": {}, "execution_count": 4 } ], "metadata": {} }, { "cell_type": "code", "execution_count": 5, "source": [ "gal.y" ], "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ "" ], "text/latex": [ "$[10,~2,~3,~100] \\; \\mathrm{kpc}$" ] }, "metadata": {}, "execution_count": 5 } ], "metadata": {} }, { "cell_type": "code", "execution_count": 6, "source": [ "gal.vx" ], "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ "" ], "text/latex": [ "$[1000,~1023,~2346,~1334] \\; \\mathrm{\\frac{km}{s}}$" ] }, "metadata": {}, "execution_count": 6 } ], "metadata": {} }, { "cell_type": "code", "execution_count": 7, "source": [ "gal.m" ], "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ "" ], "text/latex": [ "$[200,~100,~20,~5] \\; \\mathrm{M_{\\odot}}$" ] }, "metadata": {}, "execution_count": 7 } ], "metadata": {} }, { "cell_type": "code", "execution_count": 8, "source": [ "gal.notes" ], "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ "'A random galaxy with arbitrary numbers.'" ] }, "metadata": {}, "execution_count": 8 } ], "metadata": {} }, { "cell_type": "markdown", "source": [ "### Galaxy with Explicit Units\n", "\n", "A different alternative is to provide units compatible with the default unit.\n", "In this case, we have to be mindful of the phyisical equivalence of units with the ones given at the time the class was created.\n", "\n", "For example, we could suggest that the dimension `z` be given in parsecs, `vy` in $km/h$ and masses in $kg$." ], "metadata": {} }, { "cell_type": "code", "execution_count": 9, "source": [ "gal = Galaxy(\n", " x=[1, 1, 3, 4],\n", " y=[10, 2, 3, 100],\n", " z=[1000, 1000, 1000, 1000] * u.parsec,\n", " vx=[1000, 1023, 2346, 1334],\n", " vy=[9956, 833, 954, 1024] * (u.km / u.h),\n", " vz=[1253, 956, 1054, 3568],\n", " m=[200, 100, 20, 5] * u.kg,\n", " notes=\"A random galaxy with arbitrary numbers.\",\n", ")" ], "outputs": [], "metadata": {} }, { "cell_type": "markdown", "source": [ "As we note above, this works as expected without error.\n", "We can further access any of the attributes and verify that they keep the suggested units." ], "metadata": {} }, { "cell_type": "code", "execution_count": 10, "source": [ "gal.z # parsecs" ], "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ "" ], "text/latex": [ "$[1000,~1000,~1000,~1000] \\; \\mathrm{pc}$" ] }, "metadata": {}, "execution_count": 10 } ], "metadata": {} }, { "cell_type": "code", "execution_count": 11, "source": [ "gal.m # kg" ], "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ "" ], "text/latex": [ "$[200,~100,~20,~5] \\; \\mathrm{kg}$" ] }, "metadata": {}, "execution_count": 11 } ], "metadata": {} }, { "cell_type": "code", "execution_count": 12, "source": [ "gal.vx # default km/s" ], "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ "" ], "text/latex": [ "$[1000,~1023,~2346,~1334] \\; \\mathrm{\\frac{km}{s}}$" ] }, "metadata": {}, "execution_count": 12 } ], "metadata": {} }, { "cell_type": "code", "execution_count": 13, "source": [ "gal.vy # km/h" ], "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ "" ], "text/latex": [ "$[9956,~833,~954,~1024] \\; \\mathrm{\\frac{km}{h}}$" ] }, "metadata": {}, "execution_count": 13 } ], "metadata": {} }, { "cell_type": "markdown", "source": [ "On the other hand, if we try to input a unit that is incompatible with the suggested input unit, a `ValueError` exception is raised.\n", "\n", "To show this, let's try to assign `x` values with units of grams ($g$)." ], "metadata": {} }, { "cell_type": "code", "execution_count": 14, "source": [ "gal = Galaxy(\n", " x=[1, 1, 3, 4] * u.g,\n", " y=[10, 2, 3, 100],\n", " z=[1000, 1000, 1000, 1000] * u.parsec,\n", " vx=[1000, 1023, 2346, 1334],\n", " vy=[9956, 833, 954, 1024] * (u.km / u.h),\n", " vz=[1253, 956, 1054, 3568],\n", " m=[200, 100, 20, 5] * u.kg,\n", " notes=\"A random galaxy with arbitrary numbers.\",\n", ")" ], "outputs": [ { "output_type": "error", "ename": "ValueError", "evalue": "Unit of attribute 'x' must be equivalent to 'kpc'. Found 'g'.", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mUnitConversionError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m~/proyectos/uttrs/src/uttr.py\u001b[0m in \u001b[0;36mvalidate_is_equivalent_unit\u001b[0;34m(self, instance, attribute, value)\u001b[0m\n\u001b[1;32m 131\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 132\u001b[0;31m \u001b[0munity\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mto\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0munit\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 133\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mUnitConversionError\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/proyectos/uttrs/lib/python3.8/site-packages/astropy/units/quantity.py\u001b[0m in \u001b[0;36mto\u001b[0;34m(self, unit, equivalencies)\u001b[0m\n\u001b[1;32m 688\u001b[0m \u001b[0munit\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mUnit\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0munit\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 689\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_new_view\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_to_value\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0munit\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mequivalencies\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0munit\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 690\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/proyectos/uttrs/lib/python3.8/site-packages/astropy/units/quantity.py\u001b[0m in \u001b[0;36m_to_value\u001b[0;34m(self, unit, equivalencies)\u001b[0m\n\u001b[1;32m 659\u001b[0m \u001b[0mequivalencies\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_equivalencies\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 660\u001b[0;31m return self.unit.to(unit, self.view(np.ndarray),\n\u001b[0m\u001b[1;32m 661\u001b[0m equivalencies=equivalencies)\n", "\u001b[0;32m~/proyectos/uttrs/lib/python3.8/site-packages/astropy/units/core.py\u001b[0m in \u001b[0;36mto\u001b[0;34m(self, other, value, equivalencies)\u001b[0m\n\u001b[1;32m 986\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 987\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_get_converter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mother\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mequivalencies\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mequivalencies\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvalue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 988\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/proyectos/uttrs/lib/python3.8/site-packages/astropy/units/core.py\u001b[0m in \u001b[0;36m_get_converter\u001b[0;34m(self, other, equivalencies)\u001b[0m\n\u001b[1;32m 917\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 918\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mexc\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 919\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/proyectos/uttrs/lib/python3.8/site-packages/astropy/units/core.py\u001b[0m in \u001b[0;36m_get_converter\u001b[0;34m(self, other, equivalencies)\u001b[0m\n\u001b[1;32m 902\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 903\u001b[0;31m return self._apply_equivalencies(\n\u001b[0m\u001b[1;32m 904\u001b[0m self, other, self._normalize_equivalencies(equivalencies))\n", "\u001b[0;32m~/proyectos/uttrs/lib/python3.8/site-packages/astropy/units/core.py\u001b[0m in \u001b[0;36m_apply_equivalencies\u001b[0;34m(self, unit, other, equivalencies)\u001b[0m\n\u001b[1;32m 885\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 886\u001b[0;31m raise UnitConversionError(\n\u001b[0m\u001b[1;32m 887\u001b[0m \"{} and {} are not convertible\".format(\n", "\u001b[0;31mUnitConversionError\u001b[0m: 'g' (mass) and 'kpc' (length) are not convertible", "\nDuring handling of the above exception, another exception occurred:\n", "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m gal = Galaxy(\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m3\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m4\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m*\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mg\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0my\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m10\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m3\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m100\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0mz\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1000\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1000\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1000\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1000\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m*\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mparsec\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0mvx\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1000\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1023\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2346\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1334\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, x, y, z, vx, vy, vz, m, notes)\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnotes\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnotes\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 10\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0m_config\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_run_validators\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 11\u001b[0;31m \u001b[0m__attr_validator_x\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0m__attr_x\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 12\u001b[0m \u001b[0m__attr_validator_y\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0m__attr_y\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0my\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 13\u001b[0m \u001b[0m__attr_validator_z\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0m__attr_z\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mz\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/proyectos/uttrs/lib/python3.8/site-packages/attr/_make.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, inst, attr, value)\u001b[0m\n\u001b[1;32m 2721\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__call__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0minst\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mattr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2722\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mv\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_validators\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 2723\u001b[0;31m \u001b[0mv\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minst\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mattr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2724\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2725\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m~/proyectos/uttrs/src/uttr.py\u001b[0m in \u001b[0;36mvalidate_is_equivalent_unit\u001b[0;34m(self, instance, attribute, value)\u001b[0m\n\u001b[1;32m 133\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mUnitConversionError\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 134\u001b[0m \u001b[0munit\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0maname\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mufound\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0munit\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mattribute\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mname\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0munit\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 135\u001b[0;31m raise ValueError(\n\u001b[0m\u001b[1;32m 136\u001b[0m \u001b[0;34mf\"Unit of attribute '{aname}' must be equivalent to '{unit}'.\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 137\u001b[0m \u001b[0;34mf\" Found '{ufound}'.\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mValueError\u001b[0m: Unit of attribute 'x' must be equivalent to 'kpc'. Found 'g'." ] } ], "metadata": {} }, { "cell_type": "markdown", "source": [ "## Automatic Cohersion of Units: Array Accessor\n", "\n", "One powerful feauture of *uttrs* is the ability to easily transform all units to plain `numpy.ndarray`, using the default units.\n", "\n", "This is achieved using the `uttr.array_accessor()` function.\n", "This allows for uniform access of attributes defined by uttrs, in a data structure that has faster access time than its counterpart with units.\n", "\n", "By default the `@uttr.s` automataclly add an array accessor to decorated class. You can disabled this functionallity using the decorator like\n", "`@uttr.s(aaccessor=None)`, or change the name of the property with `@uttr.s(aaccessor=\"other_name\")`.\n", "\n", "\n", "Expanding on the previous example:" ], "metadata": {} }, { "cell_type": "code", "execution_count": null, "source": [ "@uttr.s\n", "class Galaxy:\n", " x = uttr.ib(unit=u.kpc)\n", " y = uttr.ib(unit=u.kpc)\n", " z = uttr.ib(unit=u.kpc)\n", "\n", " vx = uttr.ib(unit=u.km / u.s)\n", " vy = uttr.ib(unit=u.km / u.s)\n", " vz = uttr.ib(unit=u.km / u.s)\n", "\n", " m = uttr.ib(unit=u.M_sun)\n", "\n", " notes = attr.ib(validator=attr.validators.instance_of(str))" ], "outputs": [], "metadata": {} }, { "cell_type": "markdown", "source": [ "Let's instantiate the class again with some parameters with custom units." ], "metadata": {} }, { "cell_type": "code", "execution_count": null, "source": [ "gal = Galaxy(\n", " x=[1, 1, 3, 4],\n", " y=[10, 2, 3, 100],\n", " z=[1000, 1000, 1000, 1000] * u.parsec,\n", " vx=[1000, 1023, 2346, 1334],\n", " vy=[9956, 833, 954, 1024] * (u.km / u.h),\n", " vz=[1253, 956, 1054, 3568],\n", " m=[200, 100, 20, 5] * u.kg,\n", " notes=\"A random galaxy with arbitrary numbers.\",\n", ")" ], "outputs": [], "metadata": {} }, { "cell_type": "markdown", "source": [ "If we now access `z` through our `arr_` accessor, *uttrs* will convert the values in parsec units to kiloparsecs and return a uniform numpy array." ], "metadata": {} }, { "cell_type": "code", "execution_count": null, "source": [ "gal.arr_.z" ], "outputs": [], "metadata": {} }, { "cell_type": "markdown", "source": [ "While `z` keeps its original unit." ], "metadata": {} }, { "cell_type": "code", "execution_count": null, "source": [ "gal.arr_.z" ], "outputs": [], "metadata": {} }, { "cell_type": "markdown", "source": [ "The same applies to `vy` and `m`." ], "metadata": {} }, { "cell_type": "code", "execution_count": null, "source": [ "gal.arr_.m" ], "outputs": [], "metadata": {} }, { "cell_type": "code", "execution_count": null, "source": [ "gal.arr_.vy" ], "outputs": [], "metadata": {} }, { "cell_type": "markdown", "source": [ "If we try to access a private attribute not from `uttr.ib`, an `AttributeError` exception is raised." ], "metadata": {} }, { "cell_type": "code", "execution_count": null, "source": [ "gal.arr_.notes" ], "outputs": [], "metadata": {} }, { "cell_type": "markdown", "source": [ "## Using the `array_accessor`\n", "\n", "It is a known issue that Astropy units can slow down complex computations.\n", "\n", "To avoid this, developers usually choose to uniformize units and convert the values to numpy arrays to operate on them faster; reverting back to values with units at the end of the calculation.\n", "\n", "As a helper, `array_accesor` will perform the transformation in a transparent way to the user, avoiding the need to replicate information regarding units.\n", "\n", "For example, if we wanted to program code that generates a new Galaxy object with a single particle that is the average mean of all the rest, we could do something like this:" ], "metadata": {} }, { "cell_type": "code", "execution_count": null, "source": [ "@uttr.s\n", "class Galaxy:\n", " x = uttr.ib(unit=u.kpc)\n", " y = uttr.ib(unit=u.kpc)\n", " z = uttr.ib(unit=u.kpc)\n", "\n", " vx = uttr.ib(unit=u.km / u.s)\n", " vy = uttr.ib(unit=u.km / u.s)\n", " vz = uttr.ib(unit=u.km / u.s)\n", "\n", " m = uttr.ib(unit=u.M_sun)\n", "\n", " notes = attr.ib(validator=attr.validators.instance_of(str))\n", "\n", " def mean(self):\n", " x = np.mean(self.arr_.x)\n", " y = np.mean(self.arr_.y)\n", " z = np.mean(self.arr_.z)\n", "\n", " vx = np.mean(self.arr_.vx)\n", " vy = np.mean(self.arr_.vy)\n", " vz = np.mean(self.arr_.vz)\n", "\n", " m = np.mean(self.arr_.m)\n", "\n", " return Galaxy(\n", " x=x, y=y, z=z, vx=vx, vy=vy, vz=vz, m=m, notes=self.notes\n", " )" ], "outputs": [], "metadata": {} }, { "cell_type": "markdown", "source": [ "We could now create a galaxy with 1 million random elements and calculate the \"average\" galaxy." ], "metadata": {} }, { "cell_type": "code", "execution_count": null, "source": [ "import numpy as np\n", "\n", "# Fix random seed\n", "random = np.random.default_rng(seed=42)\n", "\n", "size = 1_000_000\n", "\n", "gal = Galaxy(\n", " x=random.random(size=size),\n", " y=random.random(size=size),\n", " z=random.random(size=size) * u.parsec,\n", " vx=random.random(size=size),\n", " vy=random.random(size=size),\n", " vz=random.random(size=size) * (u.km / u.h),\n", " m=random.random(size=size) * u.kg,\n", " notes=\"A random galaxy with arbitrary numbers.\",\n", ")" ], "outputs": [], "metadata": {} }, { "cell_type": "code", "execution_count": null, "source": [ "gal.mean()" ], "outputs": [], "metadata": {} }, { "cell_type": "markdown", "source": [ "To complete the example, let's see how would a `mean` method look like without `array_accessor`." ], "metadata": {} }, { "cell_type": "code", "execution_count": null, "source": [ "@uttr.s(aaccessor=None)\n", "class Galaxy:\n", " x = uttr.ib(unit=u.kpc)\n", " y = uttr.ib(unit=u.kpc)\n", " z = uttr.ib(unit=u.kpc)\n", "\n", " vx = uttr.ib(unit=u.km / u.s)\n", " vy = uttr.ib(unit=u.km / u.s)\n", " vz = uttr.ib(unit=u.km / u.s)\n", "\n", " m = uttr.ib(unit=u.M_sun)\n", "\n", " notes = attr.ib(validator=attr.validators.instance_of(str))\n", "\n", " def mean(self):\n", " x = np.mean(self.x.to_value(u.kpc))\n", " y = np.mean(self.y.to_value(u.kpc))\n", " z = np.mean(self.z.to_value(u.kpc))\n", "\n", " vx = np.mean(self.vx.to_value(u.km / u.s))\n", " vy = np.mean(self.vy.to_value(u.km / u.s))\n", " vz = np.mean(self.vz.to_value(u.km / u.s))\n", "\n", " m = np.mean(self.m.to_value(u.M_sun))\n", "\n", " return Galaxy(\n", " x=x, y=y, z=z, vx=vx, vy=vy, vz=vz, m=m, notes=self.notes\n", " )" ], "outputs": [], "metadata": {} }, { "cell_type": "code", "execution_count": null, "source": [ "import datetime as dt\n", "dt.date.today().isoformat()" ], "outputs": [], "metadata": {} }, { "cell_type": "code", "execution_count": null, "source": [], "outputs": [], "metadata": {} } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.5" } }, "nbformat": 4, "nbformat_minor": 4 }