Author: jcfilippazzo

Create and populate a SQL database using astrodbkit

The astrodbkit package can be used to modify an existing SQL database (such as The BDNYC Database) but it can also be used to create and populate a SQL database from scratch.

To do this, import the BDdb module and create a new database with

from astrodbkit import astrodb
dbpath = '/path/to/new_database.db'
astrodb.create_databse(dbpath)

Then load your new database with

db = astrodb.get_database(dbpath)

and start adding tables! The db.table() method accepts as its arguments the table name, list of field names, and list of data types like so:

db.table('my_new_table', ['field1','field2'], ['INTEGER','TEXT'], new_table=True)

Note new_table=True is necessary to create a new table. Otherwise, it looks for an existing table to modify (which you could do as well!).

To populate your new database with data, read the documentation here or a summary at Adding Data to the BDNYC Database.

As always, I recommend the SQLite Browser for a nice GUI to make changes outside of the command line.

Happy databasing!

Deploying Package Releases to PyPI

So you’ve made some cool new improvements to your Python package and you want to deploy a new release. It’s just a few easy steps.

  1. After your package modules have been updated, you have to update the version number in your __init__.py file. The format is major.minor.micro depending on what you changed. For example, a small bug fix to v0.2.3 would increment to v0.2.4 while a new feature might increment to v0.3.0.
  2. Then make sure all the changes are committed and pushed to Github.
  3. Build with python setup.py sdist
  4. Twine with twine upload dist/compressed_package_filename

That’s it!

Adding Data to the BDNYC Data Archive

To add data to any table in BDNYC.db, there are two easy steps. As our working example, we’ll add some new objects to the SOURCES table.

Step 1: Create an ascii file of the data

Put the data to be added in an ascii file with the following formatting:

  1. The first line must be the delimiter-separated column names to insert/update, e.g. ra|dec|publication_id. Note that the column names in the ascii file need not be in the same order as the table. Also, only the column names that match will be added and non-matching or missing column names will be ignored, e.g. spectral_type|ra|publication_id|dec will ignore the spectral_type values as this is not a column in the SOURCES table and input the other columns in the correct places.
  2. If a record (i.e. a line in your ascii file) has no value for a particular column, type nothing. E.g. for the given column names ra|dec|publication_id|comments, a record with no publication_id should read 34.567|12.834||This object is my favorite!.
  3. Be mindful of your delimiter and be sure not to use it in any of the record values!

Step 2: Add the data to the specified table

To add the data to the table (in our example, the SOURCES table), import BDdb.py and initialize the .db file. Then run the add_data() method with the path to the ascii file as the first argument and the table to add the data to as the second argument. Here’s what that looks like:

from BDNYCdb import BDdb
db = BDdb.get_db('/path/to/the/database/file.db')
db.add_data('/path/to/the/upload/file.csv', 'sources', delimiter='|')

If you want to use a delimiter other than ‘|’, just specify it with the *delimiter argument.

That’s it!

Example Query: SpeX Prism Spectra for Very Red, Field Age, Early-L Dwarfs

Here we needed the SpeX Prism spectra for all the field age L0-L5 dwarfs with J-Ks color greater than a certain value that is different for each spectral type.

To perform this query we needed to join the SOURCES, SPECTRAL_TYPES, PHOTOMETRY, and SPECTRA tables by source_id. Then in the ‘where’ clause, we had to specify:

  • Our band 1 is ‘J’ and band 2 is ‘Ks’ both from system 2 (2MASS) to get the J-Ks color,
  • The instrument_id for the spectra must be 6 (SpeX),
  • The gravity must not be $$beta$$ or $$gamma$$ if it is field age,
  • And then we joined a bunch of conditionals for each spectral type/color requirement in parentheses with ‘or’ operators.

