-->

Etiquetas

El color de los objetos celestes: parte I

El color de los objetos celestes: parte I

En este post se inicia una serie de varios artículos dedicados al tema del color de los objetos celestes. Este primer post tendrá un carácter fundamentalmente teórico. En posteriores entradas abordaremos la realización de diagramas color-color basados en datos reales obtenidos en diferentes catálogos

Importaciones y referencias

Una interesante introducción (en inglés) puede consultarse en: Photometry y en Color-Magnitude and Color-Color plots

In [1]:
%matplotlib inline

from __future__ import division

import quantities as pq
import numpy as np
import matplotlib.pyplot as plt

# Generar un cuadro con versiones de las librerías utilizadas en este notebook
#https://github.com/jrjohansson/version_information
%load_ext version_information
%version_information numpy, matplotlib, quantities
Out[1]:
SoftwareVersion
Python2.7.9 64bit [GCC 4.4.7 20120313 (Red Hat 4.4.7-1)]
IPython2.3.1
OSLinux 3.13.0 45 generic x86_64 with debian jessie sid
numpy1.9.1
matplotlib1.4.2
quantities0.10.1
Mon Feb 23 17:59:28 2015 CET

Magnitudes aparentes

Un objeto celeste de magnitud aparente 1 es por definición 100 veces más brillante que uno de magnitud 6.

Por lo tanto, asignando provisionalmente al objeto de magnitud 6 un brillo aparente unidad, y llamando \(k\) a la razón entre una magnitud aparente de valor m y una magnitud de valor m+1, podemos razonar como se indica en el cuadro siguiente:

\[ \begin{array}{l|c|c|c|c|c|c} \text{Magnitud} & 1 & 2 & 3 & 4 & 5 & 6 \\\\ \hline \\\\ \text{Brillo aparente} & k^5 = 100 & k^4 & k^3 & k^2& k & 1 \\\\ \end{array} \]

Por lo que el valor de \(k\) será:

\[k = 100^{\frac{1}{5}}\]

In [2]:
k = 100**(1./5)
print k
2.51188643151

En realidad, la magnitud que define rigurosamente el brillo de una estrella que es percibido por un observador es el flujo radiante, definido como la cantidad de energía en una banda de longitudes de onda determinada, que se recibe en un área unidad orientada perpendicularmente a la trayectoria de la luz por unidad de tiempo. Se mide en vatios por metro cuadrado.

Representemos por \(F\) el flujo radiante recibido de una estrella, y sea \(F_{Vega}\) el flujo radiante de la estrella Vega, el cual se toma como referencia de la magnitud \(0\) en todas las longitudes de onda, tendremos:

\[ \begin{array}{l|c|c|c|c|c} \text{Magnitud} & 0 & 1 & 2 & \cdots & m \\\\ \hline \\\\ \text{Flujo} & F_{Vega} & \frac{1}{k} F_{Vega} & \frac{1}{k^2} F_{Vega} & \cdots & F = \frac{1}{k^m} F_{Vega} \\\\ \end{array} \]

Si ahora consideramos dos estrellas de magnitudes relativas \(m_1\) y \(m_2\), y sus correspondientes flujos radiantes \(F_1\) y \(F_2\), expresando ambos en función de \(F_{Vega}\), dividiendo ambas expresiones y simplificando se obtiene la relación siguiente:

\[ \frac{F_2}{F_1} = k^{m_1 - m_2} \]

Y sustituyendo \(k\) por su valor, llegamos a la fórmula fundamental siguiente:

\[ \frac{F_2}{F_1} = 100^\frac{m_1 - m_2}{5} \]

Otra forma muy habitual de expresar la relación entre magnitudes aparentes y flujos radiantes es tomando logaritmos decimales en la expresión anterior, con lo que se llega a la expresión comunmente utilizada (observese que se ha invertido el cociente entre los flujos radiantes, lo que hace aparecer el signo -):

\[ \boxed{ m_1 - m_2 = -2.5 \: log_{10} \left( \frac{F_1}{F_2} \right) }\]

Magnitudes absolutas

Lógicamente, la magnitud aparente de un objeto dependerá, tanto de su luminosidad intrínseca como de la distancia a la que se encuentra de nosotros. Por lo tanto tiene interés definir una magnitud absoluta que nos de una idea de la cantidad de energía emitida por el objeto, independientemente de la distancia a la que este se encuentre.

Previamente debemos precisar el concepto de luminosidad \(L\) de un objeto, la cual se define como la cantidad de energía total emitida por el objeto en un segundo. Esta magnitud \(L\) tiene una relación con el flujo radiante \(F\), ya que si suponemos que el objeto se encuentra a una distancia \(d\) de nosotros, la energía total radiada por segundo se repartirá en una superficie esférica de área \(4 \pi d^2\), por lo que la energía que recibiremos por unidad de superficie en un segundo será:

\[F = \frac{L}{4 \pi d^2}\]

(En realidad la expresión anterior es solo una aproximación, ya que una parte de la energía luminosa radiada es absorbida o dispersada en mayor o menor medida por el polvo y gas existente en el medio interestelar).

