astro-otter 0.2.0__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of astro-otter might be problematic. Click here for more details.

Files changed (30) hide show
  1. {astro_otter-0.2.0/src/astro_otter.egg-info → astro_otter-0.3.2}/PKG-INFO +5 -9
  2. {astro_otter-0.2.0 → astro_otter-0.3.2}/README.md +2 -7
  3. {astro_otter-0.2.0 → astro_otter-0.3.2}/pyproject.toml +2 -1
  4. {astro_otter-0.2.0 → astro_otter-0.3.2/src/astro_otter.egg-info}/PKG-INFO +5 -9
  5. {astro_otter-0.2.0 → astro_otter-0.3.2}/src/astro_otter.egg-info/requires.txt +1 -0
  6. {astro_otter-0.2.0 → astro_otter-0.3.2}/src/otter/_version.py +1 -1
  7. {astro_otter-0.2.0 → astro_otter-0.3.2}/src/otter/io/otter.py +96 -24
  8. {astro_otter-0.2.0 → astro_otter-0.3.2}/src/otter/io/transient.py +253 -66
  9. {astro_otter-0.2.0 → astro_otter-0.3.2}/src/otter/schema.py +7 -1
  10. {astro_otter-0.2.0 → astro_otter-0.3.2}/src/otter/util.py +27 -1
  11. {astro_otter-0.2.0 → astro_otter-0.3.2}/tests/test_data_finder.py +6 -4
  12. {astro_otter-0.2.0 → astro_otter-0.3.2}/tests/test_otter.py +10 -104
  13. {astro_otter-0.2.0 → astro_otter-0.3.2}/tests/test_transient.py +1 -1
  14. {astro_otter-0.2.0 → astro_otter-0.3.2}/LICENSE +0 -0
  15. {astro_otter-0.2.0 → astro_otter-0.3.2}/setup.cfg +0 -0
  16. {astro_otter-0.2.0 → astro_otter-0.3.2}/src/astro_otter.egg-info/SOURCES.txt +0 -0
  17. {astro_otter-0.2.0 → astro_otter-0.3.2}/src/astro_otter.egg-info/dependency_links.txt +0 -0
  18. {astro_otter-0.2.0 → astro_otter-0.3.2}/src/astro_otter.egg-info/top_level.txt +0 -0
  19. {astro_otter-0.2.0 → astro_otter-0.3.2}/src/otter/__init__.py +0 -0
  20. {astro_otter-0.2.0 → astro_otter-0.3.2}/src/otter/exceptions.py +0 -0
  21. {astro_otter-0.2.0 → astro_otter-0.3.2}/src/otter/io/__init__.py +0 -0
  22. {astro_otter-0.2.0 → astro_otter-0.3.2}/src/otter/io/data_finder.py +0 -0
  23. {astro_otter-0.2.0 → astro_otter-0.3.2}/src/otter/io/host.py +0 -0
  24. {astro_otter-0.2.0 → astro_otter-0.3.2}/src/otter/plotter/__init__.py +0 -0
  25. {astro_otter-0.2.0 → astro_otter-0.3.2}/src/otter/plotter/otter_plotter.py +0 -0
  26. {astro_otter-0.2.0 → astro_otter-0.3.2}/src/otter/plotter/plotter.py +0 -0
  27. {astro_otter-0.2.0 → astro_otter-0.3.2}/tests/test_exceptions.py +0 -0
  28. {astro_otter-0.2.0 → astro_otter-0.3.2}/tests/test_host.py +0 -0
  29. {astro_otter-0.2.0 → astro_otter-0.3.2}/tests/test_package.py +0 -0
  30. {astro_otter-0.2.0 → astro_otter-0.3.2}/tests/test_util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astro-otter
3
- Version: 0.2.0
3
+ Version: 0.3.2
4
4
  Author-email: Noah Franz <nfranz@arizona.edu>
5
5
  License: MIT License
6
6
 
@@ -36,7 +36,7 @@ Classifier: Programming Language :: Python :: 3
36
36
  Classifier: Programming Language :: Python :: 3.10
37
37
  Classifier: Programming Language :: Python :: 3.11
38
38
  Classifier: Development Status :: 2 - Pre-Alpha
39
- Requires-Python: <3.12,>=3.10
39
+ Requires-Python: >=3.9
40
40
  Description-Content-Type: text/markdown
41
41
  License-File: LICENSE
42
42
  Requires-Dist: numpy<2,>=1.20
@@ -45,6 +45,7 @@ Requires-Dist: pandas
45
45
  Requires-Dist: synphot
46
46
  Requires-Dist: typing-extensions
47
47
  Requires-Dist: pyarango
48
+ Requires-Dist: tabulate
48
49
  Requires-Dist: matplotlib
49
50
  Requires-Dist: plotly
50
51
  Requires-Dist: astroquery
@@ -117,6 +118,7 @@ python3 -m pip install astro-otter
117
118
  ```
118
119
  git clone https://github.com/astro-otter/otter.git $OTTER_ROOT/otter
119
120
  git clone https://github.com/astro-otter/otterdb.git $OTTER_ROOT/otterdb
121
+ git clone https://github.com/astro-otter/otter-web.git $OTTER_ROOT/otter-web
120
122
  ```
121
123
  3. Install the NASA ADS Python API by following the instructions at https://ads.readthedocs.io/en/latest/#getting-started
122
124
  4. Install otter, the API for this database. From
@@ -125,13 +127,7 @@ python3 -m pip install astro-otter
125
127
  cd $OTTER_ROOT/otter
126
128
  python -m pip install -e .
127
129
  ```
128
- 5. Process the data to build the local "database" (although it is really just a directory).
129
- Then, you can build the "database" by running the
130
- following commands:
131
- ```
132
- cd $OTTER_ROOT/otter/scripts/
133
- python3 gen_summary_table.py --otterroot $OTTER_ROOT
134
- ```
130
+ 5. Process the data to build the local copy of the database. Follow the instructions in the otterdb repo README.
135
131
  6. Easily access the data using the Otter code! In python:
136
132
  ```
137
133
  import os
@@ -45,6 +45,7 @@ python3 -m pip install astro-otter
45
45
  ```
46
46
  git clone https://github.com/astro-otter/otter.git $OTTER_ROOT/otter
47
47
  git clone https://github.com/astro-otter/otterdb.git $OTTER_ROOT/otterdb
48
+ git clone https://github.com/astro-otter/otter-web.git $OTTER_ROOT/otter-web
48
49
  ```
49
50
  3. Install the NASA ADS Python API by following the instructions at https://ads.readthedocs.io/en/latest/#getting-started
50
51
  4. Install otter, the API for this database. From
@@ -53,13 +54,7 @@ python3 -m pip install astro-otter
53
54
  cd $OTTER_ROOT/otter
54
55
  python -m pip install -e .
55
56
  ```
56
- 5. Process the data to build the local "database" (although it is really just a directory).
57
- Then, you can build the "database" by running the
58
- following commands:
59
- ```
60
- cd $OTTER_ROOT/otter/scripts/
61
- python3 gen_summary_table.py --otterroot $OTTER_ROOT
62
- ```
57
+ 5. Process the data to build the local copy of the database. Follow the instructions in the otterdb repo README.
63
58
  6. Easily access the data using the Otter code! In python:
64
59
  ```