import BDdb
db = BDdb.get_db('/path/to/Dropbox/BDNYCdb/BDNYC.db')
red_field_Ls = db.query.execute("SELECT a.id, a.names, b.spectral_type, b.gravity, (c1.magnitude-c2.magnitude) AS color, d.wavelength, d.flux, d.unc FROM sources a JOIN spectral_types b ON a.id=b.source_id JOIN photometry c1 ON a.id=c1.source_id JOIN photometry c2 ON a.id=c2.source_id JOIN spectra d ON a.id=d.source_id WHERE b.gravity IS NULL AND c1.band='J' AND c1.system=2 AND c2.band='Ks' AND c2.system=2 AND d.instrument_id=6 AND b.regime='OPT' AND ((color>=1.3 AND b.spectral_type=10) OR (color>=1.35 AND b.spectral_type=11) OR (color>=1.48 AND b.spectral_type=12) OR (color>=1.64 AND b.spectral_type=13) OR (color>=1.69 AND b.spectral_type=14) OR (color>=1.72 AND b.spectral_type=15))").fetchall()

This returns a list of the form [source_id, name, optical spectral type, gravity suffix (which should be None), J-Ks color, wavelength array, flux array, uncertainty array]

Example Query: High Resolution NIRSPEC data

Here we needed the high resolution J band spectra for NIRSPEC orders 61 and 65 for all available objects in the database. In this snippet, we use the dict method instead of the usual query method so we can dump all of our spectra and meta data into a dictionary D.

import BDdb
db = BDdb.get_db('/path/to/Dropbox/BDNYCdb/BDNYC.db')
D = db.dict.execute("select * from spectra where wavelength_order in (61,65)").fetchall()

Then we can just use a list comprehension to iterate through the list and grab all the data like a Python dictionary with the column names as keys, i.e. D[n][‘flux’] gives you the flux array, D[n][‘source_id’] the source_id, etc. for the n results in your query.

Adding Spectra to the BDNYC Data Archive

Once your BDNYC development environment is all setup, adding spectra to the database is relatively simple and all done through Terminal.

Open Terminal and then launch iPython from the command line with ipython --pylab.

Next, import the BDdb module and load the actual database with:

import BDdb
db = BDdb.get_db('/Users/your_computer/Dropbox/BDNYCdb/BDNYC.db')

Now you’re all set to start adding spectra. There are two types of files that we typically use for spectra: ascii files and FITS files.

Adding Spectra from ASCII Files

Ascii files are just text files with two or three columns of data, which should always be the wavelength, flux and (if three columns) the uncertainty values at each point. Adding this data is super simple:

db.add_ascii('/path/to/the/ascii/file', source_id)

So how do you find the source_id for your object? Just use the identify() method. For example, say my object is ‘2MASS J00361617+1821104’. I could search by the first four digits of the object name, in this case:

db.identify('0036')

This may return a number of different sources, all with ‘0036’ appearing somewhere in their name, so I would then visually inspect to find the source I’m looking for. In this case, the source_id is 86.

The file path and the source_id are the only required information to provide to add this type of data though the more columns you can complete the better! The values of the optional columns can just be entered in the function as arguments, e.g. telescope_id=6. The other columns include:

Argument Description
wavelength_units Units of the wavelength axis, e.g. ‘um’ for microns, ‘A’ for angstroms
flux_units Units of the flux axis, e.g. ‘erg/s/cm2/A’, ‘W/m2/A’, ‘Jy’, etc.
wavelength_order For high resolution data only, e.g. 58, 59, 60, etc.
publication_id The id from the PUBLICATIONS table that corresponds to the paper where the data was first published.
obs_date The date of the observations as YYYY-MM-DD
instrument_id The id from the INSTRUMENTS table that corresponds to the instrument used to record the spectrum.
telescope_id The id from the TELESCOPES table that corresponds to the telescope used to record the spectrum.
mode_id The id from the MODES table that corresponds to the instrument mode used to record the spectrum.
airmass The airmass at the time the spectrum was recorded, which is any value between 0 and 2.
comment Comments about the spectrum that a future user of the data might need to know!