Definamos ahora la magnitud absoluta \(M\) de un objeto como la magnitud relativa que tendría dicho objeto si se encontrara a una distancia de 10 parsec (10 pc).

Partiendo de las fórmulas anteriores, vamos a deducir una expresión para obtener la diferencia \(m-M\) entre la magnitud aparente y la magnitud absoluta de un objeto. Representemos por \(F\) el flujo radiante que realmente medimos, y por \(F_{10}\) el flujo radiante que mediríamos si el objeto se encontrara a 10 pc de nosotros.

Que suele escribirse como:

\[ \boxed{ m-M = 5 \: log_{10} \left( \frac{d}{10} \right) } \]

Donde la distancia \(d\) vendrá expresada en parsec.

Esta sencilla expresión permite en principio calcular la magnitud absoluta a partir de la magnitud aparente, o a la inversa, pero la distancia que nos separa del objeto forma como es natural parte de la ecuación. De hecho, si conocemos el valor de \(m-M\) nos será posible conocer también la distancia d. Por ello, la magnitud \(m-M\) puede ser vista como una forma de expresar la distancia que nos separa del astro en una escala logarítmica, y por esta razón se la conoce como módulo de distancia

Ejemplo: la estrella Vega se encuentra a una distancia de 8.1 pc. ¿Cual es su magnitud absoluta y módulo de distancia?

In [3]:
m = 0 # Vega es la referencia para la magnitud aparente 0

M = -5 * np.log10(8.1 / 10)
print "M(Vega) = %.2f" %M
print u"módulo de distancia de Vega: %.2f" %(0 - M)
M(Vega) = 0.46
módulo de distancia de Vega: -0.46

Magnitudes aparentes en un sistema de filtros de color

En la práctica, la magnitud aparente de un objeto se puede medir considerando la radiación recibida en todas las longitudes de onda, en cuyo caso recibe el nombre de magnitud bolométrica, o bien, haciendo pasar la luz por filtros de diferentes colores. Un sistema de filtros utilizado de forma habitual para este fin recibe el nombre de sistema fotométrico UBV, o sistema fotométrico de Johnson, el cual está formado por tres filtros con las siguientes características:

\[ \begin{array}{|l|c|c|} \text{Filtro} & \lambda \: \text{(Angstroms)} & \Delta \lambda \: \text{(Ancho de banda)}\\\\ \hline \\\\ \text{Ultraviolet (u)} & 3650 & 680 \\\\ \text{Blue (b)} & 4400 & 980 \\\\ \text{Visible (v)} & 5500 & 890 \\\\ \end{array} \]

El filtro u deja pasar la luz en la región del ultravioleta, el filtro b en la región del azul, y el filtro v en la región del color verde.

Las magnitudes aparentes medidas con estos filtros se representan mediante las iniciales \(U, B, V\) (adviertase que, aun tratandose de magnitudes aparentes, en este caso se emplean letras mayúsculas).

Índices de color

La magnitud aparente de un cuerpo, tomada aisladamente no es un buen indicador de las propiedades de dicho objeto. Imaginemos que queremos averiguar algo sobre el color de la luz emitida por una estrella y, haciendola pasar por un filtro azul medimos una magnitud aparente debil (es decir, un valor de m elevado). No podremos saber si esto es debido a que la estrella radia poca energía en ese rango de longitudes de onda o bien simplemente se trata de una estrella muy lejana. En definitiva, para saber el color de una estrella debemos comparar su magnitud aparente medida a través de al menos dos filtros.

Por este motivo se definen los índices de color, como la diferencia entre dos magnitudes aparentes con dos filtros diferentes. Así por ejemplo, en el sistema fotométrico UBV tendremos dos índices de color:

  • Índice de color \(U-B\) : Diferencia entre las magnitudes aparentes ultravioleta y azul
  • Índice de color \(B-V\) : Diferencia entre las magnitudes aparentes azul y visible

Téngase en cuenta que, de acuerdo con la definición anterior de módulo de distancia, la relación entre magnitudes aparentes y magnitudes absolutas sse puede escribir como:

\[ m = M + \mu \]

Siendo \(\mu\) el módulo de distancia del objeto. Por lo tanto:

índice de color = \(m_1 - m_2 = M_1 - M_2 \)

Es decir, ¡un índice de color es independiente de la distancia a la que se encuentre el objeto!

Relación entre índice de color y temperatura

Una razón por la que los índices de color de los objetos astronómicos tienen tanta importancia es por su estrecha relación con la temperatura superficial del objeto. Las estrellas radian siguiendo un espectro que en una primera aproximación se aproxima al espectro de radiación de un cuerpo negro, el cual depende exclusivamente de la temperatura. En un anterior post se mostraba como utilizar la función de Plank para calcular la radiación que en cada longitud de onda y para una temperatura dada emite un cuerpo negro. Vamos a volver a utilizar esta función:

In [4]:
def B(wl,T):
    '''wl es un array de longitudes de onda con unidades de longitud
    T es una temperatura expresada en Kelvin
    el resultado es un array de valores de la radiancia espectral
    con unidades W/(m**2 * nm * sr)
    '''
    I = 2 * pq.constants.h * (pq.c)**2 / wl**5 *  \
        1 / (np.exp((pq.constants.h*pq.c \
        / (wl*pq.constants.k*T)).simplified)-1)
    return I.rescale(pq.watt/(pq.m**2 * pq.nm *pq.sr))

Me propongo ahora mostrar, suponiendo que las estrellas fueran cuerpos negros perfectos, cual sería la variación del índice de color en función de la temperatura. Para ello voy a seguir la aproximación al cálculo de los índices de color a partir de la función de Plank que se expone en el libro An Introduction to Modern Astrophysics de Bradley W. Carroll y Dale A. Ostlie, sección 3.6. Consiste en utilizar las aproximaciones siguientes:

\[ U-B = -2.5 \: log_{10} \left( \frac{B(\lambda_u, T) \: \Delta \lambda_u}{B(\lambda_b, T) \: \Delta \lambda_b} \right) + C_{U-B} \]

\[ B-V = -2.5 \: log_{10} \left( \frac{B(\lambda_b, T) \: \Delta \lambda_b}{B(\lambda_v, T) \: \Delta \lambda_v} \right) + C_{B-V} \]

Es decir, en la expresión ya vista anteriormente entre magnitudes y flujos radiantes, se trata de sustituir cada flujo radiante por el producto de la radiancia espectral dada por la función de Plank para la longitud de onda central de cada filtro, multiplicado por el ancho de banda efectivo de dicho filtro, y sumando una constante de ajuste.

Las constantes de ajuste \(C_{U-B}\) y \(C_{B-V}\) se van a determinar utilizando unos ciertos valores de \(T\) y de los índices de color que se utilizan para calibrar los filtros. En este artículo de la wikipedia se incluye una tabla con estos valores. Vamos a utilizar en particular que para una temperatura \(T = 42000K\) debemos obtener valores de los índices de color U-B = -1.19, B-V = -0.33

In [5]:
# Definición de las constantes del sistema fotométrico UBV

lambda_u = 365 * pq.nm
delta_u = 68 * pq.nm
lambda_b = 440 * pq.nm
delta_b = 98 * pq.nm
lambda_v = 550 * pq.nm
delta_v = 89 * pq.nm
In [6]:
# Cálculo de Cu-b

T = 42000*pq.kelvin
F = B(lambda_u, T) * delta_u/(B(lambda_b, T)* delta_b)
Cub = -1.19 + 2.5 * np.log10(F)
print Cub
-0.874317278235 dimensionless

In [7]:
# Cálculo de Cb-v

F = B(lambda_b, T) * delta_b/(B(lambda_v, T)* delta_v)
Cbv = -0.33 + 2.5 * np.log10(F)
print Cbv
0.649368425063 dimensionless

Habiendo determinado las constantes en la fórmula que aproxima los índices de color a partir de la temperatura para un cuerpo negro, podemos definir sendas funciones que nos permitirán calcular dichos índices de color para diferentes temperaturas.

In [8]:
def get_UB(T):
    F = B(lambda_u, T) * delta_u/(B(lambda_b, T)* delta_b)
    return -2.5 * np.log10(F) + Cub

def get_BV(T):
    F = B(lambda_b, T) * delta_b/(B(lambda_v, T)* delta_v)
    return -2.5 * np.log10(F) + Cbv

Y finalmente vamos a representar en un gráfico la curva que relaciona la temperatura superficial del objeto con su índice de color, suponiendo, tengamoslo siempre presente, que dicho objeto fuera un cuerpo negro perfecto. Para los objetos astronómicos reales esta curva será solo una aproximación que nos permitirá una primera estimaciión de su temperatura superficial en función del índice de color.

Precisamente, para ilustrar este último hecho, representaré en el mismo gráfico tres estrellas representativas, con datos de sus temperaturas efectivas superficiales e índice de color

In [9]:
temp = np.arange(2500, 30000, 100)*pq.Kelvin
BmenosV = get_BV(temp)

fig, ax = plt.subplots(figsize=(10, 8))
ax.plot(BmenosV, temp, lw =2)
ax.grid()
ax.set_title(u"Relación Índice de color - Temperatura \n \
     para un cuerpo negro")
ax.title.set_fontsize(18)
ax.set_xlabel(u"Índice de color B-V")
ax.xaxis.label.set_fontsize(15)
ax.set_ylabel("Temperatura superficial en K")
ax.yaxis.label.set_fontsize(15)

# Dibuja tres estrellas representativas

T = np.array([3300, 5800, 22000])*pq.kelvin
BV = [1.85, 0.656, -0.21]
             
ax.scatter(BV[0],T[0], s=600, c='r', marker='*')
ax.scatter(BV[1],T[1], s=600, c='y', marker='*')
ax.scatter(BV[2],T[2], s=600, c='b', marker='*')

# Anotamos los nombres de las tres estrellas

