Skip to content

Commit 3a0fb2e

Browse files
authored
Adds BSNIP module (#76)
* Adds bsnip module with Stahl20 release * Fixes import errors * Adds tests for bsnip * Add doc comment about published error * Adds bsnip to docs index * Drops shebang line * Updates index page to include BSNIP * Fix typo in quick start * Adds paper tables * Fix table parsing for table a1 * Drop breakpoint * Fixes table parsing for table s1 * Adds units to a1 and s1 tables * Adds missing meta data to object tables * Fix object types for table ids * Fixes bug in parsing table IDs from CDS readmes * Update test structure for Stahl20 tests * Revert attribute rename * Update string handling in table names * Revert "Update string handling in table names" This reverts commit 065973b.
1 parent eda3b5c commit 3a0fb2e

7 files changed

Lines changed: 238 additions & 3 deletions

File tree

docs/source/getting_started/quick_start.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ For a more in depth overview, see the :ref:`SlowStart`.
3030
3131
# Check what objects are included in the data release
3232
object_ids = dr3.get_available_ids()
33-
print(obj_ids)
33+
print(object_ids)
3434
3535
# Read in the data for an object using it's Id
3636
demo_id = object_ids[0]

docs/source/index.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ check the `Server Status Page`_.
3939
+------------------------------------------------------+------------------------------+---------------+
4040
| Survey Name | Data Release | Data Type |
4141
+======================================================+==============================+===============+
42+
| Berkeley Supernova Ia Program | `Stahl et al. 2020`_ | Spectroscopic |
43+
+------------------------------------------------------+------------------------------+---------------+
4244
| | `DR1`_ | Spectroscopic |
4345
+ Carnegie Supernova Project +------------------------------+---------------+
4446
| | `DR3`_ | Photometric |
@@ -53,7 +55,7 @@ check the `Server Status Page`_.
5355
+------------------------------------------------------+------------------------------+---------------+
5456
| | `Sako et al. 2018`_ | Photometric |
5557
+ Sloan Digital Sky Survey +------------------------------+---------------+
56-
| | `Sako et al. 2018`_ | Photometric |
58+
| | `Sako et al. 2018`_ | Spectroscopic |
5759
+------------------------------------------------------+------------------------------+---------------+
5860
+ Supernova Legacy Survey | `Balland et al. 2009`_ | Spectroscopic |
5961
+------------------------------------------------------+------------------------------+---------------+
@@ -97,6 +99,7 @@ check the `Server Status Page`_.
9799
:maxdepth: 1
98100
:caption: Data Releases:
99101

102+
module_docs/bsnip
100103
module_docs/csp
101104
module_docs/des
102105
module_docs/essence

sndata/bsnip/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""The ``bsnip`` module provides access to data from the Berkeley Supernova
2+
Ia Program (BSNIP). For the photometric compliment to this survey, see the
3+
``loss`` module.
4+
"""
5+
6+
from ._stahl20 import Stahl20
7+
8+
survey_name = 'Berkeley Supernova Ia Program'
9+
survey_abbrev = 'BSNIP'

sndata/bsnip/_stahl20.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
"""This module defines the BSNIP Stahl20 API"""
2+
3+
from typing import List
4+
5+
from astropy import units as u
6+
from astropy.io import ascii
7+
from astropy.io.ascii.core import InconsistentTableError
8+
from astropy.table import Table, vstack
9+
10+
from ..base_classes import SpectroscopicRelease
11+
from ..utils import unit_conversion, downloads, data_parsing
12+
13+
14+
class Stahl20(SpectroscopicRelease):
15+
"""The second data release of the Berkeley Supernova Ia Program
16+
(BSNIP), including 637 low-redshift optical spectra collected between
17+
2009 and 2018. Targets include 626 spectra (of 242 objects) that are
18+
unambiguously classified as belonging to Type Ia supernovae (SNe Ia).
19+
Of these, 70 spectra of 30 objects are classified as spectroscopically
20+
peculiar and 79 SNe Ia (covered by 328 spectra) have complementary
21+
photometric coverage. The median SN in the data set has one epoch of
22+
spectroscopy, a redshift of 0.0208 (with a low of 0.0007 and high of
23+
0.1921), and is first observed spectroscopically 1.1 days after maximum
24+
light. (Source: Stahl et al. 2020)
25+
26+
Deviations from the standard UI:
27+
- Metadata such as object Ra, DEC, and redshifts are not included
28+
in the official data release files.
29+
- Reported error values may or may not be available depending on the
30+
particular published spectra.
31+
32+
Cuts on returned data:
33+
- None
34+
"""
35+
36+
survey_name = 'Berkeley Supernova Ia Program'
37+
survey_abbrev = 'BSNIP'
38+
release = 'Stahl20'
39+
survey_url = 'http://heracles.astro.berkeley.edu/sndb/'
40+
publications = ('Stahl et al. 2020',)
41+
ads_url = 'https://ui.adsabs.harvard.edu/abs/2020MNRAS.492.4325S/abstract'
42+
43+
def __init__(self):
44+
"""Define local and remote paths of data"""
45+
46+
super().__init__()
47+
self._spectra_dir = self._data_dir / 'spectra'
48+
self._tables_dir = self._data_dir / 'tables'
49+
self._meta_data_path = self._data_dir / 'meta_data.yml'
50+
51+
# Define urls / path for remote / local data.
52+
self._spectra_url = 'http://heracles.astro.berkeley.edu/sndb/static/BSNIPdata2/spectra.tar.gz'
53+
self._tables_url = 'https://cdsarc.cds.unistra.fr/viz-bin/nph-Cat/tar.gz?J/MNRAS/492/4325'
54+
self._tables_dir = self._data_dir / 'tables'
55+
self._meta_table_url = 'http://heracles.astro.berkeley.edu/sndb/static/BSNIPdata2/spectra.csv'
56+
self._meta_table_path = self._data_dir / 'spectra.csv'
57+
58+
def _get_available_tables(self) -> List[str]:
59+
"""Get Ids for available vizier tables published by this data release"""
60+
61+
tables = ['spectra']
62+
for file in self._tables_dir.glob('table*.dat'):
63+
table_id = file.stem[5:]
64+
if table_id.isnumeric():
65+
table_id = int(table_id)
66+
67+
tables.append(table_id)
68+
69+
return sorted(tables, key=str)
70+
71+
def _load_table(self, table_id: str) -> Table:
72+
"""Return a Vizier table published by this data release
73+
74+
Args:
75+
table_id: The published table number or table name
76+
"""
77+
78+
if table_id == 'spectra':
79+
return Table.read(self._meta_table_path)
80+
81+
readme_path = self._tables_dir / 'ReadMe'
82+
table_path = self._tables_dir / f'table{table_id}.dat'
83+
84+
# The CDS readme has an incorrect data type for the second columns in tables a1 adn s1
85+
# As a workaround, we parse the file manually
86+
if table_id == 'a1':
87+
data = Table.read(
88+
table_path,
89+
format='ascii.fixed_width_no_header',
90+
delimiter=' ',
91+
col_starts=[0, 24, 35, 44, 53, 60, 62, 68, 76, 79, 85, 91],
92+
units=[None, '"Y:M:D"', u.deg, u.deg, None, None, u.mag, None, None, u.day, u.day, None],
93+
names=['Name', 'Discov', 'RAdeg', 'DEdeg', 'z', 'r_z', 'E(B-V)',
94+
'Subtype', 'Nsp', 'fepoch', 'lepoch', 'References'])
95+
96+
elif table_id == 's1':
97+
p1nm = u.CompositeUnit(0.1, [u.nm], [1])
98+
data = Table.read(
99+
table_path,
100+
format='ascii.fixed_width_no_header',
101+
delimiter=' ',
102+
col_starts=[0, 24, 39, 45, 47, 52, 58, 63, 68, 74, 79, 84, 90],
103+
units=[None, '"Y:M:D"', u.day, None, p1nm, p1nm, p1nm, p1nm, u.deg, None, u.s, None, None],
104+
names=['Name', 'UTDate', 'tLC', 'Inst', 'lambdamin', 'lambdamax',
105+
'Resb', 'Resr', 'PA', 'Airmass', 'ExpTime', 'S/N', 'Ref'])
106+
107+
else:
108+
data = ascii.read(str(table_path), format='cds', readme=str(readme_path))
109+
110+
description_dict = data_parsing.parse_vizier_table_descriptions(readme_path)
111+
data.meta['description'] = description_dict[table_id]
112+
return data
113+
114+
def _get_available_ids(self) -> List[str]:
115+
"""Return a list of target object IDs for the current survey"""
116+
117+
obj_ids = self.load_table('spectra')['ObjName']
118+
return sorted(set(obj_ids))
119+
120+
def _get_data_for_id(self, obj_id: str, format_table: bool = True) -> Table:
121+
"""Returns data for a given object ID
122+
123+
Args:
124+
obj_id: The ID of the desired object
125+
format_table: Format for use with ``sncosmo`` (Default: True)
126+
127+
Returns:
128+
An astropy table of data for the given ID
129+
"""
130+
131+
data_tables = []
132+
all_spectra_inventory = self.load_table('spectra')
133+
object_spectra_inventory = all_spectra_inventory[all_spectra_inventory['ObjName'] == obj_id]
134+
for row in object_spectra_inventory:
135+
path = self._spectra_dir / row['Filename']
136+
137+
# Tables either have two or three columns
138+
try:
139+
table = Table.read(
140+
str(path), format='ascii',
141+
names=['wavelength', 'flux', 'fluxerr'])
142+
143+
except InconsistentTableError:
144+
table = Table.read(
145+
str(path), format='ascii',
146+
names=['wavelength', 'flux'])
147+
148+
if format_table:
149+
table['time'] = unit_conversion.convert_to_jd(row['UT_Date'], format='UT')
150+
table['instrument'] = row['Instrument']
151+
152+
data_tables.append(table)
153+
154+
meta_data = self.load_table('a1')
155+
object_meta_data = meta_data[meta_data['Name'] == obj_id][0]
156+
157+
all_data = vstack(data_tables)
158+
all_data.sort('wavelength')
159+
all_data.meta['obj_id'] = obj_id
160+
all_data.meta['ra'] = object_meta_data['RAdeg']
161+
all_data.meta['dec'] = object_meta_data['DEdeg']
162+
all_data.meta['z'] = object_meta_data['z']
163+
all_data.meta['z_err'] = None
164+
165+
# Return data with columns in a standard order
166+
return all_data
167+
168+
def _download_module_data(self, force: bool = False, timeout: float = 15):
169+
"""Download data for the current survey / data release
170+
171+
Args:
172+
force: Re-Download locally available data
173+
timeout: Seconds before timeout for individual files/archives
174+
"""
175+
176+
downloads.download_file(
177+
url=self._meta_table_url,
178+
destination=self._meta_table_path,
179+
force=force,
180+
timeout=timeout
181+
)
182+
183+
downloads.download_tar(
184+
url=self._tables_url,
185+
out_dir=self._tables_dir,
186+
skip_exists=self._tables_dir,
187+
mode='r:gz',
188+
force=force,
189+
timeout=timeout
190+
)
191+
192+
downloads.download_tar(
193+
url=self._spectra_url,
194+
out_dir=self._data_dir,
195+
skip_exists=self._spectra_dir,
196+
mode='r:gz',
197+
force=force,
198+
timeout=timeout
199+
)

sndata/utils/data_parsing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def parse_vizier_table_descriptions(readme_path: Union[Path, str]):
7878
# Iterate until end of table marker
7979
while not line.startswith('---'):
8080
line_list = line.split()
81-
table_num = line_list[0].lstrip('table').rstrip('.dat')
81+
table_num = line_list[0][len('table'):].rstrip('.dat')
8282
if table_num.isdigit():
8383
table_num = int(table_num)
8484

tests/test_bsnip/__init__.py

Whitespace-only changes.

tests/test_bsnip/test_Stahl20.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""Tests for the ``bsnip.Stahl20`` module."""
2+
3+
from unittest import TestCase
4+
5+
from sndata.bsnip import Stahl20
6+
from ..common_tests import SpectroscopicDataParsing, SpectroscopicDataUI, download_data_or_skip
7+
8+
download_data_or_skip(Stahl20())
9+
10+
11+
class Stahl20Parsing(TestCase, SpectroscopicDataParsing):
12+
"""Data parsing tests for the Stahl20 release"""
13+
14+
@classmethod
15+
def setUpClass(cls):
16+
cls.test_class = Stahl20()
17+
18+
19+
class Stahl20UI(TestCase, SpectroscopicDataUI):
20+
"""UI tests for the Stahl20 release"""
21+
22+
@classmethod
23+
def setUpClass(cls):
24+
cls.test_class = Stahl20()

0 commit comments

Comments
 (0)