So as an example, if I want to add a NIR spectrum to the database taken with the SpeX Prism instrument on the NASA IRTF, my code should look like:

db.add_ascii('/Users/Joe/Desktop/NIR_spectra/2m0036_nir.txt', 86, telescope_id=7, instrument_id=6, mode_id=1, obs_date='2013-01-17')

Note: the order of the optional arguments does not matter!

How do you know the telescope_id, instrument_id, mode_id, or publication_id? You can browse these tables in Terminal with the lookup() method by passing the table name and search criterion.

For example db.lookup('instruments') will show you every instrument_id and name. But db.lookup('instruments', 'SpeX') will return only those records that have ‘spex’ in the name.

The naming convention for publications is the first four letters of the first author’s last name and then the two digit year. So I could try db.lookup('publications', 'Kirk01') to find Kirkpatrick et al. 2001 or just db.lookup('publications', 'Kirk') to see all Kirkpatrick first author publications.

Some values might be in the ascii filename (e.g. 2m0036_2013may02_58.txt might be the object name, obs_date and wavelength_order) or in the ascii file itself above the data columns. Sometimes you might just know the correct information and can add it where possible. Otherwise you can always just leave these blank.

Adding Spectra from FITS Files

Adding spectra in FITS files works exactly the same way but with a little more automation.

Just like adding an ascii file above, you simply do:

db.add_fits('/path/to/the/fits/file.fits', source_id)

The automation comes in since many FITS files have a metadata “header” with common keys like ‘OBS_DATE’, ‘TELESCOP’, ‘INSTRUME’, etc., which BDdb automatically tries to pull out and insert into our new spectrum record.

However, you can still fill in blanks and/or override the automation by simply adding in the optional arguments as we did above with db.add_ascii().

OOPS! Don’t panic!

If anything gets entered incorrectly, remember that you can always just open up the SQLite Database Browser, navigate to the record you just added in the SPECTRA table, and double-click the cells you need to edit.

Or you can just select the row of your erroneous record and click the Delete Record button, and then try adding the spectrum again.

Setting Up Your BDNYC Astro Python Environment

Here’s a step-by-step tutorial on how to set up your Python development environment.

Distribution and Compiler

The first step is to install Anaconda. Click here, select the appropriate version for your machine and operating system, download the .dmg file, open it and double-click the installer.

Make sure that when the installer asks where to put the distribution, you choose your actual hard drive and not some sub-directory on your machine. This will make things much easier later on.

What’s nice about this distribution is that it includes common plotting, computing, and astronomy packages such as numpy and scipy, astropy, matplotlib, and many others.

Next you’ll need a C compiler. If you’re using MacOSX, I recommend using Xcode which you can download free here. (This step may take a while so go get a beverage.) Once the download is complete, run the installer.

Required Packages

For future installation of packages, I recommend using Pip. To install this, at your Terminal command line type sudo easy_install pip. Then whenever you want to install a package you might need, you just open Terminal and do pip install package_name.

Not every package is this easy (though most are). If you can’t get something through Pip just download, unzip and put the folder of your new module with the rest of your packages in the directory /anaconda/pkgs/.

Then in Terminal, navigate to that directory with cd /anaconda/pkgs/package_dir_name and do python setup.py install.

Development Tools

MacOSX comes with the text editing application TextEdit but it is not good for editing code. I strongly recommend using TextMate though it is not free so you should ask your advisor to buy a license for you! Otherwise, some folks find the free TextWrangler to be pretty good.

Next, you’ll want to get access to the BDNYC database. Detailed instructions are here on how to setup Dropbox and Github on your machine in order to interact with the database.

Launching Python

Now to use Python, in Terminal just type ipython --pylab. I recommend always launching Python this way to have the plotting library preloaded.

Enjoy!

The BDNYC Data Archive

Setting Up the Database

To install, just do:

pip install BDNYCdb

Then download the bdnyc198.db database file. This initial release contains the astrometry, photometry and spectra for the 198 objects in the Filippazzo et al. (2015) sample.