ax.annotate("Betelgeuse", xy=(BV[0],T[0]+1000*pq.kelvin), size=14)
ax.annotate("Sol", xy=(BV[1],T[1]+1000*pq.kelvin), size=14)
ax.annotate("Bellatrix", xy=(BV[2],T[2]+1000*pq.kelvin), size=14);

En la figura anterior se observa que la curva teórica anterior puede proporcionar en muchos casos una aproximación razonable para obtener las temperaturas efectivas superficiales de las estrellas a partir de su índice de color.

Diagramas color-color

Otro tipo de diagramas que se emplean a menudo son los diagramas color-color en los que se representa en cada eje un índice de color diferente, por ejemplo: U-B sobre B-V. El hecho es que la mayoría de las estrellas se ubican en un gráfico color-color a lo largo de una banda bien definida conocida como "locus estelar" Estos diagramas son útiles para por ejemplo detectar objetos atípicos que en un diagrama color-color se situan fuera del locus

En otras entradas mas adelante generaremos diagramas color-color con datos reales, pero en este momento vamos a preguntarnos que aspecto tendría el diagrama color-color de un cuerpo negro ideal. La respuesta es, como vamos a ver, que se trataría de ¡una línea recta!. El locus estelar de los diagramas "color-color" de objetos reales se situará en alguna medida alrededor de esta recta.

Otra conclusión que podremos extraer del siguiente gráfico es que los objetos más calientes se situan en la esquina inferior izquierda del gráfico, y los más frios en el extremo opuesto.

In [10]:
fig, ax = plt.subplots(figsize=(10, 8))

temp = np.arange(2500,30000,100)*pq.kelvin
BmenosV = get_BV(temp)
UmenosB = get_UB(temp)

ax.scatter(BmenosV, UmenosB)
ax.grid()
ax.set_title(u'Gráfico color-color de los cuerpos negros')
ax.title.set_fontsize(20)
ax.set_xlabel('B-V')
ax.xaxis.label.set_fontsize(15)
ax.set_ylabel('U-B')
ax.yaxis.label.set_fontsize(15)

temp = np.array([3000, 4000, 5000, 7500, 10000, 20000, 30000])*pq.kelvin
BmenosV = get_BV(temp)
UmenosB = get_UB(temp)

ax.scatter(BmenosV, UmenosB, s=150., c='r')

for t in temp:
    ax.annotate("%d K" %t,xy=(get_BV(t)+0.1,get_UB(t)-0.05))

An easy way to make SQL queries from Python to the SDSS database

An easy way to make SQL queries from Python to the SDSS database

Author: Eduardo Martín Calleja

In this entry we will see a very simple method of executing SQL queries on the Sloan Digital Sky Survey (SDSS) database. In this way we can get a lot of data about all kinds of celestial objects and load them into Python data structures, like Pandas dataframes, for later process or plotting.

This post has been written entirely using the IPython Notebook. I will also use the Python module "mechanize" to surf the web and run interactively HTML forms. I will explain in detail each of the steps, and at the end I will summarize to avoid being lost in the details and show the usability of the proposed method.

Imports and references

  • For an introduction to the execution of SQL queries on the SDSS database I can not think of a better resource than their own tutorial, together with its complement of examples: Sample SQL Queries

  • To view the tables and views that exist in the database, and what information is available in each of them, you can use: Schema Browser

  • And to run interactively a SQL on the SDSS database, you can access the page: SQL Search

  • The mechanize home page

  • A nice notebook using the method in this post to query the SDSS database: here

In [1]:
%matplotlib inline

from __future__ import division

import numpy as np
import pandas as pd
import mechanize
from StringIO import StringIO # To read a string like a file

# This IPython magic generates a table with version information
#https://github.com/jrjohansson/version_information
%load_ext version_information
%version_information numpy, pandas, StringIO
Out[1]:
SoftwareVersion
Python2.7.9 64bit [GCC 4.4.7 20120313 (Red Hat 4.4.7-1)]
IPython2.3.1
OSLinux 3.13.0 45 generic x86_64 with debian jessie sid
numpy1.9.1
pandas0.15.2
StringIOStringIO
Sat Feb 21 12:08:20 2015 CET
In [2]:
# URL to the SDSS SQL Search DR10
url = "http://skyserver.sdss3.org/dr10/en/tools/search/sql.aspx"

SQL preparation

Before we begin to interact from Python with the web page of the SDSS that allows us to send a SQL statement to the database, I will prepare a test SQL query on a string variable. In this SQL we will find 10 objects (this is a test!) of type 6 = 'STAR', with clean photometry and blue color in a certain region of sky. But, be careful to avoid any comments (those preceded by a double dash -) when you create the string.

In [3]:
s = 'SELECT TOP 10                         \
    objID, ra, dec, modelMag_u, modelMag_g \
FROM                                       \
    PhotoPrimary                           \
WHERE                                      \
    ra BETWEEN 140 and 141                 \
AND dec BETWEEN 20 and 21                  \
AND type = 6                               \
AND clean = 1                              \
AND modelMag_u - modelMag_g < 0.5'

Web surfing with the Python mechanize module

The first step in using mechanize will be to create a Browser-like object to be able to navigate using its methods