65
60
  import os
@@ -9,7 +9,7 @@ readme = "README.md"
9
9
  license = {file = "LICENSE"}
10
10
  dynamic = ["version"]
11
11
 
12
- requires-python = ">=3.10,<3.12"
12
+ requires-python = ">=3.9" #,<3.12"
13
13
 
14
14
  classifiers = [
15
15
  "License :: OSI Approved :: BSD License",
@@ -33,6 +33,7 @@ dependencies = [
33
33
  "synphot",
34
34
  "typing-extensions",
35
35
  "pyarango",
36
+ "tabulate",
36
37
 
37
38
  # for the plotting
38
39
  "matplotlib",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astro-otter
3
- Version: 0.2.0
3
+ Version: 0.3.2
4
4
  Author-email: Noah Franz <nfranz@arizona.edu>
5
5
  License: MIT License
6
6
 
@@ -36,7 +36,7 @@ Classifier: Programming Language :: Python :: 3
36
36
  Classifier: Programming Language :: Python :: 3.10
37
37
  Classifier: Programming Language :: Python :: 3.11
38
38
  Classifier: Development Status :: 2 - Pre-Alpha
39
- Requires-Python: <3.12,>=3.10
39
+ Requires-Python: >=3.9
40
40
  Description-Content-Type: text/markdown
41
41
  License-File: LICENSE
42
42
  Requires-Dist: numpy<2,>=1.20
@@ -45,6 +45,7 @@ Requires-Dist: pandas
45
45
  Requires-Dist: synphot
46
46
  Requires-Dist: typing-extensions
47
47
  Requires-Dist: pyarango
48
+ Requires-Dist: tabulate
48
49
  Requires-Dist: matplotlib
49
50
  Requires-Dist: plotly
50
51
  Requires-Dist: astroquery
@@ -117,6 +118,7 @@ python3 -m pip install astro-otter
117
118
  ```
118
119
  git clone https://github.com/astro-otter/otter.git $OTTER_ROOT/otter
119
120
  git clone https://github.com/astro-otter/otterdb.git $OTTER_ROOT/otterdb
121
+ git clone https://github.com/astro-otter/otter-web.git $OTTER_ROOT/otter-web
120
122
  ```
121
123
  3. Install the NASA ADS Python API by following the instructions at https://ads.readthedocs.io/en/latest/#getting-started
122
124
  4. Install otter, the API for this database. From
@@ -125,13 +127,7 @@ python3 -m pip install astro-otter
125
127
  cd $OTTER_ROOT/otter
126
128
  python -m pip install -e .
127
129
  ```
128
- 5. Process the data to build the local "database" (although it is really just a directory).
129
- Then, you can build the "database" by running the
130
- following commands:
131
- ```
132
- cd $OTTER_ROOT/otter/scripts/
133
- python3 gen_summary_table.py --otterroot $OTTER_ROOT
134
- ```
130
+ 5. Process the data to build the local copy of the database. Follow the instructions in the otterdb repo README.
135
131
  6. Easily access the data using the Otter code! In python:
136
132
  ```
137
133
  import os
@@ -4,6 +4,7 @@ pandas
4
4
  synphot
5
5
  typing-extensions
6
6
  pyarango
7
+ tabulate
7
8
  matplotlib
8
9
  plotly
9
10
  astroquery
@@ -2,4 +2,4 @@
2
2
  Just define the package version in one place
3
3
  """
4
4
 
5
- __version__ = "0.2.0"
5
+ __version__ = "0.3.2"
@@ -3,11 +3,12 @@ This is the primary class for user interaction with the catalog
3
3
  """
4
4
 
5
5
  from __future__ import annotations
6
+ from typing import Optional
6
7
  import os
7
8
  import json
8
9
  import glob
9
- from warnings import warn
10
10
  from copy import deepcopy
11
+ import logging
11
12
 
12
13
  from pyArango.connection import Connection
13
14
  from pyArango.database import Database
@@ -22,7 +23,7 @@ from astropy import units as u
22
23
 
23
24
  from .transient import Transient
24
25
  from ..exceptions import FailedQueryError, OtterLimitationError, TransientMergeError
25
- from ..util import bibcode_to_hrn, freq_to_obstype, freq_to_band
26
+ from ..util import bibcode_to_hrn, freq_to_obstype, freq_to_band, _DuplicateFilter
26
27
 
27
28
  import warnings
28
29
 
@@ -30,6 +31,8 @@ warnings.simplefilter("once", RuntimeWarning)
30
31
  warnings.simplefilter("once", UserWarning)
31
32
  warnings.simplefilter("once", u.UnitsWarning)
32
33
 
34
+ logger = logging.getLogger(__name__)
35
+
33
36
 
34
37
  def _np_encoder(object):
35
38
  """
@@ -44,23 +47,34 @@ class Otter(Database):
44
47
  This is the primary class for users to access the otter backend database
45
48
 
46
49
  Args:
50
+ url (str): The url where the database api endpoints are located
51
+ username (str): The username to log into the database with
52
+ password (str): The password to log into the database with
53
+ gen_summary (bool): Generate a local summary table, this should generally be
54
+ left as False!
47
55
  datadir (str): Path to the data directory with the otter data. If not provided
48
56
  will default to a ".otter" directory in the CWD where you call
49
57
  this class from.
50
58
  debug (bool): If we should just debug and not do anything serious.
51
59
 
60
+ Returns:
61
+ An Otter object that is connected to the otter database
52
62
  """
53
63
 
54
64
  def __init__(
55
65
  self,
56
66
  url: str = "http://127.0.0.1:8529",
57
- username: str = "user-guest",
58
- password: str = "",
67
+ username: str = os.environ.get("ARANGO_USER_USERNAME", "user-guest"),
68
+ password: str = os.environ.get("ARANGO_USER_PASSWORD", ""),
59
69
  gen_summary: bool = False,
60
70
  datadir: str = None,
61
71
  debug: bool = False,
62
72
  **kwargs,
63
73
  ) -> None:
74
+ print("Attempting to login with the following credentials:")
75
+ print(f"username: {username}")
76
+ print(f"password: {password}")
77
+
64
78
  # save inputs
65
79
  if datadir is None:
66
80
  self.CWD = os.path.dirname(os.path.abspath("__FILE__"))
@@ -79,7 +93,7 @@ class Otter(Database):
79
93
  try:
80
94
  os.makedirs(self.DATADIR)
81
95
  except FileExistsError:
82
- warn(
96
+ logger.warning(
83
97
  "Directory was created between the if statement and trying "
84
98
  + "to create the directory!"
85
99
  )
@@ -93,7 +107,8 @@ class Otter(Database):
93
107
  Get the metadata of the objects matching the arguments
94
108
 
95
109
  Args:
96
- **kwargs : Arguments to pass to Otter.query()
110
+ **kwargs : Arguments to pass to Otter.query(). See that documentation with
111
+ `help(otter.Otter.query)`.
97
112
  Return:
98
113
  The metadata for the transients that match the arguments. Will be an astropy
99
114
  Table by default, if raw=True will be a dictionary.
@@ -146,9 +161,9 @@ class Otter(Database):
146
161
  unit conversion for you!
147
162
 
148
163
  Args:
149
- flux_units (astropy.unit.Unit): Either a valid string to convert
164
+ flux_unit (astropy.unit.Unit): Either a valid string to convert
150
165
  or an astropy.unit.Unit
151
- date_units (astropy.unit.Unit): Either a valid string to convert to a date
166
+ date_unit (astropy.unit.Unit): Either a valid string to convert to a date
152
167
  or an astropy.unit.Unit
153
168
  return_type (str): Either 'astropy' or 'pandas'. If astropy, returns an
154
169
  astropy Table. If pandas, returns a pandas DataFrame.
@@ -159,6 +174,8 @@ class Otter(Database):
159
174
  keep_raw (bool): If True, keep the raw flux/date/freq/wave associated with
160
175
  the dataset. Else, just keep the converted data. Default
161
176
  is False.
177
+ wave_unit (str): The astropy wavelength unit to return with
178
+ freq_unit (str): The astropy frequency unit to return with`
162
179
  **kwargs : Arguments to pass to Otter.query(). Can be::
163
180
 
164
181
  names (list[str]): A list of names to get the metadata for
@@ -181,6 +198,9 @@ class Otter(Database):
181
198
  FailedQueryError: When the query returns no results
182
199
  IOError: if one of your inputs is incorrect
183
200
  """
201
+ warn_filt = _DuplicateFilter()
202
+ logger.addFilter(warn_filt)
203
+
184
204
  queryres = self.query(hasphot=True, **kwargs)
185
205
 
186
206
  dicts = []
@@ -239,6 +259,7 @@ class Otter(Database):
239
259
  else:
240
260
  fullphot = fullphot[keys_to_keep]
241
261
 
262
+ logger.removeFilter(warn_filt)
242
263
  if return_type == "astropy":
243
264
  return Table.from_pandas(fullphot)
244
265
  elif return_type == "pandas":
@@ -252,6 +273,9 @@ class Otter(Database):
252
273
 
253
274
  Args:
254
275
  filename (str): The path to the OTTER JSON file to load
276
+
277
+ Returns:
278
+ dictionary with the otter JSON file contents
255
279
  """
256
280
 
257
281
  # read in files from summary
@@ -270,6 +294,8 @@ class Otter(Database):
270
294
  refs: list[str] = None,
271
295
  hasphot: bool = False,
272
296
  hasspec: bool = False,
297
+ spec_classed: bool = False,
298
+ unambiguous: bool = False,
273
299
  classification: str = None,
274
300
  class_confidence_threshold: float = 0,
275
301
  query_private=False,
@@ -280,7 +306,7 @@ class Otter(Database):
280
306
 
281
307
  WARNING! This does not do any conversions for you!
282
308
  This is how it differs from the `get_meta` method. Users should prefer to use
283
- `get_meta`, `getPhot`, and `getSpec` independently because it is a better
309
+ `get_meta`, and `get_phot` independently because it is a better
284
310
  workflow and can return the data in an astropy table with everything in the
285
311
  same units.
286
312
 
@@ -293,10 +319,17 @@ class Otter(Database):
293
319
  refs (list[str]): A list of ads bibcodes to match to. Will only return
294
320
  metadata for transients that have this as a reference.
295
321
  hasphot (bool): if True, only returns transients which have photometry.
296
- hasspec (bool): if True, only return transients that have spectra.
322
+ hasspec (bool): NOT IMPLEMENTED! Will return False for all targets!
323
+ spec_classed (bool): If True, only returns transients that have been
324
+ specotroscopically classified/confirmed
325
+ unambiguous (bool): If True, only returns transients that only have a single
326
+ published classification in OTTER. If classifications
327
+ disagree for a transient, it will be filtered out.
297
328
  classification (str): A classification string to search for
298
329
  class_confidence_threshold (float): classification confidence cutoff for
299
330
  query, between 0 and 1. Default is 0.
331
+ query_private (bool): Set to True if you would like to also query the
332
+ dataset located at whatever you set datadir to
300
333
 
301
334
  Return:
302
335
  Get all of the raw (unconverted!) data for objects that match the criteria.
@@ -310,9 +343,15 @@ class Otter(Database):
310
343
  if hasspec is True:
311
344
  query_filters += "FILTER 'spectra' IN ATTRIBUTES(transient)\n"
312
345
 
346
+ if spec_classed:
347
+ query_filters += "FILTER transient.classification.spec_classed >= 1"
348
+
349
+ if unambiguous:
350
+ query_filters += "FILTER transient.classification.unambiguous"
351
+
313
352
  if classification is not None:
314
353
  query_filters += f"""
315
- FOR subdoc IN transient.classification
354
+ FOR subdoc IN transient.classification.value
316
355
  FILTER subdoc.confidence > TO_NUMBER({class_confidence_threshold})
317
356
  FILTER subdoc.object_class LIKE '%{classification}%'
318
357
  """
@@ -573,7 +612,7 @@ class Otter(Database):
573
612
 
574
613
  def upload(self, json_data, collection="vetting", testing=False) -> Document:
575
614
  """
576
- Upload json_data to collection
615
+ Upload json_data to collection WITHOUT deduplication!
577
616
 
578
617
  Args:
579
618
  json_data [dict] : A dictionary of the json data to upload to Otter
@@ -793,7 +832,7 @@ class Otter(Database):
793
832
  def from_csvs(
794
833
  metafile: str,
795
834
  photfile: str = None,
796
- local_outpath: str = "private_otter_data",
835
+ local_outpath: Optional[str] = None,
797
836
  db: Otter = None,
798
837
  ) -> Otter:
799
838
  """
@@ -815,9 +854,37 @@ class Otter(Database):
815
854
  """
816
855
  # read in the metadata and photometry file
817
856
  meta = pd.read_csv(metafile)
857
+ meta.columns = meta.columns.str.strip() # clean up the col names
818
858
  phot = None
859
+
860
+ required_phot_cols = [
861
+ "name",
862
+ "date",
863
+ "date_format",
864
+ "filter",
865
+ "filter_eff",
866
+ "filter_eff_units",
867
+ "flux",
868
+ "flux_err",
869
+ "flux_unit",
870
+ ]
871
+
819
872
  if photfile is not None:
820
- phot = pd.read_csv(photfile)
873
+ phot_unclean = pd.read_csv(photfile)
874
+ phot_unclean.columns = phot_unclean.columns.str.strip() # cleanup colnames
875
+
876
+ phot = phot_unclean.dropna(subset=required_phot_cols)
877
+ if len(phot) != len(phot_unclean):
878
+ logger.warning("""
879
+ Filtered out rows with nan in the photometry file! Make sure you
880
+ expect this behaviour!
881
+ """)
882
+
883
+ if "bibcode" not in phot:
884
+ phot["bibcode"] = "private"
885
+ logger.warning("""
886
+ Setting the bibcode column to the special keyword 'private'!
887
+ """)
821
888
 
822
889
  # we need to generate columns of wave_eff and freq_eff
823
890
  wave_eff = []
@@ -842,7 +909,7 @@ class Otter(Database):
842
909
  phot["band_eff_freq_unit"] = str(freq_eff_unit)
843
910
 
844
911
  if not os.path.exists(local_outpath):
845
- os.mkdir(local_outpath)
912
+ os.makedirs(local_outpath)
846
913
 
847
914
  # drop duplicated names in meta and keep the first
848
915
  meta = meta.drop_duplicates(subset="name", keep="first")
@@ -883,7 +950,7 @@ class Otter(Database):
883
950
  ra_units=tde.ra_unit[0],
884
951
  dec_units=tde.dec_unit[0],
885
952
  reference=[tde.coord_bibcode[0]],
886
- coordinate_type="equitorial",
953
+ coordinate_type="equatorial",
887
954
  )
888
955
  ]
889
956
 
@@ -932,13 +999,18 @@ class Otter(Database):
932
999
  ### Classification information that is in the csvs
933
1000
  # classification
934
1001
  if "classification" in tde:
935
- json["classification"] = [
936
- dict(
937
- object_class=tde.classification[0],
938
- confidence=1, # we know this is at least an tde
939
- reference=[tde.classification_bibcode[0]],
940
- )
941
- ]
1002
+ class_flag = 0
1003
+ if "classification_flag" in tde:
1004
+ class_flag = tde.classification_flag[0]
1005
+ json["classification"] = dict(
1006
+ value=[
1007
+ dict(
1008
+ object_class=tde.classification[0],
1009
+ confidence=class_flag,
1010
+ reference=[tde.classification_bibcode[0]],
1011
+ )
1012
+ ]
1013
+ )
942
1014
 
943
1015
  # discovery date
944
1016
  # print(tde)
@@ -1236,7 +1308,7 @@ class Otter(Database):
1236
1308
  if db is None:
1237
1309
  db = Otter(datadir=local_outpath)
1238
1310
  else:
1239
- db.datadir = local_outpath
1311
+ db.DATADIR = local_outpath
1240
1312
 
1241
1313
  # always save this document as a new one
1242
1314
  db.save(all_jsons)
@@ -24,11 +24,9 @@ from ..exceptions import (
24
24
  OtterLimitationError,
25
25
  TransientMergeError,
26
26
  )
27
- from ..util import XRAY_AREAS
27
+ from ..util import XRAY_AREAS, _KNOWN_CLASS_ROOTS, _DuplicateFilter
28
28
  from .host import Host
29
29
 
30
- warnings.simplefilter("once", RuntimeWarning)
31
- warnings.simplefilter("once", UserWarning)
32
30
  np.seterr(divide="ignore")
33
31
  logger = logging.getLogger(__name__)
34
32
 
@@ -289,7 +287,7 @@ class Transient(MutableMapping):
289
287
  raise TransientMergeError(f"{key} was not expected! Can not merge!")
290
288
  else:
291
289
  # Throw a warning and only keep the old stuff
292
- warnings.warn(
290
+ logger.warning(
293
291
  f"{key} was not expected! Only keeping the old information!"
294
292
  )
295
293
  out[key] = deepcopy(self[key])
@@ -323,17 +321,17 @@ class Transient(MutableMapping):
323
321
  else:
324
322
  # run some checks
325
323
  if "photometry" in keys:
326
- warnings.warn("Not returing the photometry!")
324
+ logger.warning("Not returing the photometry!")
327
325
  _ = keys.pop("photometry")
328
326
  if "spectra" in keys:
329
- warnings.warn("Not returning the spectra!")
327
+ logger.warning("Not returning the spectra!")
330
328
  _ = keys.pop("spectra")
331
329
 
332
330
  curr_keys = self.keys()
333
331
  for key in keys:
334
332
  if key not in curr_keys:
335
333
  keys.remove(key)
336
- warnings.warn(
334
+ logger.warning(
337
335
  f"Not returning {key} because it is not in this transient!"
338
336
  )
339
337
 
@@ -352,7 +350,7 @@ class Transient(MutableMapping):
352
350
  """
353
351
 
354
352
  # now we can generate the SkyCoord
355
- f = "df['coordinate_type'] == 'equitorial'"
353
+ f = "df['coordinate_type'] == 'equatorial'"
356
354
  coord_dict = self._get_default("coordinate", filt=f)
357
355
  coordin = self._reformat_coordinate(coord_dict)
358
356
  coord = SkyCoord(**coordin).transform_to(coord_format)
@@ -407,7 +405,7 @@ class Transient(MutableMapping):
407
405
  and a list of the bibcodes corresponding to that classification. Or, None
408
406
  if there is no classification.
409
407
  """
410
- default = self._get_default("classification")
408
+ default = self._get_default("classification/value")
411
409
  if default is None:
412
410
  return default
413
411
  return default.object_class, default.confidence, default.reference
@@ -421,7 +419,7 @@ class Transient(MutableMapping):
421
419
  The BLAST result will always be the last value in the returned list.
422
420
 
423
421
  Args:
424
- max_hosts [int] : The maximum number of hosts to return
422
+ max_hosts [int] : The maximum number of hosts to return, default is 3
425
423
  **kwargs : keyword arguments to be passed to getGHOST
426
424
 
427
425
  Returns:
@@ -437,7 +435,7 @@ class Transient(MutableMapping):
437
435
 
438
436
  # then try BLAST
439
437
  if search:
440
- logger.warn(
438
+ logger.warning(
441
439
  "Trying to find a host with BLAST/astro-ghost. Note\
442
440
  that this won't work for older targets! See https://blast.scimma.org"
443
441
  )
@@ -489,7 +487,7 @@ class Transient(MutableMapping):
489
487
  """
490
488
  coordin = None
491
489
  if "ra" in item and "dec" in item:
492
- # this is an equitorial coordinate
490
+ # this is an equatorial coordinate
493
491
  coordin = {
494
492
  "ra": item["ra"],
495
493
  "dec": item["dec"],
@@ -511,7 +509,6 @@ class Transient(MutableMapping):
511
509
  date_unit: u.Unit = "MJD",
512
510
  freq_unit: u.Unit = "GHz",
513
511
  wave_unit: u.Unit = "nm",
514
- by: str = "raw",
515
512
  obs_type: str = None,
516
513
  ) -> pd.DataFrame:
517
514
  """
@@ -529,10 +526,6 @@ class Transient(MutableMapping):
529
526
  wave_unit (astropy.unit.Unit): The astropy unit or string representation of
530
527
  an astropy unit to convert and return the
531
528
  wavelength as.
532
- by (str): Either 'raw' or 'value'. 'raw' is the default and is highly
533
- recommended! If 'value' is used it may skip some photometry.
534
- See the schema definition to understand this keyword completely
535
- before using it.
536
529
  obs_type (str): "radio", "xray", or "uvoir". If provided, it only returns
537
530
  data taken within that range of wavelengths/frequencies.
538
531
  Default is None which will return all of the data.
@@ -540,14 +533,17 @@ class Transient(MutableMapping):
540
533
  Returns:
541
534
  A pandas DataFrame of the cleaned up photometry in the requested units
542
535
  """
536
+ warn_filt = _DuplicateFilter()
537
+ logger.addFilter(warn_filt)
538
+
543
539
  # these imports need to be here for some reason
544
540
  # otherwise the code breaks
545
541
  from synphot.units import VEGAMAG, convert_flux
546
542
  from synphot.spectrum import SourceSpectrum
547
543
 
548
- # check inputs
549
- if by not in {"value", "raw"}:
550
- raise IOError("Please choose either value or raw!")
544
+ # variable so this warning only displays a single time each time this
545
+ # function is called
546
+ source_map_warning = True
551
547
 
552
548
  # turn the photometry key into a pandas dataframe
553
549
  if "photometry" not in self:
@@ -594,12 +590,82 @@ class Transient(MutableMapping):
594
590
  # merge the photometry with the filter information
595
591
  df = c.merge(filters, on="filter_key")
596
592
 
597
- # make sure 'by' is in df
598
- if by not in df:
599
- if by == "value":
600
- by = "raw"
601
- else:
602
- by = "value"
593
+ # drop irrelevant obs_types before continuing
594
+ if obs_type is not None:
595
+ valid_obs_types = {"radio", "uvoir", "xray"}
596
+ if obs_type not in valid_obs_types:
597
+ raise IOError("Please provide a valid obs_type")
598
+ df = df[df.obs_type == obs_type]
599
+
600
+ # add some mockup columns if they don't exist
601
+ if "value" not in df:
602
+ df["value"] = np.nan
603
+ df["value_err"] = np.nan
604
+ df["value_units"] = "NaN"
605
+
606
+ # fix some bad units that are old and no longer recognized by astropy
607
+ with warnings.catch_warnings():
608
+ warnings.filterwarnings("ignore")
609
+ df.raw_units = df.raw_units.str.replace("ergs", "erg")
610
+ df.raw_units = ["mag(AB)" if uu == "AB" else uu for uu in df.raw_units]
611
+ df.value_units = df.value_units.str.replace("ergs", "erg")
612
+ df.value_units = ["mag(AB)" if uu == "AB" else uu for uu in df.value_units]
613
+
614
+ # merge the raw and value keywords based on the requested flux_units
615
+ # first take everything that just has `raw` and not `value`
616
+ df_raw_only = df[df.value.isna()]
617
+ remaining = df[df.value.notna()]
618
+ if len(remaining) == 0:
619
+ df_raw = df_raw_only
620
+ df_value = [] # this tricks the code later
621
+ else:
622
+ # then take the remaining rows and figure out if we want the raw or value
623
+ with warnings.catch_warnings():
624
+ warnings.filterwarnings("ignore")
625
+ flux_unit_astropy = u.Unit(flux_unit)
626
+
627
+ val_unit_filt = np.array(
628
+ [
629
+ u.Unit(uu).is_equivalent(flux_unit_astropy)
630
+ for uu in remaining.value_units
631
+ ]
632
+ )
633
+
634
+ df_value = remaining[val_unit_filt]
635
+ df_raw_and_value = remaining[~val_unit_filt]
636
+
637
+ # then merge the raw dataframes
638
+ df_raw = pd.concat([df_raw_only, df_raw_and_value], axis=0)
639
+
640
+ # then add columns to these dataframes to convert stuff later
641
+ df_raw = df_raw.assign(
642
+ _flux=df_raw["raw"].values,
643
+ _flux_units=df_raw["raw_units"].values,
644
+ _flux_err=(
645
+ df_raw["raw_err"].values
646
+ if "raw_err" in df_raw
647
+ else [np.nan] * len(df_raw)
648
+ ),
649
+ )
650
+
651
+ if len(df_value) == 0:
652
+ df = df_raw
653
+ else:
654
+ df_value = df_value.assign(
655
+ _flux=df_value["value"].values,
656
+ _flux_units=df_value["value_units"].values,
657
+ _flux_err=(
658
+ df_value["value_err"].values
659
+ if "value_err" in df_value
660
+ else [np.nan] * len(df_value)
661
+ ),
662
+ )
663
+
664
+ # then merge df_value and df_raw back into one df
665
+ df = pd.concat([df_raw, df_value], axis=0)
666
+
667
+ # then, for the rest of the code to work, set the "by" variables to _flux
668
+ by = "_flux"
603
669
 
604
670
  # skip rows where 'by' is nan
605
671
  df = df[df[by].notna()]
@@ -612,12 +678,21 @@ class Transient(MutableMapping):
612
678
  # the TDE lightcurves for this systematic effect. "
613
679
  df = df[df[by].astype(float) > 0]
614
680
 
615
- # drop irrelevant obs_types before continuing
616
- if obs_type is not None:
617
- valid_obs_types = {"radio", "uvoir", "xray"}
618
- if obs_type not in valid_obs_types:
619
- raise IOError("Please provide a valid obs_type")
620
- df = df[df.obs_type == obs_type]
681
+ # filter out anything that has _flux_units == "ct" because we can't convert that
682
+ try:
683
+ # this is a test case to see if we can convert ct -> flux_unit
684
+ convert_flux(
685
+ [1 * u.nm, 2 * u.nm], 1 * u.ct, u.Unit(flux_unit), area=1 * u.m**2
686
+ )
687
+ except u.UnitsError:
688
+ bad_units = df[df._flux_units == "ct"]
689
+ if len(bad_units) > 0:
690
+ logger.warning(
691
+ f"""Removing {len(bad_units)} photometry points from
692
+ {self.default_name} because we can't convert them from ct ->
693
+ {flux_unit}"""
694
+ )
695
+ df = df[df._flux_units != "ct"]
621
696
 
622
697
  # convert the ads bibcodes to a string of human readable sources here
623
698
  def mappedrefs(row):
@@ -629,7 +704,10 @@ class Transient(MutableMapping):
629
704
  try:
630
705
  df["human_readable_refs"] = df.apply(mappedrefs, axis=1)
631
706
  except Exception as exc:
632
- warnings.warn(f"Unable to apply the source mapping because {exc}")
707
+ if source_map_warning:
708
+ source_map_warning = False
709
+ logger.warning(f"Unable to apply the source mapping because {exc}")
710
+
633
711
  df["human_readable_refs"] = df.reference
634
712
 
635
713
  # Figure out what columns are good to groupby in the photometry
@@ -662,8 +740,16 @@ class Transient(MutableMapping):
662
740
  try:
663
741
  if isvegamag:
664
742
  astropy_units = VEGAMAG
743
+ elif unit == "AB":
744
+ # In astropy "AB" is a magnitude SYSTEM not unit and while
745
+ # u.Unit("AB") will succeed without error, it will not produce
746
+ # the expected result!
747
+ # We can assume here that this unit really means astropy's "mag(AB)"
748
+ astropy_units = u.Unit("mag(AB)")
665
749
  else:
666
- astropy_units = u.Unit(unit)
750
+ with warnings.catch_warnings():
751
+ warnings.simplefilter("ignore")
752
+ astropy_units = u.Unit(unit)
667
753
 
668
754
  except ValueError:
669
755
  # this means there is something likely slightly off in the input unit
@@ -688,10 +774,12 @@ class Transient(MutableMapping):
688
774
  indata_err = np.zeros(len(data))
689
775
 
690
776
  # convert to an astropy quantity
691
- q = indata * u.Unit(astropy_units)
692
- q_err = indata_err * u.Unit(
693
- astropy_units
694
- ) # assume error and values have the same unit
777
+ with warnings.catch_warnings():
778
+ warnings.filterwarnings("ignore")
779
+ q = indata * u.Unit(astropy_units)
780
+ q_err = indata_err * u.Unit(
781
+ astropy_units
782
+ ) # assume error and values have the same unit
695
783
 
696
784
  # get and save the effective wavelength
697
785
  # because of cleaning we did to the filter dataframe above wave_eff
@@ -700,8 +788,10 @@ class Transient(MutableMapping):
700
788
  raise ValueError("Flushing out the effective wavelength array failed!")
701
789
 
702
790
  zz = zip(data["wave_eff"], data["wave_units"])
703
- wave_eff = u.Quantity([vv * u.Unit(uu) for vv, uu in zz], wave_unit)
704
- freq_eff = wave_eff.to(freq_unit, equivalencies=u.spectral())
791
+ with warnings.catch_warnings():
792
+ warnings.filterwarnings("ignore")
793
+ wave_eff = u.Quantity([vv * u.Unit(uu) for vv, uu in zz], wave_unit)
794
+ freq_eff = wave_eff.to(freq_unit, equivalencies=u.spectral())
705
795
 
706
796
  data["converted_wave"] = wave_eff.value
707
797
  data["converted_wave_unit"] = wave_unit
@@ -727,10 +817,12 @@ class Transient(MutableMapping):
727
817
  # we also need to make this wave_min and wave_max
728
818
  # instead of just the effective wavelength like for radio and uvoir
729
819
  zz = zip(data["wave_min"], data["wave_max"], data["wave_units"])
730
- wave_eff = u.Quantity(
731
- [np.array([m, M]) * u.Unit(uu) for m, M, uu in zz],
732
- u.Unit(wave_unit),
733
- )
820
+ with warnings.catch_warnings():
821
+ warnings.filterwarnings("ignore")
822
+ wave_eff = u.Quantity(
823
+ [np.array([m, M]) * u.Unit(uu) for m, M, uu in zz],
824
+ u.Unit(wave_unit),
825
+ )
734
826
 
735
827
  else:
736
828
  area = None
@@ -744,13 +836,15 @@ class Transient(MutableMapping):
744
836
 
745
837
  flux, flux_err = [], []
746
838
  for wave, xray_point, xray_point_err in zip(wave_eff, q, q_err):
747
- f_val = convert_flux(
748
- wave,
749
- xray_point,
750
- u.Unit(flux_unit),
751
- vegaspec=SourceSpectrum.from_vega(),
752
- area=area,
753
- ).value
839
+ with warnings.catch_warnings():
840
+ warnings.filterwarnings("ignore")
841
+ f_val = convert_flux(
842
+ wave,
843
+ xray_point,
844
+ u.Unit(flux_unit),
845
+ vegaspec=SourceSpectrum.from_vega(),
846
+ area=area,
847
+ ).value
754
848
 
755
849
  # approximate the uncertainty as dX = dY/Y * X
756
850
  f_err = np.multiply(
@@ -764,7 +858,9 @@ class Transient(MutableMapping):
764
858
 
765
859
  else:
766
860
  # this will be faster and cover most cases
767
- flux = convert_flux(wave_eff, q, u.Unit(flux_unit)).value
861
+ with warnings.catch_warnings():
862
+ warnings.filterwarnings("ignore")
863
+ flux = convert_flux(wave_eff, q, u.Unit(flux_unit)).value
768
864
 
769
865
  # since the error propagation is different between logarithmic units
770
866
  # and linear units, unfortunately
@@ -806,20 +902,21 @@ class Transient(MutableMapping):
806
902
  # magnitude upperlimits are independent of the actual measurement!)
807
903
  # sigma_m > (1/3) * (ln(10)/2.5)
808
904
  def is_upperlimit(row):
809
- if pd.isna(row.upperlimit):
905
+ if "upperlimit" in row and pd.isna(row.upperlimit):
810
906
  return row.converted_flux_err > np.log(10) / (3 * 2.5)
811
907
  else:
812
908
  return row.upperlimit
813
909
  else:
814
910
 
815
911
  def is_upperlimit(row):
816
- if pd.isna(row.upperlimit):
912
+ if "upperlimit" in row and pd.isna(row.upperlimit):
817
913
  return row.converted_flux < 3 * row.converted_flux_err
818
914
  else:
819
915
  return row.upperlimit
820
916
 
821
917
  outdata["upperlimit"] = outdata.apply(is_upperlimit, axis=1)
822
918
 
919
+ logger.removeFilter(warn_filt)
823
920
  return outdata
824
921
 
825
922
  def _merge_names(t1, t2, out): # noqa: N805
@@ -871,7 +968,7 @@ class Transient(MutableMapping):
871
968
  elif score2 > score1:
872
969
  out[key]["default_name"] = t2[key]["default_name"]
873
970
  else:
874
- warnings.warn(
971
+ logger.warning(
875
972
  "Names have the same score! Just using the existing default_name"
876
973
  )
877
974
  out[key]["default_name"] = t1[key]["default_name"]
@@ -986,36 +1083,108 @@ class Transient(MutableMapping):
986
1083
  Combine the classification attribute
987
1084
  """
988
1085
  key = "classification"
1086
+ subkey = "value"
989
1087
  out[key] = deepcopy(t1[key])
990
- classes = np.array([item["object_class"] for item in out[key]])
991
- for item in t2[key]:
1088
+ classes = np.array([item["object_class"] for item in out[key][subkey]])
1089
+
1090
+ for item in t2[key][subkey]:
992
1091
  if item["object_class"] in classes:
993
1092
  i = np.where(item["object_class"] == classes)[0][0]
994
- if int(item["confidence"]) > int(out[key][i]["confidence"]):
995
- out[key][i]["confidence"] = item[
1093
+ if int(item["confidence"]) > int(out[key][subkey][i]["confidence"]):
1094
+ out[key][subkey][i]["confidence"] = item[
996
1095
  "confidence"
997
1096
  ] # we are now more confident
998
1097
 
999
- if not isinstance(out[key][i]["reference"], list):
1000
- out[key][i]["reference"] = [out[key][i]["reference"]]
1098
+ if not isinstance(out[key][subkey][i]["reference"], list):
1099
+ out[key][subkey][i]["reference"] = [
1100
+ out[key][subkey][i]["reference"]
1101
+ ]
1001
1102
 
1002
1103
  if not isinstance(item["reference"], list):
1003
1104
  item["reference"] = [item["reference"]]
1004
1105
 
1005
- newdata = list(np.unique(out[key][i]["reference"] + item["reference"]))
1006
- out[key][i]["reference"] = newdata
1106
+ newdata = list(
1107
+ np.unique(out[key][subkey][i]["reference"] + item["reference"])
1108
+ )
1109
+ out[key][subkey][i]["reference"] = newdata
1007
1110
 
1008
1111
  else:
1009
- out[key].append(item)
1112
+ out[key][subkey].append(item)
1010
1113
 
1011
1114
  # now that we have all of them we need to figure out which one is the default
1012
- maxconf = max(out[key], key=lambda d: d["confidence"])
1013
- for item in out[key]:
1115
+ maxconf = max(out[key][subkey], key=lambda d: d["confidence"])
1116
+ for item in out[key][subkey]:
1014
1117
  if item == maxconf:
1015
1118
  item["default"] = True
1016
1119
  else:
1017
1120
  item["default"] = False
1018
1121
 
1122
+ # then rederive the classification flags
1123
+ out = Transient._derive_classification_flags(out)
1124
+
1125
+ @classmethod
1126
+ def _derive_classification_flags(cls, out):
1127
+ """
1128
+ Derive the classification flags based on the confidence flags. This will find
1129
+ - spec_classed
1130
+ - unambiguous
1131
+
1132
+ See the paper for a detailed description of how this algorithm makes its
1133
+ choices
1134
+ """
1135
+
1136
+ if "classification" not in out or "value" not in out["classification"]:
1137
+ # this means that the transient doesn't have any classifications
1138
+ # just return itself without any changes
1139
+ return out
1140
+
1141
+ # get the confidences of all of the classifications of this transient
1142
+ confs = np.array(
1143
+ [item["confidence"] for item in out["classification"]["value"]]
1144
+ ).astype(float)
1145
+
1146
+ all_class_roots = np.array(
1147
+ [
1148
+ _fuzzy_class_root(item["object_class"])
1149
+ for item in out["classification"]["value"]
1150
+ ]
1151
+ )
1152
+
1153
+ if np.any(confs >= 3):
1154
+ unambiguous = len(np.unique(all_class_roots)) == 1
1155
+ if np.any(confs == 3) or np.any(confs == 3.3):
1156
+ # this is a "gold spectrum"
1157
+ spec_classed = 3
1158
+ elif np.any(confs == 3.2):
1159
+ # this is a silver spectrum
1160
+ spec_classed = 2
1161
+ elif np.any(confs == 3.1):
1162
+ # this is a bronze spectrum
1163
+ spec_classed = 1
1164
+ else:
1165
+ raise ValueError("Not prepared for this confidence flag!")
1166
+
1167
+ elif np.any(confs == 2):
1168
+ # these always have spec_classed = True, by definition
1169
+ # They also have unambiguous = False by definition because they don't
1170
+ # have a peer reviewed citation for their classification
1171
+ spec_classed = 1
1172
+ unambiguous = False
1173
+
1174
+ elif np.any(confs == 1):
1175
+ spec_classed = 0 # by definition
1176
+ unambiguous = len(np.unique(all_class_roots)) == 1
1177
+
1178
+ else:
1179
+ spec_classed = 0
1180
+ unambiguous = False
1181
+
1182
+ # finally, set these keys in the classification dict
1183
+ out["classification"]["spec_classed"] = spec_classed
1184
+ out["classification"]["unambiguous"] = unambiguous
1185
+
1186
+ return out
1187
+
1019
1188
  @staticmethod
1020
1189
  def _merge_arbitrary(key, t1, t2, out, merge_subkeys=None, groupby_key=None):
1021
1190
  """
@@ -1103,3 +1272,21 @@ class Transient(MutableMapping):
1103
1272
  outdict_cleaned = [{**x[i]} for i, x in outdict.stack().groupby(level=0)]
1104
1273
 
1105
1274
  out[key] = outdict_cleaned
1275
+
1276
+
1277
+ def _fuzzy_class_root(s):
1278
+ """
1279
+ Extract the fuzzy classification root name from the string s
1280
+ """
1281
+ s = s.upper()
1282
+ # first split the class s using regex
1283
+ for root in _KNOWN_CLASS_ROOTS:
1284
+ if s.startswith(root):
1285
+ remaining = s[len(root) :]
1286
+ if remaining and root == "SN":
1287
+ # we want to be able to distinguish between SN Ia and SN II
1288
+ # we will use SN Ia to indicate thoes and SN to indicate CCSN
1289
+ if "IA" in remaining or "1A" in remaining:
1290
+ return "SN Ia"
1291
+ return root
1292
+ return s
@@ -135,6 +135,12 @@ class ClassificationSchema(BaseModel):
135
135
  class_type: str = None
136
136
 
137
137
 
138
+ class ClassificationDictSchema(BaseModel):
139
+ spec_classed: Optional[int] = None
140
+ unambiguous: Optional[bool] = None
141
+ value: list[ClassificationSchema]
142
+
143
+
138
144
  class ReferenceSchema(BaseModel):
139
145
  name: str
140
146
  human_readable_name: str
@@ -283,7 +289,7 @@ class OtterSchema(BaseModel):
283
289
  name: NameSchema
284
290
  coordinate: list[CoordinateSchema]
285
291
  distance: Optional[list[DistanceSchema]] = None
286
- classification: Optional[list[ClassificationSchema]] = None
292
+ classification: Optional[ClassificationDictSchema] = None
287
293
  reference_alias: list[ReferenceSchema]
288
294
  date_reference: Optional[list[DateSchema]] = None
289
295
  photometry: Optional[list[PhotometrySchema]] = None
@@ -580,6 +580,22 @@ VIZIER_LARGE_CATALOGS = [
580
580
  ViZier catalog names that we query for host information in the Host class
581
581
  """
582
582
 
583
+ _KNOWN_CLASS_ROOTS = [
584
+ "SN",
585
+ "SLSN",
586
+ "TDE",
587
+ "GRB",
588
+ "LGRB",
589
+ "SGRB",
590
+ "AGN",
591
+ "FRB",
592
+ "QSO",
593
+ "ANT",
594
+ ]
595
+ """
596
+ Classification root names
597
+ """
598
+
583
599
  DATADIR = os.path.join(BASEDIR, "data", "base")
584
600
  """
585
601
  Deprecated database directory that IS NOT always constant anymore
@@ -597,7 +613,7 @@ schema = {
597
613
  "name": {"default_name": None, "alias": []},
598
614
  "coordinate": [],
599
615
  "distance": [],
600
- "classification": [],
616
+ "classification": {"value": []},
601
617
  "reference_alias": [],
602
618
  "date_reference": [],
603
619
  "photometry": [],
@@ -808,3 +824,13 @@ subschema = {
808
824
  """
809
825
  A useful variable to describe all of the subschemas that are available and can be used
810
826
  """
827
+
828
+
829
+ class _DuplicateFilter(object):
830
+ def __init__(self):
831
+ self.msgs = set()
832
+
833
+ def filter(self, record):
834
+ rv = record.msg not in self.msgs
835
+ self.msgs.add(record.msg)
836
+ return rv
@@ -99,7 +99,6 @@ def test_query_simbad():
99
99
  res = df1.query_simbad()
100
100
 
101
101
  assert len(res) == 3
102
- assert res["RA"][0] == "12 48 15.2253"
103
102
 
104
103
 
105
104
  def test_query_vizier():
@@ -227,6 +226,10 @@ def test_query_nvss():
227
226
  assert "J/ApJ/737/45/table1" in res.keys()
228
227
 
229
228
 
229
+ @pytest.mark.skip(
230
+ reason="""This has a tendancy to timeout, not sure why but it sounds like its
231
+ not on us (hopefully)..."""
232
+ )
230
233
  def test_query_sparcl():
231
234
  """
232
235
  Test querying SPARCL for spectra
@@ -253,12 +256,11 @@ def test_query_heasarc():
253
256
  df1 = construct_data_finder()
254
257
 
255
258
  # test with x-ray
256
- res = df1.query_heasarc()
259
+ res = df1.query_heasarc(catalog="xray")
257
260
  assert isinstance(res, Table)
258
261
  assert len(res) >= 14, "Missing some HEASARC data"
259
262
 
260
263
  # test with radio
261
- res2 = df1.query_heasarc(heasarc_table="radio")
264
+ res2 = df1.query_heasarc(catalog="radio")
262
265
  assert isinstance(res2, Table)
263
266
  assert len(res2) >= 2, "Missing some HEASARC data"
264
- assert b"NVSS " in res2["DATABASE_TABLE"].data
@@ -11,13 +11,8 @@ import numpy as np
11
11
  import pandas as pd
12
12
  import pytest
13
13
 
14
- # get the testing path
15
- otterpath = os.path.join(os.path.dirname(os.path.realpath(__file__)), ".otter-testing")
16
-
17
- pytest.skip(
18
- "Skipping OTTER tests because they currently don't work with GitHub",
19
- allow_module_level=True,
20
- )
14
+ OTTER_URL = os.environ.get("OTTER_TEST_URL")
15
+ OTTER_TEST_PASSWORD = os.environ.get("OTTER_TEST_PASSWORD")
21
16
 
22
17
 
23
18
  def test_otter_constructor():
@@ -25,10 +20,8 @@ def test_otter_constructor():
25
20
  Just make sure everything constructs correctly
26
21
  """
27
22
 
28
- db = Otter(otterpath)
29
-
30
- assert db.DATADIR == otterpath
31
- assert db.CWD == os.path.dirname(otterpath)
23
+ db = Otter(url=OTTER_URL, password=OTTER_TEST_PASSWORD)
24
+ assert isinstance(db, Otter)
32
25
 
33
26
 
34
27
  def test_get_meta():
@@ -36,7 +29,7 @@ def test_get_meta():
36
29
  Tests the Otter.get_meta method and make sure it returns as expected
37
30
  """
38
31
 
39
- db = Otter(otterpath)
32
+ db = Otter(url=OTTER_URL, password=OTTER_TEST_PASSWORD)
40
33
 
41
34
  # first make sure everything is just copied over correctly
42
35
  allmeta = db.get_meta()
@@ -58,7 +51,7 @@ def test_cone_search():
58
51
  Tests the Otter.cone_search method
59
52
  """
60
53
 
61
- db = Otter(otterpath)
54
+ db = Otter(url=OTTER_URL, password=OTTER_TEST_PASSWORD)
62
55
 
63
56
  # just search around '2018hyz' coordinates to make sure it picks it up
64
57
  coord = SkyCoord(151.711964138, 1.69279894089, unit="deg")
@@ -74,7 +67,7 @@ def test_get_phot():
74
67
  work as expected. So, this will just test that everything comes out as expected.
75
68
  """
76
69
 
77
- db = Otter(otterpath)
70
+ db = Otter(url=OTTER_URL, password=OTTER_TEST_PASSWORD)
78
71
 
79
72
  true_keys = [
80
73
  "name",
@@ -113,18 +106,6 @@ def test_get_phot():
113
106
  db.get_phot(names="foo")
114
107
 
115
108
 
116
- def test_load_file():
117
- """
118
- Tests loading a single file from the OTTER repository
119
- """
120
- db = Otter(otterpath)
121
- testfile = os.path.join(otterpath, "AT2018hyz.json")
122
-
123
- t = db.load_file(testfile)
124
-
125
- assert t["name/default_name"] == "2018hyz"
126
-
127
-
128
109
  def test_query():
129
110
  """
130
111
  Tests the Otter.query method that basically all of this is based on
@@ -133,7 +114,7 @@ def test_query():
133
114
  but lets make sure it's complete
134
115
  """
135
116
 
136
- db = Otter(otterpath)
117
+ db = Otter(url=OTTER_URL, password=OTTER_TEST_PASSWORD)
137
118
 
138
119
  # test min and max z queries
139
120
  zgtr1 = db.query(minz=1)
@@ -146,84 +127,9 @@ def test_query():
146
127
  assert all(t["name/default_name"] in result for t in zless001)
147
128
 
148
129
  # test refs
149
- res = db.query(refs="2020MNRAS.tmp.2047S")[0]
150
- assert res["name/default_name"] == "2018hyz"
130
+ # res = db.query(refs="2020MNRAS.tmp.2047S")[0]
131
+ # assert res["name/default_name"] == "2018hyz"
151
132
 
152
133
  # test hasphot and hasspec
153
134
  assert len(db.query(hasspec=True)) == 0
154
135
  assert "ASASSN-20il" not in {t["name/default_name"] for t in db.query(hasphot=True)}
155
-
156
-
157
- def test_save():
158
- """
159
- Tests the Otter.save method which is used to update and save an OTTER JSON
160
- """
161
-
162
- db = Otter(otterpath)
163
-
164
- # first with some random data that won't match anything else
165
- test_transient = {
166
- "key1": "foo",
167
- "key2": "bar",
168
- "coordinate": [
169
- {
170
- "ra": 0,
171
- "dec": 0,
172
- "ra_units": "deg",
173
- "dec_units": "deg",
174
- "reference": ["me!"],
175
- "coordinate_type": "equitorial",
176
- }
177
- ],
178
- "name": {
179
- "default_name": "new_test_tde",
180
- "alias": [{"value": "new_test_tde", "reference": ["me!"]}],
181
- },
182
- "reference_alias": [{"name": "me!", "human_readable_name": "Noah"}],
183
- }
184
-
185
- # now try saving this
186
- db.save(test_transient, testing=True)
187
- db.save(test_transient)
188
-
189
- assert os.path.exists(os.path.join(otterpath, "new_test_tde.json"))
190
-
191
- # then remove this file because we don't want it clogging stuff up
192
- os.remove(os.path.join(otterpath, "new_test_tde.json"))
193
-
194
- # and now we need to update this to have coordinates matching another object
195
- # in otter to test merging them
196
- # This should be the same as ASASSN-20il
197
- test_transient["coordinate"] = [
198
- {
199
- "ra": "5:03:11.3",
200
- "dec": "-22:48:52.1",
201
- "ra_units": "hour",
202
- "dec_units": "deg",
203
- "reference": ["me!"],
204
- "coordinate_type": "equitorial",
205
- }
206
- ]
207
-
208
- db.save(test_transient)
209
-
210
- data = db.load_file(os.path.join(otterpath, "ASASSN-20il.json"))
211
- assert "new_test_tde" in {alias["value"] for alias in data["name/alias"]}
212
-
213
-
214
- def test_generate_summary_table():
215
- """
216
- Tests generating the summary table for the OTTER
217
- """
218
-
219
- db = Otter(otterpath)
220
-
221
- sumtab = db.generate_summary_table()
222
-
223
- assert isinstance(sumtab, pd.DataFrame)
224
-
225
- # check a random row
226
- sumtab_hyz = sumtab[sumtab.name == "2018hyz"].iloc[0]
227
- assert sumtab_hyz["name"] == "2018hyz"
228
- assert sumtab_hyz["z"] == "0.0457266"
229
- assert sumtab_hyz["json_path"] == os.path.join(otterpath, "AT2018hyz.json")
@@ -1249,7 +1249,7 @@ def generate_test_json():
1249
1249
  "computed": False,
1250
1250
  "default": True,
1251
1251
  "uuid": "b98cdbe9-fff7-415b-a6d6-78cbd88544b8",
1252
- "coordinate_type": "equitorial",
1252
+ "coordinate_type": "equatorial",
1253
1253
  },
1254
1254
  {
1255
1255
  "reference": "b98cdbe9-fff7-415b-a6d6-78cbd88544b8",
File without changes
File without changes