To use the published and unpublished spectra, photometry and astrometry for all 1300 sources, ask someone who has access to the private database to share the BDNYCdb Dropbox folder with you.

Accessing the Database

Create a database instance by launching the Python interpreter and pointing the get_db() function to the database file by doing:

import BDdb
db = BDdb.get_db(filepath)

Voila! You can see an inventory of all data for a specific source by passing a source_id to the inventory() method.

db.inventory(767)

This will also plot all available spectra for that source for visual inspection if you set plot=True.

Querying the Database

Now that you have the database at your fingertips, you’ll want to get some info out of it. To do this, you can pass SQL queries wrapped in double-quotes (“) to the query() method.

data = db.query( "SQL_query_goes_here" )

Here is a detailed post about how to write a SQL query.

The result of a SQL query is a sequence of tuples with the data requested from each record. For example, we can get a source’s photometry from the PHOTOMETRY table with:

db.query("select band,magnitude from photometry where source_id=202")

which gives the output

[('J', 13.526),('H', 12.807),('Ks', 12.503),('W1', 12.486),('W2', 12.386),('W3', 12.313),('W4', 8.525)]

Alternatively, we can have this data returned as a Python dictionary with:

db.query("select band,magnitude from photometry where source_id=202", DICT=True)

which looks like

[{'band': 'J', 'magnitude': 13.526},{'band': 'H', 'magnitude': 12.807},{'band': 'Ks', 'magnitude': 12.503},{'band': 'W1', 'magnitude': 12.486},{'band': 'W2', 'magnitude': 12.386},{'band': 'W3', 'magnitude': 12.313},{'band': 'W4', 'magnitude': 8.525}]

Example Queries

Some SQL query examples to pass to the query() method (wrapped in double-quotes (“) of course) are:

  1. SELECT shortname, ra, dec FROM sources WHERE (222<ra AND ra<232) AND (5<dec AND dec<15)
  2. SELECT band, magnitude, magnitude_unc FROM photometry WHERE source_id=58
  3. SELECT source_id, band, magnitude FROM photometry WHERE band=’z’ AND magnitude<15
  4. SELECT wavelength, flux, unc FROM spectra WHERE observation_id=75”

As you hopefully gathered:

  1. Returns the shortname, ra and dec of all objects in a 10 square degree patch of sky centered at RA = 227, DEC = 10
  2. Returns all the photometry and uncertainties available for object 58
  3. Returns all objects and z magnitudes with z less than 15
  4. Returns the wavelength, flux and uncertainty arrays for all spectra of object 75

The above examples are for querying individual tables only. We can query from multiple tables at the same time with the JOIN command like so:

  1. SELECT t.name, p.band, p.magnitude, p.magnitude_unc FROM telescopes as t JOIN photometry AS p ON p.telescope_id=t.id WHERE p.source_id=58
  2. SELECT p1.magnitude-p2.magnitude FROM photometry AS p1 JOIN photometry AS p2 ON p1.source_id=p2.source_id WHERE p1.band=’J’ AND p2.band=’H’
  3. SELECT src.designation, src.unum, spt.spectral_type FROM sources AS src JOIN spectral_types AS spt ON spt.source_id=src.id WHERE spt.spectral_type>=10 AND spt.spectral_type<20 AND spt.regime=’optical’
  4. SELECT s.unum, p.parallax, p.parallax_unc, p.publication_id FROM sources as s JOIN parallaxes AS p ON p.source_id=s.id

As you may have gathered:

  1. Returns the survey, band and magnitude for all photometry of source 58
  2. Returns the J-H color for every object
  3. Returns the designation, U-number and optical spectral type for all L dwarfs
  4. Returns the parallax measurements and publications for all sources

Database Schema and Browsing

In order to write the SQL queries above you of course need to know the names of the fields in each table are. One way to do this is:

db.query("PRAGMA table_info('PHOTOMETRY')")