In [4]:
br = mechanize.Browser()

Then we must open a session using the url defined above, pointing to the SDSS web page that allows us to make the SQL queries:

In [5]:
resp = br.open(url)
In [6]:
resp.info()
Out[6]:
<httplib.HTTPMessage instance at 0x7f4ba6cdd950>

When you want to interact with a web page, you will be interested to know the HTML forms contained in it. An HTML form is a section of the document between the tags: FORM and /FORM

A HTML form contains a series of special objects called controls such as checkboxes, radio buttons, menus, etc. and labels of these objects. The user interacts with the page by modifying a control, for example by selecting an option, introducing a text in a field, etc. and sending this modified form back to the server.

Each HTML form on the page has a name, although this can in some cases be empty. To get a list of the names of the forms in the page we can write:

In [7]:
for f in br.forms():
    print f.name
sql

That is, in this case there is only one form on the page, named "sql"

At the same time, each form has a list of controls that also have a name, which can also be left blank. To list the forms on the page, along with their controls, and each control type, we can do the following:

In [8]:
for f in br.forms():
    print f.name
    for c in f.controls:
        print '\t',c.name, '\t', c.type
sql
 clear  button
 cmd  textarea
 None  submit
 syntax  checkbox
 format  radio
 reset  reset

We will focus on the "cmd" control which is the text area in which we write our SQL, and the "format" control, which, as you can see on the web page, is used to control the type of output desired: HTML, XML, CSV, etc.. To access these controls you must previously select the form to which they belong:

In [9]:
br.select_form(name="sql")

Then we will modify the control 'cmd' to enter our SQL, and the 'format' control, to select the output in csv format.

In [10]:
br['cmd'] = s  # This is the string with the sql query
br['format']=['csv'] # data output format
response = br.submit()

We can get a string with the contents of the answer using the get_data() method:

In [11]:
print response.get_data()
#Table1
objID,ra,dec,modelMag_u,modelMag_g
1237667293189833237,140.000264887516,20.3528168302492,23.27699,23.07312
1237667293189833455,140.001621936922,20.4298007848266,23.79515,24.28631
1237667293189832757,140.003651871714,20.2546305174986,24.7036,24.82169
1237667430093553674,140.004909940259,20.1719995965921,24.04017,24.81654
1237667430093488938,140.009035667549,20.0174593142685,23.4923,23.47844
1237667430093488288,140.010686497758,20.0781480229235,19.89794,19.64356
1237667209974448712,140.011074894392,20.9763084088396,22.70029,22.85815
1237667430093554008,140.012379856521,20.1469377004989,23.12733,22.86493
1237667209974448933,140.013140656439,20.9086701836405,23.96772,24.16748
1237667430093488949,140.013238349062,20.0312906376057,24.73975,25.34514


But attention!, The submit() method closes the session, so, to send another SQL query you must first repeat the br.open() and br.select() calls.

Then, and in order to be able to process the data more easily, the most advisable could be to generate a Python Pandas dataframe. We can see that the first line should be discarded, while the second row contains the names of the columns, so we will keep it in the dataframe:

In [12]:
file_like = StringIO(response.get_data())
df =pd.read_csv(file_like, skiprows = 1) # skip the first row
df
Out[12]:
objID ra dec modelMag_u modelMag_g
0 1237667293189833237 140.000265 20.352817 23.27699 23.07312
1 1237667293189833455 140.001622 20.429801 23.79515 24.28631
2 1237667293189832757 140.003652 20.254631 24.70360 24.82169
3 1237667430093553674 140.004910 20.172000 24.04017 24.81654
4 1237667430093488938 140.009036 20.017459 23.49230 23.47844
5 1237667430093488288 140.010686 20.078148 19.89794 19.64356
6 1237667209974448712 140.011075 20.976308 22.70029 22.85815
7 1237667430093554008 140.012380 20.146938 23.12733 22.86493
8 1237667209974448933 140.013141 20.908670 23.96772 24.16748
9 1237667430093488949 140.013238 20.031291 24.73975 25.34514

From here we could do such things like rename the columns giving them names more to our liking, and calculate a new column as the difference of the columns u and g, which will indicate the color of the star (more on that in another post).

In [13]:
df.columns = ['objID','ra','dec','u','g']
df['u-g'] = df['u']-df['g']
df
Out[13]:
objID ra dec u g u-g
0 1237667293189833237 140.000265 20.352817 23.27699 23.07312 0.20387
1 1237667293189833455 140.001622 20.429801 23.79515 24.28631 -0.49116
2 1237667293189832757 140.003652 20.254631 24.70360 24.82169 -0.11809
3 1237667430093553674 140.004910 20.172000 24.04017 24.81654 -0.77637
4 1237667430093488938 140.009036 20.017459 23.49230 23.47844 0.01386
5 1237667430093488288 140.010686 20.078148 19.89794 19.64356 0.25438
6 1237667209974448712 140.011075 20.976308 22.70029 22.85815 -0.15786
7 1237667430093554008 140.012380 20.146938 23.12733 22.86493 0.26240
8 1237667209974448933 140.013141 20.908670 23.96772 24.16748 -0.19976
9 1237667430093488949 140.013238 20.031291 24.73975 25.34514 -0.60539

Summary

Once we have seen the rationale of the use of the mechanize module and the creation of a Pandas dataframe, we can define a function to streamline the process for new SQL queries:

In [14]:
def SDSS_select(sql):
    '''input: string with a valid SQL query
    output: a Pandas dataframe
    '''
    br.open(url)
    br.select_form(name="sql")
    br['cmd'] = sql
    br['format']=['csv']
    response = br.submit()
    file_like = StringIO(response.get_data())
    return pd.read_csv(file_like,  skiprows=1)

The steps are as follows:

  • The following instructions will be executed only the first time:
In [15]:
# URL a SQL Search DR10
url = "http://skyserver.sdss3.org/dr10/en/tools/search/sql.aspx"
  • We prepare a string with our SQL (you can test it first on the web page, as there's no exception handling in the above function)
In [16]:
sql = 'SELECT TOP 10    \
objID, ra, dec, modelMag_u,modelMag_g,modelMag_r,modelMag_i,modelMag_z \
FROM  Star               \
WHERE ra BETWEEN 150 and 152 AND dec BETWEEN 30 and 31 AND clean = 1'
  • We make a call to the function, obtaining a Pandas dataframe in return
In [17]:
df = SDSS_select(sql)

And we already have our dataframe ready!

In [18]:
df
Out[18]:
objID ra dec modelMag_u modelMag_g modelMag_r modelMag_i modelMag_z
0 1237664869216289142 150.000355 30.732103 23.06350 22.79704 21.60797 21.22958 21.32821
1 1237665099003593049 150.000483 30.591703 22.71077 22.14142 21.63762 21.84505 21.44720
2 1237665098466656334 150.000711 30.105872 20.72021 18.12753 16.79675 16.08280 15.69484
3 1237665098466656330 150.000754 30.097463 22.66031 19.98154 18.61721 17.61466 17.07129
4 1237664869216289439 150.000985 30.748886 25.00283 23.08974 22.20838 21.99796 21.30983
5 1237664869216288905 150.001134 30.829336 21.41318 20.29182 19.80290 19.55658 19.49337
6 1237665129067840138 150.001698 30.483674 23.61431 22.05697 20.63076 20.13519 19.81597
7 1237665098466657196 150.002180 30.248386 25.05051 23.19651 21.70999 20.04427 19.07293
8 1237665129067840349 150.002215 30.429241 23.51003 24.00885 21.71184 21.25962 20.59406
9 1237665098466656342 150.002250 30.174147 20.84485 18.57332 17.56038 17.15493 16.93803

Here ends this post, but the access method to the SDSS database we have seen here, will be used systematically in future posts, to analyze with Python, based on real data, various properties of celestial objects.

Como acceder con SQL a la base de datos del SDSS desde Python

En esta entrada vamos a ver un método muy sencillo de ejecutar sentencias SQL sobre el servidor del Sloan Digital Sky Survey (SDSS). De esta manera podremos obtener una gran cantidad de datos acerca de todo tipo de objetos celestes.

Este post está escrito íntegramente utilizando el Notebook de IPython. También se utilizará el módulo mechanize de Python para navegar por la web y ejecutar de forma interactiva formularios HTML. Iré explicando en detalle cada uno de los pasos, y al final haré un resumen para evitar que nos perdamos en los detalles y mostrar la facilidad de utilización del método utilizado.

Importaciones y referencias

Para una introducción a la ejecución de peticiones SQL sobre el SDSS no se me ocurre mejor recurso que su propio tutorial, junto con su complemento de ejemplos: Sample SQL Queries

Para ver que tablas y vistas existen en la base de datos, y qué información hay disponible en cada una de ellas, una herramienta imprescindible será: Schema Browser

Y, para ejecutar de forma interactiva un SQL contra la base de datos del SDSS, accedase a la página: SQL Search

In [1]:
%matplotlib inline

from __future__ import division

import numpy as np
import pandas as pd
import mechanize
from StringIO import StringIO # Para leer un string como si fuera un fichero

# Generar un cuadro con versiones de las librerías utilizadas en este notebook
#https://github.com/jrjohansson/version_information
%load_ext version_information
%version_information numpy, pandas, StringIO
Out[1]:
SoftwareVersion
Python2.7.9 64bit [GCC 4.4.7 20120313 (Red Hat 4.4.7-1)]
IPython2.3.1
OSLinux 3.13.0 44 generic x86_64 with debian jessie sid
numpy1.9.1
pandas0.15.2
StringIOStringIO
Fri Jan 23 12:06:07 2015 CET
In [2]:
# URL de la página de búsquedas de la base de datos DR10
url = "http://skyserver.sdss3.org/dr10/en/tools/search/sql.aspx"

Preparación del SQL

Antes de comenzar a interactuar desde Python con la página web del SDSS que nos permite enviar una sentencia SQL a su base de datos, vamos a preparar un SQL de prueba en una variable de tipo string. En este SQL buscaremos 10 objetos (¡se trata de una prueba!) de tipo 6 = 'STAR', con fotometría límpia y color azul, en una región del cielo determinada. Eso si, debemos omitir las líneas con comentarios en el SQL (aquellas precedidas de doble guión --) al crear el fichero.

Nota: En una próxima entrada en este blog, se verá cuales son las tablas (o vistas) y campos más útiles para construir nuestras SELECT.

In [3]:
# Como el string a generar ocupa más de una línea
#Utilizamos paréntesis para concatenar automáticamente las líneas

s = ('SELECT TOP 10 '
    'objID, ra, dec, modelMag_u, modelMag_g '
'FROM '
    'PhotoPrimary '
'WHERE '
    'ra BETWEEN 140 and 141 '
    'AND dec BETWEEN 20 and 21 '
    'AND type = 6 '
    'AND clean = 1 '
    'AND modelMag_u - modelMag_g < 0.5')
In [4]:
# Comprobación:
s
Out[4]:
'SELECT TOP 10 objID, ra, dec, modelMag_u, modelMag_g FROM PhotoPrimary WHERE ra BETWEEN 140 and 141 AND dec BETWEEN 20 and 21 AND type = 6 AND clean = 1 AND modelMag_u - modelMag_g < 0.5'

El primer paso para utilizar mechanize será crear un objeto tipo Browser para poder hacer la navegación utilizando sus métodos

In [5]:
br = mechanize.Browser()

A continuación, debemos abrir una sesión con el url que apunta a la página web del SDSS que nos permite hacer las búsquedas SQL:

In [6]:
resp = br.open(url)
In [7]:
resp.info()
Out[7]:
<httplib.HTTPMessage instance at 0x7f3d3bc3d2d8>

Cuando se desea interactuar con una página web, estaremos interesados en conocer los formularios HTML que contiene. Un formulario HTML es una sección del documento comprendida entre las etiquetas:

<FORM> y </FORM>

Un formulario contiene una serie de objetos especiales denominados controles, tales como checkboxes, radio buttons, menus, etc. y labels de estos objetos. El usuario interactua con la página modificando un control, por ejemplo seleccionando una opción, introduciendo un texto en un campo, etc. y haciendo un envío de dicho form al servidor.

Cada form en la página tiene un nombre, si bien este puede en algunos casos estar vacío. Para listar estos nombres de los formularios existentes en la página podemos escribir:

In [8]:
for f in br.forms():
    print f.name
sql

Es decir, en este caso hay un único form en la página, de nombre "sql"

A su vez, cada formulario posee una lista de controles, que tambien poseen un nombre, el cual también puede estar en blanco. Para listar los formularios en la página, junto con sus controles y tipo de cada control, podemos hacer lo siguiente:

In [9]:
for f in br.forms():
    print f.name
    for c in f.controls:
        print '\t',c.name, '\t', c.type
sql
 clear  button
 cmd  textarea
 None  submit
 syntax  checkbox
 format  radio
 reset  reset

Centraremos nuestra atención en el control "cmd" que es un control de texto en el cual debemos escribir nuestro SQL, y el control "format" que, tal como se puede ver en la página web en el navegador, permite controlar el tipo de salida deseada: HTML, XML, CSV, etc. Para poder acceder a estos controles debemos seleccionar previamente el formulario al cual pertenecen:

In [10]:
br.select_form(name="sql")

A continuación, modificaremos el control 'cmd' para introducir nuestro SQL, y el control 'format', para seleccionar la salida en formato csv.

In [11]:
br['cmd'] = s    # El string con el sql
br['format']=['csv'] # formato de salida de los datos
response = br.submit()

El contenido del fichero csv con la respuesta del sql podemos obtenerlo como un string con el método get_data()

In [12]:
print response.get_data()
#Table1
objID,ra,dec,modelMag_u,modelMag_g
1237667293189833237,140.000264887516,20.3528168302492,23.27699,23.07312
1237667293189833455,140.001621936922,20.4298007848266,23.79515,24.28631
1237667293189832757,140.003651871714,20.2546305174986,24.7036,24.82169
1237667430093553674,140.004909940259,20.1719995965921,24.04017,24.81654
1237667430093488938,140.009035667549,20.0174593142685,23.4923,23.47844
1237667430093488288,140.010686497758,20.0781480229235,19.89794,19.64356
1237667209974448712,140.011074894392,20.9763084088396,22.70029,22.85815
1237667430093554008,140.012379856521,20.1469377004989,23.12733,22.86493
1237667209974448933,140.013140656439,20.9086701836405,23.96772,24.16748
1237667430093488949,140.013238349062,20.0312906376057,24.73975,25.34514


Pero, ¡atención!, el método submit() cierra la sesión, por lo que, para enviar otro SQL habrá que repetir el br.open() y el br.select()

A continuación, y con el fin de poder tratar los datos con más facilidad, lo más aconsejable es generar un dataframe de Pandas. Observemos que la primera línea debe ser descartada, y la segunda fila contiene los nombres de las columnas, por lo que la mantendremos en el dataframe

In [13]:
file_like = StringIO(response.get_data())
df =pd.read_csv(file_like, skiprows = 1) # saltamos la primera línea
df
Out[13]:
objID ra dec modelMag_u modelMag_g
0 1237667293189833237 140.000265 20.352817 23.27699 23.07312
1 1237667293189833455 140.001622 20.429801 23.79515 24.28631
2 1237667293189832757 140.003652 20.254631 24.70360 24.82169
3 1237667430093553674 140.004910 20.172000 24.04017 24.81654
4 1237667430093488938 140.009036 20.017459 23.49230 23.47844
5 1237667430093488288 140.010686 20.078148 19.89794 19.64356
6 1237667209974448712 140.011075 20.976308 22.70029 22.85815
7 1237667430093554008 140.012380 20.146938 23.12733 22.86493
8 1237667209974448933 140.013141 20.908670 23.96772 24.16748
9 1237667430093488949 140.013238 20.031291 24.73975 25.34514

A partir de aquí podríamos renombrar las columnas dandoles un nombre más a nuestro gusto, y calcular una nueva columna como diferencia de las columnas u y g, la cual nos indicará el color del astro.

In [14]:
df.columns = ['objID','ra','dec','u','g']
df['u-g'] = df['u']-df['g']
df
Out[14]:
objID ra dec u g u-g
0 1237667293189833237 140.000265 20.352817 23.27699 23.07312 0.20387
1 1237667293189833455 140.001622 20.429801 23.79515 24.28631 -0.49116
2 1237667293189832757 140.003652 20.254631 24.70360 24.82169 -0.11809
3 1237667430093553674 140.004910 20.172000 24.04017 24.81654 -0.77637
4 1237667430093488938 140.009036 20.017459 23.49230 23.47844 0.01386
5 1237667430093488288 140.010686 20.078148 19.89794 19.64356 0.25438
6 1237667209974448712 140.011075 20.976308 22.70029 22.85815 -0.15786
7 1237667430093554008 140.012380 20.146938 23.12733 22.86493 0.26240
8 1237667209974448933 140.013141 20.908670 23.96772 24.16748 -0.19976
9 1237667430093488949 140.013238 20.031291 24.73975 25.34514 -0.60539

Resumen

Una vez visto el fundamento de la utilización del módulo mechanize y la creación de un dataframe de Pandas, podemos definir una función para agilizar el proceso de nuevos SQL

In [15]:
def SDSS_select(sql):
    '''input: string con una sentencia SQL válida
    output: un dataframe de Pandas
    '''
    br.open(url)
    br.select_form(name="sql")
    br['cmd'] = sql
    br['format']=['csv']
    response = br.submit()
    file_like = StringIO(response.get_data())
    return pd.read_csv(file_like,  skiprows=1)

Los pasos a seguir serán los siguientes:

  • Las instrucciones siguientes se ejecutarán solo la primera vez:
In [16]:
# URL de la página de búsquedas de la base de datos DR10
url = "http://skyserver.sdss3.org/dr10/en/tools/search/sql.aspx"
  • Preparamos un string con nuestro SQL
In [17]:
sql = ('SELECT TOP 10 '
'objID, ra, dec, modelMag_u, modelMag_g, modelMag_r, modelMag_i, modelMag_z '
'FROM  Star '
'WHERE ra BETWEEN 150 and 152 AND dec BETWEEN 30 and 31 AND clean = 1')
  • Llamamos a la función, obteniendo un dataframe de Pandas como retorno
In [18]:
df = SDSS_select(sql)

Y ya tenemos listo nuestro dataframe

In [19]:
df
Out[19]:
objID ra dec modelMag_u modelMag_g modelMag_r modelMag_i modelMag_z
0 1237664869216289142 150.000355 30.732103 23.06350 22.79704 21.60797 21.22958 21.32821
1 1237665099003593049 150.000483 30.591703 22.71077 22.14142 21.63762 21.84505 21.44720
2 1237665098466656334 150.000711 30.105872 20.72021 18.12753 16.79675 16.08280 15.69484
3 1237665098466656330 150.000754 30.097463 22.66031 19.98154 18.61721 17.61466 17.07129
4 1237664869216289439 150.000985 30.748886 25.00283 23.08974 22.20838 21.99796 21.30983
5 1237664869216288905 150.001134 30.829336 21.41318 20.29182 19.80290 19.55658 19.49337
6 1237665129067840138 150.001698 30.483674 23.61431 22.05697 20.63076 20.13519 19.81597
7 1237665098466657196 150.002180 30.248386 25.05051 23.19651 21.70999 20.04427 19.07293
8 1237665129067840349 150.002215 30.429241 23.51003 24.00885 21.71184 21.25962 20.59406
9 1237665098466656342 150.002250 30.174147 20.84485 18.57332 17.56038 17.15493 16.93803

Aquí finalizo este post, pero el método de acceso a la base de datos del SDSS que hemos visto aquí lo utilizaré en futuras entradas para analizar, en base a datos reales, diversas propiedades de los objetos celestes.