SQL browserEven easier is to use the DB Browser for SQLite pictured at left which lets you expand and collapse each table, sort and order columns, and other fun stuff.

It even allows you to manually create/edit/destroy records with a very nice GUI.

IMPORTANT: Keep in mind that if you change a database record, you immediately change it for everyone since we share the same database file on Dropbox. Be careful!

Always check and double-check that you are entering the correct data for the correct source before you save any changes with the SQLite Database Browser.

SQL Queries

An SQL database is comprised of a bunch of tables (kind of like a spreadsheet) that have fields (column names) and records (rows of data). For example, our database might have a table called students that looks like this:

id first last grade GPA
1 Al Einstein 6 2.7
2 Annie Cannon 6 3.8
3 Artie Eddington 8 3.2
4 Carlie Herschel 8 3.2

So in our students table, the fields are [id, first, last, grade, GPA], and there are a total of four records, each with a required yet arbitrary id in the first column.

To pull these records out, we tell SQL to SELECT values for the following fields FROM a certain table. In SQL this looks like:

In [1]: db.execute("SELECT id, first, last, grade, GPA FROM students").fetchall()
Out[1]: [(1,'Al','Einstein',6,2.7),(2,'Annie','Cannon',6,3.8),(3,'Artie','Eddington',8,3.2),(4,'Carlie','Herschel',8,3.2)]

Or equivalently, we can just use a wildcard “*” if we want to return all fields with the SQL query "SELECT * FROM students".

We can modify our SQL query to change the order of fields or only return certain ones as well. For example:

In [2]: db.execute("SELECT last, first, GPA FROM students").fetchall()
Out[1]: [('Einstein','Al',2.7),('Cannon','Annie',3.8),('Eddington','Artie',3.2),('Herschel','Carlie',3.2)]

Now that we know how to get records from tables, we can restrict which records it returns with the WHERE statement:

In [3]: db.execute("SELECT last, first, GPA FROM students WHERE GPA>3.1").fetchall()
Out[3]: [('Cannon','Annie',3.8),('Eddington','Artie',3.2),('Herschel','Carlie',3.2)]

Notice the first student had a GPA less than 3.1 so he was omitted from the result.

Now let’s say we have a second table called quizzes which is a table of every quiz grade for all students that looks like this:

id student_id quiz_number score
1 1 3 89
2 2 3 96
3 3 3 94
4 4 3 92
5 1 4 78
6 3 4 88
7 4 4 91

Now if we want to see only Al’s grades, we have to JOIN the tables ON some condition. In this case, we want to tell SQL that the student_id (not the id) in the quizzes table should match the id in the students table (since only those grades are Al’s). This looks like:

In [4]: db.execute("SELECT quizzes.quiz_number, quizzes.score FROM quizzes JOIN students ON students.id=quizzes.student_id WHERE students.last='Einstein'").fetchall()
Out[4]: [(3,89),(4,78)]

So students.id=quizzes.student_id associates each quiz with a student from the students table and students.last='Einstein' specifies that we only want the grades from the student with last name Einstein.

Similarly, we can see who scored 90 or greater on which quiz with:

In [5]: db.execute("SELECT students.last, quizzes.quiz_number, quizzes.score FROM quizzes JOIN students ON students.id=quizzes.student_id WHERE quizzes.score>=90").fetchall()
Out[5]: [('Cannon',3,96),('Eddington',3,94),('Herschel',3,92),('Herschel',4,91)]

That’s it! We can JOIN as many tables as we want with as many restrictions we need to pull out data in the desired form.

This is powerful, but the queries can become lengthy. A slight shortcut is to use the AS statement to assign a table to a variable (e.g. students => s, quizzes => q) like such:

In [6]: db.execute("SELECT s.last, q.quiz_number, q.score FROM quizzes AS q JOIN students AS s ON s.id=q.student_id WHERE q.score>=90").fetchall()
Out[6]: [('Cannon',3,96),('Eddington',3,94),('Herschel',3,92),('Herschel',4,91)]