├── .gitignore ├── .vscode └── launch.json ├── CHANGES.md ├── LICENSE ├── README.md ├── setup.py ├── swiftbat ├── __init__.py ├── batcatalog.py ├── catalog ├── clockinfo.py ├── generaldir.py ├── recent_bcttb.fits.gz ├── sfmisc.py ├── swinfo.py └── swutil.py └── tests ├── sampleevents.np ├── testclock.py ├── testswinfo.py └── testxy.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/python,pycharm 2 | 3 | .DS_Store 4 | ### PyCharm ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | .idea/* 9 | 10 | # User-specific stuff: 11 | .idea/**/workspace.xml 12 | .idea/**/tasks.xml 13 | .idea/dictionaries 14 | 15 | # Sensitive or high-churn files: 16 | .idea/**/dataSources/ 17 | .idea/**/dataSources.ids 18 | .idea/**/dataSources.xml 19 | .idea/**/dataSources.local.xml 20 | .idea/**/sqlDataSources.xml 21 | .idea/**/dynamic.xml 22 | .idea/**/uiDesigner.xml 23 | 24 | # Gradle: 25 | .idea/**/gradle.xml 26 | .idea/**/libraries 27 | 28 | # CMake 29 | cmake-build-debug/ 30 | 31 | # Mongo Explorer plugin: 32 | .idea/**/mongoSettings.xml 33 | 34 | ## File-based project format: 35 | *.iws 36 | 37 | ## Plugin-specific files: 38 | 39 | # IntelliJ 40 | /out/ 41 | 42 | # mpeltonen/sbt-idea plugin 43 | .idea_modules/ 44 | 45 | # JIRA plugin 46 | atlassian-ide-plugin.xml 47 | 48 | # Cursive Clojure plugin 49 | .idea/replstate.xml 50 | 51 | # Ruby plugin and RubyMine 52 | /.rakeTasks 53 | 54 | # Crashlytics plugin (for Android Studio and IntelliJ) 55 | com_crashlytics_export_strings.xml 56 | crashlytics.properties 57 | crashlytics-build.properties 58 | fabric.properties 59 | 60 | ### PyCharm Patch ### 61 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 62 | 63 | # *.iml 64 | # modules.xml 65 | # .idea/misc.xml 66 | # *.ipr 67 | 68 | # Sonarlint plugin 69 | .idea/sonarlint 70 | 71 | ### Python ### 72 | # Byte-compiled / optimized / DLL files 73 | __pycache__/ 74 | *.py[cod] 75 | *$py.class 76 | 77 | # C extensions 78 | *.so 79 | 80 | # Distribution / packaging 81 | .Python 82 | build/ 83 | develop-eggs/ 84 | dist/ 85 | downloads/ 86 | eggs/ 87 | .eggs/ 88 | lib/ 89 | lib64/ 90 | parts/ 91 | sdist/ 92 | var/ 93 | wheels/ 94 | *.egg-info/ 95 | .installed.cfg 96 | *.egg 97 | 98 | # PyInstaller 99 | # Usually these files are written by a python script from a template 100 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 101 | *.manifest 102 | *.spec 103 | 104 | # Installer logs 105 | pip-log.txt 106 | pip-delete-this-directory.txt 107 | 108 | # Unit test / coverage reports 109 | htmlcov/ 110 | .tox/ 111 | .coverage 112 | .coverage.* 113 | .cache 114 | nosetests.xml 115 | coverage.xml 116 | *.cover 117 | .hypothesis/ 118 | 119 | # Translations 120 | *.mo 121 | *.pot 122 | 123 | # Django stuff: 124 | *.log 125 | local_settings.py 126 | 127 | # Flask stuff: 128 | instance/ 129 | .webassets-cache 130 | 131 | # Scrapy stuff: 132 | .scrapy 133 | 134 | # Sphinx documentation 135 | docs/_build/ 136 | 137 | # PyBuilder 138 | target/ 139 | 140 | # Jupyter Notebook 141 | .ipynb_checkpoints 142 | 143 | # pyenv 144 | .python-version 145 | 146 | # celery beat schedule file 147 | celerybeat-schedule.* 148 | 149 | # SageMath parsed files 150 | *.sage.py 151 | 152 | # Environments 153 | .env 154 | .venv 155 | env/ 156 | venv/ 157 | ENV/ 158 | env.bak/ 159 | venv.bak/ 160 | 161 | # Spyder project settings 162 | .spyderproject 163 | .spyproject 164 | 165 | # Rope project settings 166 | .ropeproject 167 | 168 | # mkdocs documentation 169 | /site 170 | 171 | # mypy 172 | .mypy_cache/ 173 | 174 | # End of https://www.gitignore.io/api/python,pycharm 175 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Current File", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal", 13 | "justMyCode": false 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes for swiftbat_python 2 | 3 | ## v 0.1.4 2023-08-05 4 | 5 | - Started CHANGES.md file 6 | - Replace `pyephem` with `skyfield` 7 | - pyephem is no longer supported 8 | - Pointing history replaced screen-scraping with `swifttool` database access 9 | - Appeared as DOS on the scraped website when I ran too often 10 | - General decrufting 11 | - reformatted with black 12 | - Code still suffers from being the first Python I ever wrote, on python2.6 13 | - Some (but not much) type hinting) 14 | - Requirements updated: 15 | - Python >= 3.9 16 | - astropy >= 5 17 | - skyfield >= 1.4 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Triad National Security, LLC. 2 | All rights reserved. 3 | 4 | This program was produced under U.S. Government contract 5 | 89233218CNA000001 for Los Alamos National Laboratory (LANL), 6 | which is operated by Triad National Security, LLC for the U.S. 7 | Department of Energy/National Nuclear Security Administration. 8 | 9 | All rights in the program are reserved by Triad National 10 | Security, LLC, and the U.S. Department of Energy/National 11 | Nuclear Security Administration. The Government is granted for 12 | itself and others acting on its behalf a nonexclusive, paid-up, 13 | irrevocable worldwide license in this material to reproduce, 14 | prepare derivative works, distribute copies to the public, 15 | perform publicly and display publicly, and to permit others to 16 | do so. 17 | 18 | Additionally, redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 19 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 20 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 21 | 3. Neither the name of Triad National Security, LLC, Los Alamos National Laboratory, LANL, the U.S. Government, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 22 | 23 | THIS SOFTWARE IS PROVIDED BY TRIAD NATIONAL SECURITY, LLC AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL TRIAD NATIONAL SECURITY, LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | This code was developed using funding from the National Aeronautics and Space Administration (NASA). 26 | 27 | Triad acknowledges that it will comply with the DOE OSS policy as follows: 28 | a. Submit form DOE F 241.4 to the Energy Science and Technology Software Center (ESTSC), 29 | b. Provide the unique URL on the form for ESTSC to distribute, and 30 | c. Maintain an OSS Record available for inspection by DOE. 31 | 32 | Following is a table briefly summarizing information for this software package: 33 | Identifying Number: C17139 34 | Software Name: Swiftbat Python Library 35 | Export Control Review Information: U.S. Department of Commerce, EAR99 36 | B&R Code: 450140194 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Swiftbat is a set of Python library routines and command-line utilities that have been developed for the purpose 2 | of retrieving, analyzing, and displaying data from NASA's Swift spacecraft, especially the data from the 3 | Swift Burst Alert Telescope (BAT). Development started before the launch of Swift for private use by the 4 | author and was how he learned Python. As a result, it is not a well-packaged, well-written, coherent library. 5 | 6 | All of this data is available from the Swift data archive, but some routines in this library use other access 7 | methods that are not available to the general public. These routines will not be useful except to Swift team members. 8 | This software is provided under the '3-clause BSD license' 9 | (https://opensource.org/licenses/BSD-3-Clause) . 10 | It is provided as-is with no expressed or implied warranty of fitness for any purpose. 11 | 12 | 13 | If you want to find the exposure of BAT to a point in the FOV, use 14 | ``` 15 | swiftbat.batExposure(theta, phi) 16 | ``` 17 | where theta is distance from boresight, phi is angle around the boresight, both in radians. 18 | 19 | This package also installs a command-line program 'swinfo' that 20 | tells you Swift Information such as what the MET (onboard-clock) 21 | time is, where Swift was pointing, and whether a specific source 22 | was above the horizon and/or in the field of view. 23 | ``` 24 | % swinfo 2020-05-05T12:34:56 -o -s "cyg X-1" 25 | Swift Zenith(RA,dec): 232.97, -20.46 26 | Swift Location(lon,lat,alt): -179.31 E, -20.53 N, 549 km 27 | Time(MET + UTCF -> UT): 610374920.889 + -24.888 -> 2020-05-05T12:34:55.999 28 | YYMMDD_SOD: 200505_45295.999 DOY=126 29 | Obs Sequence Number: 00033349058 30 | Obs Target Name: SGR 1935+2154 31 | Obs Date and Times: 2020-05-05 12:32:37 - 13:01:34 32 | Obs Pointing(ra,dec,roll): 293.724, 21.897, 62.77 33 | Cyg_X-1_imageloc (boresight_dist, angle): (14, -133) 34 | Cyg_X-1_exposure (cm^2 cos adjusted): 4230 35 | Cyg_X-1_altitude: 29 (up) 36 | ``` 37 | Use `swinfo --help` for more details. 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | copyright = """ 4 | Copyright (c) 2018, Triad National Security, LLC. All rights reserved. 5 | 6 | This program was produced under U.S. Government contract 7 | 89233218CNA000001 for Los Alamos National Laboratory (LANL), 8 | which is operated by Triad National Security, LLC for the U.S. 9 | Department of Energy/National Nuclear Security Administration. 10 | 11 | All rights in the program are reserved by Triad National 12 | Security, LLC, and the U.S. Department of Energy/National 13 | Nuclear Security Administration. The Government is granted for 14 | itself and others acting on its behalf a nonexclusive, paid-up, 15 | irrevocable worldwide license in this material to reproduce, 16 | prepare derivative works, distribute copies to the public, 17 | perform publicly and display publicly, and to permit others to 18 | do so. 19 | 20 | All rights in the program are reserved by Triad National Security, LLC, and the U.S. Department of Energy/National Nuclear Security Administration. The Government is granted for itself and others acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license in this material to reproduce, prepare derivative works, distribute copies to the public, perform publicly and display publicly, and to permit others to do so. 21 | Additionally, redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 22 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 23 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 24 | 3. Neither the name of Triad National Security, LLC, Los Alamos National Laboratory, LANL, the U.S. Government, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 25 | 26 | THIS SOFTWARE IS PROVIDED BY TRIAD NATIONAL SECURITY, LLC AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL TRIAD NATIONAL SECURITY, LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | 28 | """ 29 | 30 | try: 31 | with open("README.md", "r") as f: 32 | long_description = f.read() 33 | except FileNotFoundError: 34 | long_description = "" 35 | 36 | setup( 37 | name="swiftbat", 38 | version="0.1.4", 39 | packages=["swiftbat"], 40 | package_data={"": ["catalog", "recent_bcttb.fits.gz"]}, 41 | url="https://github.com/lanl/swiftbat_python/", 42 | license="BSD-3-Clause", 43 | author="David M. Palmer", 44 | author_email="palmer@lanl.gov", 45 | description="Routines for dealing with data from BAT on the Neil Gehrels Swift Observatory", 46 | long_description=long_description, 47 | long_description_content_type="text/markdown", 48 | entry_points={"console_scripts": ["swinfo=swiftbat.swinfo:swinfo_main"]}, 49 | install_requires=[ 50 | "astropy>=5", 51 | "astroquery", 52 | "numpy", 53 | "python-dateutil", 54 | "skyfield>=1.4", 55 | "swifttools", 56 | ], 57 | classifiers=[ 58 | "Development Status :: 4 - Beta", 59 | "Intended Audience :: Science/Research", 60 | "Programming Language :: Python", 61 | "Programming Language :: Python :: 3", 62 | "Topic :: Scientific/Engineering :: Astronomy", 63 | ], 64 | python_requires=">=3.9", 65 | ) 66 | -------------------------------------------------------------------------------- /swiftbat/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2018, Triad National Security, LLC 3 | All rights reserved. 4 | 5 | Copyright 2018. Triad National Security, LLC. All rights reserved. 6 | 7 | This program was produced under U.S. Government contract 89233218CNA000001 for Los Alamos National Laboratory (LANL), which is operated by Triad National Security, LLC for the U.S. Department of Energy/National Nuclear Security Administration. 8 | 9 | All rights in the program are reserved by Triad National Security, LLC, and the U.S. Department of Energy/National Nuclear Security Administration. The Government is granted for itself and others acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license in this material to reproduce, prepare derivative works, distribute copies to the public, perform publicly and display publicly, and to permit others to do so. 10 | 11 | Additionally, redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 12 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 13 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 14 | 3. Neither the name of Triad National Security, LLC, Los Alamos National Laboratory, LANL, the U.S. Government, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY TRIAD NATIONAL SECURITY, LLC AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL TRIAD NATIONAL SECURITY, LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 17 | 18 | """ 19 | 20 | from .sfmisc import sftime, sfts, loadsfephem 21 | from .clockinfo import utcf 22 | from .swutil import * 23 | from .swinfo import * 24 | from .batcatalog import BATCatalog 25 | from . import generaldir 26 | -------------------------------------------------------------------------------- /swiftbat/batcatalog.py: -------------------------------------------------------------------------------- 1 | """ 2 | BAT Catalog entries as derived from a bcttb.fits file 3 | """ 4 | 5 | """ 6 | Copyright (c) 2019, Triad National Security, LLC 7 | All rights reserved. 8 | 9 | Copyright 2019. Triad National Security, LLC. All rights reserved. 10 | 11 | This program was produced under U.S. Government contract 89233218CNA000001 for Los Alamos National Laboratory (LANL), which is operated by Triad National Security, LLC for the U.S. Department of Energy/National Nuclear Security Administration. 12 | 13 | All rights in the program are reserved by Triad National Security, LLC, and the U.S. Department of Energy/National Nuclear Security Administration. The Government is granted for itself and others acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license in this material to reproduce, prepare derivative works, distribute copies to the public, perform publicly and display publicly, and to permit others to do so. 14 | 15 | Additionally, redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 16 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 17 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 18 | 3. Neither the name of Triad National Security, LLC, Los Alamos National Laboratory, LANL, the U.S. Government, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY TRIAD NATIONAL SECURITY, LLC AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL TRIAD NATIONAL SECURITY, LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | """ 23 | 24 | from functools import lru_cache 25 | from astropy.io import fits 26 | import numpy as np 27 | from pathlib import Path 28 | from swiftbat import simbadlocation 29 | import re 30 | 31 | 32 | class BATCatalog: 33 | filebasename = "recent_bcttb.fits.gz" 34 | thisdir = Path(__file__).parent 35 | 36 | def __init__(self, catalogfilename=None): 37 | self.cattable = self._cattable(catalogfilename=catalogfilename) 38 | self.makeindices() 39 | 40 | def __getitem__(self, item): 41 | """ 42 | Index by catalog number, name, or something that resolves to same simplified name 43 | Returns only the fist match 44 | :param item: 45 | :return: 46 | """ 47 | return self.getall(item)[0] 48 | # IMPROVEME: Go to SIMBAD and resolve the source, get its position, and find BAT source near that 49 | 50 | def get(self, item, default=KeyError): 51 | """ 52 | Index by catalog number, name, or something that resolves to same simplified name 53 | :param item: 54 | :param default: What to return if nothing matches; reraise the KeyError if that is the default 55 | :return: 56 | """ 57 | try: 58 | return self.__getitem__(item) 59 | except KeyError: 60 | if default == KeyError: 61 | raise 62 | else: 63 | return default 64 | 65 | def getall(self, item): 66 | try: 67 | return self.bycatnum[int(item)] 68 | except (ValueError, KeyError): 69 | pass 70 | try: 71 | return self.byname[item] 72 | except KeyError: 73 | pass 74 | try: 75 | return self.bysimplename[item] 76 | except KeyError as e: 77 | try: 78 | simbadmatches = self.simbadmatch(item) 79 | if len(simbadmatches): 80 | return simbadmatches 81 | except: 82 | pass 83 | raise e # The KeyError, even though the last failure was by simbadmatch 84 | 85 | # IMPROVEME: Go to SIMBAD and resolve the source, get its position, and find BAT source near that 86 | 87 | def simbadmatch(self, item, tolerance=0.2): 88 | """ 89 | What rows match the catalogued 90 | :param item: 91 | :param tolerance: 92 | :return: 93 | """ 94 | ra, dec = simbadlocation(item) 95 | return self.positionmatch(ra, dec, tolerance) 96 | 97 | def positionmatch(self, radeg, decdeg, tolerance=0.2): 98 | rascale = np.cos(np.deg2rad(decdeg)) 99 | tol2 = tolerance**2 100 | raoff = ( 101 | (self.cattable["RA_OBJ"] - radeg) + 180 102 | ) % 360 - 180 # Handle the 360-0 wrap 103 | dist2 = (raoff * rascale) ** 2 + (self.cattable["DEC_OBJ"] - decdeg) ** 2 104 | return self.cattable[dist2 < tol2] 105 | 106 | def allnames(self): 107 | return set([row["NAME"] for row in self.cattable if row["NAME"]]) 108 | 109 | def makeindices(self): 110 | self.bycatnum = {} 111 | self.byname = {} 112 | self.bysimplename = {} 113 | for row in self.cattable: 114 | if not row["CATNUM"]: 115 | continue 116 | self.bycatnum.setdefault(row["CATNUM"], []).append(row) 117 | self.byname.setdefault(row["NAME"], []).append(row) 118 | self.bysimplename.setdefault(self.simplename(row["NAME"]), []).append(row) 119 | 120 | @lru_cache(maxsize=0) 121 | def _cattable(self, catalogfilename=None): 122 | # Recent catalog file e.g. 123 | # wget https://heasarc.gsfc.nasa.gov/FTP/swift/data/trend/2021_05/bat/bcatalog/sw03110986012bcttb.fits.gz -O recent_bcttb.fits.gz 124 | # Currently 2021_09 0654020283 125 | if catalogfilename is None: 126 | catalogfilename = self.thisdir.joinpath(self.filebasename) 127 | catalog = fits.getdata(catalogfilename) 128 | return catalog 129 | 130 | def simplename(self, name): 131 | """ 132 | A simple name is the alphanumerics of the name in lower case. 133 | This folds + and - dec designations, but in practice not a problem 134 | :param name: 135 | :return: 136 | """ 137 | return re.sub("[^a-z0-9]", "", name.lower()) 138 | -------------------------------------------------------------------------------- /swiftbat/clockinfo.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import glob 4 | import datetime 5 | from pathlib import Path 6 | from .generaldir import httpDir 7 | from astropy.io import fits 8 | 9 | 10 | """ 11 | From the FITS file: 12 | COMMENT 13 | COMMENT Swift clock correction file 14 | COMMENT 15 | COMMENT This file is used by tools that correct Swift times for the measured 16 | COMMENT offset of the spacecraft clock based on fits to data provided by the 17 | COMMENT Swift MOC. The columns contain polynomial coefficients for various time 18 | COMMENT ranges and are used to compute the appropriate clock correction for any 19 | COMMENT given mission elapsed time (after the first post-launch measurement). 20 | COMMENT The technique is very similar to that used for RXTE fine timing. 21 | COMMENT 22 | COMMENT In contrast to RXTE where these fine clock corrections have magnitudes 23 | COMMENT measured in the tens of microseconds, the Swift corrections are much 24 | COMMENT larger (~0.7 seconds at launch). At this writing (June 2005) the 25 | COMMENT spacecraft clock still lags behind reference clocks on the ground but 26 | COMMENT also runs fast. The spacecraft clock is currently accelerating slowly. 27 | COMMENT 28 | COMMENT Time is divided into intervals, expressed in spacecraft MET. 29 | COMMENT Columns are: TSTART TSTOP TOFFSET C0 C1 C2 TYPE CHI_N DOF 30 | COMMENT where 31 | COMMENT TSTART and TSTOP are the interval boundaries (MET), 32 | COMMENT TOFFSET is the clock offset (ie, TIMEZERO), 33 | COMMENT MAXGAP is the largest gap with no data (-1 if unknown), 34 | COMMENT CHI_N is the reduced chi-square value (-1 if unknown), 35 | COMMENT DOF is the number of degrees of freedom, 36 | COMMENT TYPE indicates the segment clock continuity (0=YES; 1=NO). 37 | COMMENT (MAXGAP, CHI_N, DOF, and TYPE are retained for continuity with 38 | COMMENT the file format used by RXTE but the values are currently all 39 | COMMENT defaults and are not used in computing the actual clock offset.) 40 | COMMENT 41 | COMMENT For a mission time of T, the correction in seconds is computed 42 | COMMENT with the following: 43 | COMMENT T1 = (T-TSTART)/86400 44 | COMMENT TCORR = TOFFSET + (C0 + C1*T1 + C2*T1*T1)*1E-6 45 | """ 46 | 47 | theClockData = None 48 | 49 | caveat_time = 86400 * 90 # print a caveat if UTCF is more than 90 days stale 50 | 51 | 52 | class clockErrData: 53 | clockurl = "https://heasarc.gsfc.nasa.gov/FTP/swift/calib_data/sc/bcf/clock/" 54 | clockhost = "heasarc.gsfc.nasa.gov" 55 | clockhostdir = "/caldb/data/swift/mis/bcf/clock/" 56 | clockfile_regex = "swclockcor20041120v\d*.fits" 57 | clockfilepattern = "swclockcor20041120v*.fits" 58 | # FIXME this should be derived from the dotswift params 59 | clocklocalsearchpath = [ 60 | "/opt/data/Swift/swift-trend/clock", 61 | os.path.expanduser("~/.swift/swiftclock"), 62 | "/tmp/swiftclock", 63 | ] 64 | 65 | def __init__(self): 66 | try: 67 | self._clockfile = self.clockfile() 68 | 69 | f = fits.open(self._clockfile) 70 | # copies are needed to prevent pyfits from holding open a file handle for each item 71 | self._tstart = f[1].data.field("TSTART").copy() 72 | self._tstop = f[1].data.field("TSTOP").copy() 73 | self._toffset = f[1].data.field("TOFFSET").copy() 74 | self._c0 = f[1].data.field("C0").copy() 75 | self._c1 = f[1].data.field("C1").copy() 76 | self._c2 = f[1].data.field("C2").copy() 77 | f.close() 78 | except: 79 | self._clockfile = "" 80 | raise RuntimeError("No clock UTCF file") 81 | 82 | def utcf( 83 | self, t, trow=None 84 | ): # Returns value in seconds to be added to MET to give correct UTCF 85 | if not self._clockfile: 86 | return 0.0, "No UTCF file" 87 | if trow is None: 88 | trow = t # What time to use for picking out the row 89 | caveats = "" 90 | # Assume that the times are in order, and take the last one before t 91 | row = [i for i in range(len(self._tstart)) if self._tstart[i] <= trow] 92 | if len(row) == 0: 93 | row = 0 94 | caveats += "Time before first clock correction table entry" 95 | else: 96 | row = row[-1] 97 | if self._tstop[row] + caveat_time < t: 98 | caveats += "Time %.1f days after clock correction interval\n" % ( 99 | (t - self._tstop[row]) / 86400 100 | ) 101 | ddays = (t - self._tstart[row]) / 86400.0 102 | tcorr = self._toffset[row] + 1e-6 * ( 103 | self._c0[row] + ddays * (self._c1[row] + ddays * self._c2[row]) 104 | ) 105 | return -tcorr, caveats 106 | 107 | def updateclockfiles(self, clockdir, ifolderthan_days=30, test_days=1): 108 | """ 109 | Update the clock files if the current clockfile is old and we haven't checked recently for new ones 110 | :param clockdir: 111 | :param ifolderthan_days: 112 | :param test_days: 113 | :return: 114 | """ 115 | testfile = os.path.join(clockdir, "clocktest") 116 | try: 117 | clockfile = sorted( 118 | list(glob.glob(os.path.join(clockdir, self.clockfilepattern))) 119 | )[-1] 120 | age = datetime.datetime.utcnow() - datetime.datetime.fromtimestamp( 121 | os.path.getmtime(clockfile) 122 | ) 123 | if age.total_seconds() < (86400 * ifolderthan_days): 124 | return 125 | # Check no more than once a day 126 | testage = datetime.datetime.utcnow() - datetime.datetime.fromtimestamp( 127 | os.path.getmtime(testfile) 128 | ) 129 | if testage.total_seconds() < (86400 * test_days): 130 | return 131 | except: 132 | pass 133 | try: 134 | clockremotedir = httpDir(self.clockurl) 135 | clockremote = sorted(clockremotedir.getMatches("", self.clockfile_regex))[ 136 | -1 137 | ] 138 | locallatest = Path(clockdir).joinpath(Path(clockremote).name) 139 | clockremotedir.copyToFile(clockremote, locallatest) 140 | open(testfile, "w").write(" ") # touch 141 | except Exception as e: 142 | print(e, file=sys.stdout) 143 | 144 | def clockfile(self): 145 | for clockdir in self.clocklocalsearchpath: 146 | # Directory must exist and have readable clock files in it. 147 | if os.path.exists(clockdir): 148 | try: 149 | clockfile = sorted( 150 | list(glob.glob(os.path.join(clockdir, self.clockfilepattern))) 151 | )[-1] 152 | clockdata = fits.getdata(clockfile) 153 | except: 154 | continue 155 | break 156 | else: 157 | for clockdir in self.clocklocalsearchpath: 158 | if os.path.exists(os.path.dirname(clockdir)): 159 | # If the parent exists, it is ok to add the clockdir to it if possible, 160 | # but don't want to build a whole new directory tree 161 | try: 162 | os.mkdir(clockdir) 163 | break 164 | except: 165 | pass 166 | else: 167 | try: 168 | os.makedirs( 169 | clockdir 170 | ) # Force directory to exist in the temp directory 171 | except FileExistsError: 172 | pass 173 | self.updateclockfiles(clockdir) 174 | clockfile = sorted( 175 | list(glob.glob(os.path.join(clockdir, self.clockfilepattern))) 176 | )[-1] 177 | return clockfile 178 | 179 | 180 | def utcf(met, printCaveats=True, returnCaveats=False): 181 | """ 182 | Correction factor to add to MET to get UTC 183 | :param met: 184 | :param printCaveats: 185 | :param returnCaveats: 186 | :return: 187 | """ 188 | global theClockData 189 | if theClockData == None: 190 | theClockData = clockErrData() 191 | try: 192 | uc = [theClockData.utcf(t_) for t_ in met] 193 | u, c = zip(*uc) 194 | if printCaveats and any(c): 195 | print("\n".join(["**** " + c_ for c_ in c if c_])) 196 | except TypeError: 197 | u, c = theClockData.utcf(met) 198 | if printCaveats and c: 199 | print("**** " + c) 200 | if returnCaveats: 201 | return u, c 202 | else: 203 | return u 204 | -------------------------------------------------------------------------------- /swiftbat/generaldir.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | """ 4 | generaldir: 5 | Treat http, ftp, and local files equally as a general directory structure 6 | Requires that the http server be apache with autoindex. 7 | The ftp requirements are that the dir listing begin with the flags and end with the name 8 | 9 | David Palmer palmer@lanl.gov 10 | (C) Copyright 2009 Triad National Security, LLC All rights reserved 11 | 12 | """ 13 | 14 | import sys 15 | import os 16 | from urllib.request import urlopen 17 | import re 18 | import shutil 19 | import stat 20 | 21 | 22 | def subTemplate( 23 | origstring, keydict, allcaps=True 24 | ): # if string includes '${FOO}' and keydict['FOO'] defined, substitute 25 | # The Template class in Python 2.4 would be the better way to do it, except that 2.4 was released yesterday (11/30/04) 26 | # If allcaps is true, the keys of keydict must be all caps, although the ${foo} need not be 27 | s = origstring 28 | found = re.compile(r"""\$\{([^}]+)\}""").findall(s) 29 | # print(s," found ",found) 30 | for v in found: 31 | if allcaps: 32 | vu = v.upper() 33 | else: 34 | vu = v 35 | try: 36 | if vu in keydict: 37 | s = s.replace("${" + v + "}", keydict[vu]) 38 | # print("->",s) 39 | except: 40 | print("replace of ${%s} failed" % v) 41 | raise 42 | return s 43 | 44 | 45 | class generalDir: 46 | _rewildsplitter = re.compile( 47 | r"""(?P(/*[^$/]+[/]+)*)(?P[^/]*)/*(?P.*)""" 48 | ) 49 | 50 | def __init__(self, url): 51 | self.url = url 52 | self.lastsubpath = None 53 | self.lastreadout = None 54 | 55 | def getMatches(self, subpath, reg): 56 | return re.compile(reg).findall(self.gettext(subpath)) 57 | 58 | def gettext(self, subpath): 59 | return self.getdata(subpath).decode("utf8") 60 | 61 | def getdata(self, subpath): 62 | if self.lastsubpath != subpath or self.lastreadout == None: 63 | # Cache value so that when we request both files and dirs, only one net trans. made 64 | self.lastsubpath = subpath 65 | self.lastreadout = self.openSub(subpath).read() 66 | return self.lastreadout 67 | 68 | def exists(self, subpath=""): 69 | """Check if referenced object exists by trying to open it""" 70 | try: 71 | o = self.openSub(subpath) 72 | o.close() 73 | return True 74 | except: 75 | return False 76 | 77 | def openSub(self, subpath): 78 | # print("Opening ",self.url + "/" + subpath) 79 | try: 80 | # print("\r"+self.url + "/" + subpath,) 81 | return urlopen(self.url + "/" + subpath) 82 | except: 83 | print("Unexpected error:", sys.exc_info()[0]) 84 | print(self.url + "/" + subpath) 85 | raise 86 | 87 | def makeDirectoryForFile(self, fname): 88 | try: 89 | os.makedirs(os.path.dirname(fname)) 90 | except FileExistsError: 91 | pass # catch exception thrown when dir already present 92 | 93 | def copyToFile(self, subpath, fname): 94 | self.makeDirectoryForFile(fname) 95 | shutil.copyfileobj( 96 | self.openSub(subpath), open(fname, "wb") 97 | ) # order is src, dest 98 | 99 | def matchPath(self, subpath, wildpath, matchdict=None, regexdict=None): 100 | """given a subpath and a wildcard path, with named values and 101 | regular expressions in matchdict and regexdict, find directories that match 102 | the pattern and the pattern matched as a list of tuples (path, matchingdict) 103 | """ 104 | # print("subpath = ",subpath) 105 | if subpath == None: 106 | subpath = "" 107 | (unwild, firstwild, restwild) = self._splitAndSub( 108 | wildpath, matchdict, regexdict 109 | ) 110 | # print("join (%s,%s)" % (subpath,unwild)) 111 | subpath = os.path.join(subpath, unwild) # Add the non-wild stuff to the subpath 112 | # print("->",subpath) 113 | if not self.exists( 114 | subpath 115 | ): # if the unwild stuff doesn't exist then this is a dead end 116 | # print("Deadend : %s / %s" % (self.url,subpath)) 117 | return [] 118 | if not firstwild: # if there is no wild part 119 | assert not restwild # there must not be any more wild part 120 | # we already checked for existence, so this is a good match 121 | return [(subpath, matchdict.copy())] 122 | else: # There is still some wildness left 123 | results = [] 124 | matchstring = subTemplate(firstwild, regexdict) 125 | if -1 != matchstring.find("$"): 126 | print( 127 | "Did not substitute variable regular expression in %s" % matchstring 128 | ) 129 | raise RuntimeError( 130 | "Did not substitute variable regular expression in %s" % matchstring 131 | ) 132 | dirmatchre = re.compile( 133 | matchstring + "/*$" 134 | ) # optional / allowed for directory 135 | dirlist = self.dirs(subpath) 136 | # print("in",subpath,"trying to match string",matchstring,"against",dirlist) 137 | # print("wildness remaining:",restwild) 138 | for d in dirlist: 139 | m = dirmatchre.match(d) 140 | if m: # d matches the first wild bit 141 | newsubpath = os.path.join(subpath, d) 142 | # print("adding",d,"to get",newsubpath) 143 | md = ( 144 | matchdict.copy() 145 | ) # don't disturb original from one recursion level up 146 | md.update(m.groupdict()) # add newly discovered variables 147 | if restwild: # more to match 148 | results.extend( 149 | self.matchPath(newsubpath, restwild, md, regexdict) 150 | ) 151 | else: # nothing more 152 | results.append((newsubpath, md)) 153 | if not restwild: # If there is no more wildness, then match might be a file 154 | filelist = self.files(subpath) 155 | filematchre = re.compile(matchstring + "$") # ending slash is forbidden 156 | for f in filelist: 157 | m = filematchre.match(f) 158 | if m: # file match 159 | newsubpath = os.path.join(subpath, f) 160 | md = ( 161 | matchdict.copy() 162 | ) # don't disturb original from one recursion level up 163 | md.update(m.groupdict()) # add newly discovered variables 164 | results.append((newsubpath, md)) 165 | return results 166 | 167 | def _splitAndSub(self, wildpath, matchdict, regexdict): 168 | """returns a (topUNwildpath, firstwildness, restofwildpath) tuple with matches and regexes subbed in""" 169 | wildpath = subTemplate(wildpath, matchdict) # Do all the matching you can 170 | if -1 == wildpath.find("$"): 171 | return (wildpath, "", "") 172 | m = self._rewildsplitter.match(wildpath) 173 | # print(wildpath+"-->",m.groupdict()) 174 | if not m or (not m.groupdict()["firstwild"] and m.groupdict()["restwild"]): 175 | # no match if totally non-wild and unslashed. Not /-terminated, but handled above 176 | assert False 177 | assert -1 == wildpath.find("$") 178 | return (wildpath, "", "") 179 | return ( 180 | m.groupdict()["unwild"], 181 | m.groupdict()["firstwild"], 182 | m.groupdict()["restwild"], 183 | ) 184 | 185 | 186 | class httpDir(generalDir): 187 | """HTTP specialization for the directory generalization 188 | Works on apache servers with autoindex generation 189 | A directory is read by reading from its URL with a trailing /. (Reading without 190 | a trailing slash gives a redirection that this lib can't follow.) In the 191 | text that comes out, subdirectories and files are represented by links where 192 | the href is the name (with a trailing slash for subdirs.) with the close quotes, 193 | close angle brackets, and then a repetition of the name (likewise with the trailing / 194 | for dirs), except the name is truncated if it is too long. In Swift, filenames 195 | can be long, but directory names are short enough to not be truncated. 196 | 197 | This can be fixed by adding ?F=0 (fancy listing off) to the end of the URL. See 198 | http://httpd.apache.org/docs-2.0/mod/mod_autoindex.html 199 | """ 200 | 201 | # 2008-06-30 added star to " " because some HTTP servers do not put a space in front 202 | dirmatch = re.compile( 203 | r"""(?P[^\?"/]+)/"> *(?P=foundname)/""", re.MULTILINE | re.IGNORECASE 204 | ) 205 | filematch = re.compile( 206 | r"""(?P[^\?"/]+)"> *(?P=foundname)""", re.MULTILINE | re.IGNORECASE 207 | ) 208 | # And some servers do not evevn understand the /?F=0 non-fancy readout 209 | filenofancymatch = re.compile( 210 | r"""HREF="(?P[^\?"/]+)">""", re.MULTILINE | re.IGNORECASE 211 | ) 212 | 213 | validurlmatch = re.compile("https?://", re.IGNORECASE) 214 | 215 | def __init__(self, url): 216 | if not self.validurlmatch.search(url): 217 | raise RuntimeError("Not an http url: %s" % url) 218 | generalDir.__init__(self, url) 219 | self.filecache = {} 220 | self.dircache = {} 221 | # print("HTTP works on %s", url) 222 | 223 | def files(self, subdir=""): 224 | try: 225 | return self.filecache[subdir] 226 | except: 227 | files = self.getMatches(subdir + "/?F=0", self.filematch) 228 | self.filecache[subdir] = files 229 | return files 230 | 231 | def filesNoFancy(self, subdir=""): # 232 | files = self.getMatches(subdir + "/?F=0", self.filenofancymatch) 233 | return files 234 | 235 | def dirs(self, subdir=""): 236 | try: 237 | return self.dircache[subdir] 238 | except: 239 | dirs = self.getMatches(subdir + "/?F=0", self.dirmatch) 240 | self.dircache[subdir] = dirs 241 | return dirs 242 | 243 | def getFullPath(self, urlrelativepathname): 244 | return os.path.join(self.url, urlrelativepathname) 245 | 246 | 247 | class ftpDir(generalDir): 248 | _filelinematch = re.compile( 249 | r"""(?<=^-[r-][w-].[r-][w-].r..\s).+$""", re.MULTILINE | re.IGNORECASE 250 | ) 251 | _dirlinematch = re.compile( 252 | r"""(?<=^d[r-][w-].[r-][w-].r..\s).+$""", re.MULTILINE | re.IGNORECASE 253 | ) 254 | _namematch = re.compile("\S+\s*$") # last string of nonwhite characters before eol 255 | 256 | def __init__(self, url): 257 | if not re.compile("ftp://", re.IGNORECASE).search(url): 258 | raise RuntimeError("Not a valid ftp url: %s" % url) 259 | generalDir.__init__(self, url) 260 | 261 | def files(self, subdir=""): 262 | lines = self.getMatches(subdir + "/", self._filelinematch) 263 | return [self._namematch.findall(l.strip())[0] for l in lines] 264 | 265 | def dirs(self, subdir=""): 266 | lines = self.getMatches(subdir + "/", self._dirlinematch) 267 | return [self._namematch.findall(l.strip())[0] for l in lines] 268 | 269 | 270 | class localDir(generalDir): 271 | def __init__(self, url): 272 | # print("Trying %s as local file" % url) 273 | nofileurl = re.compile(r"""(?<=FILE://)([^>]*)""", re.IGNORECASE).findall(url) 274 | if len(nofileurl): 275 | self.dirname = os.path.realpath(nofileurl[0]) 276 | else: 277 | self.dirname = os.path.realpath(url) 278 | generalDir.__init__(self, "FILE://" + self.dirname) 279 | 280 | def dirs(self, subdir=""): 281 | d = os.path.join(self.dirname, subdir) 282 | # IMPROVEME use os.scandir for python >= 3.5 283 | if hasattr(os, "scandir"): 284 | # Python >= 3.5 285 | return [ 286 | x.name for x in os.scandir("/Volumes/DATA/Swift/swift") if x.is_dir() 287 | ] 288 | else: 289 | allindir = os.listdir(d) 290 | # FIXME chokes on aliases or links to files that don't exist 291 | # IMPROVEME use os.path.isdir 292 | return [ 293 | x 294 | for x in allindir 295 | if stat.S_ISDIR(os.stat(os.path.join(d, x))[stat.ST_MODE]) 296 | ] 297 | 298 | def files(self, subdir=""): 299 | d = os.path.join(self.dirname, subdir) 300 | if hasattr(os, "scandir"): 301 | # Python >= 3.5 302 | return [ 303 | x.name for x in os.scandir("/Volumes/DATA/Swift/swift") if x.is_file() 304 | ] 305 | else: 306 | allindir = os.listdir(d) 307 | return [ 308 | x 309 | for x in allindir 310 | if stat.S_ISREG(os.stat(os.path.join(d, x))[stat.ST_MODE]) 311 | ] 312 | 313 | def openSub(self, subpath): 314 | open(os.path.join(self.dirname, subpath)) 315 | 316 | def exists(self, subpath=""): 317 | """Check if referenced object exists by trying to open it""" 318 | try: 319 | # print("does %s exist?" % os.path.join(self.dirname,subpath)) 320 | mode = os.stat(os.path.join(self.dirname, subpath))[stat.ST_MODE] 321 | except: 322 | # print("No") 323 | # print(os.stat(os.path.join(self.dirname,subpath))) 324 | return False 325 | if stat.S_ISREG(mode) or stat.S_ISDIR(mode): 326 | # print("Yes") 327 | return True 328 | else: 329 | # print("Not as a file or directory") 330 | return False 331 | 332 | def makedirs(self, subpath): 333 | """No analog method or HTTP and FTP genDirectories. 334 | Make the entire string of directories to the given subpath""" 335 | try: 336 | os.makedirs(os.path.join(self.dirname, subpath)) 337 | except: 338 | pass 339 | 340 | def getFullPath(self, urlrelativepathname): 341 | return os.path.join(self.dirname, urlrelativepathname) 342 | 343 | 344 | def getDir(url): 345 | for trialclass in (httpDir, ftpDir, localDir): 346 | # print("trying ", trialclass) 347 | try: 348 | return trialclass(url) 349 | except: 350 | pass 351 | print("Can't open %s" % url) 352 | 353 | 354 | def dive(url, depth, indent=0): 355 | d = getDir(url) 356 | if d: 357 | dirs = d.dirs() 358 | files = d.files() 359 | instring = ("%%%0is" % indent) % " " 360 | print(instring, "%s:" % url) 361 | instring += " " 362 | if len(files): 363 | print(instring, files) 364 | if depth > 0: 365 | for subdir in dirs: 366 | dive(url + "/" + subdir, depth - 1, indent + 4) 367 | elif len(dirs): 368 | print(instring, "D:", dirs) 369 | 370 | 371 | def main(): 372 | regexdict = { 373 | "SEQNUM": "(?P[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9])", 374 | "OBSERVATORY": "(?Psw)", 375 | "VERSION": "(?P[0-9][0-9][0-9])", 376 | "TYPE": "(?P\\W+)", 377 | "CODINGSUFFIXES": "(?P(.gz|.pgp)*)", 378 | } 379 | for url in ( 380 | "https://heasarc.gsfc.nasa.gov/FTP/swift/data/", 381 | "https://heasarc.gsfc.nasa.gov/FTP/swift/data/obs/2005_04", 382 | # 'http://swift.gsfc.nasa.gov/SDC/data/local/data1/data', # Previous location of quicklook 383 | # 'https://swift.gsfc.nasa.gov/data/swift/' # No longer works (gives a page instead of a dir listing) 384 | ): 385 | print(url) 386 | archive = getDir(url) 387 | print("----------------") 388 | dirpaths = archive.matchPath("", "(sw)?${seqnum}(.${version})?", {}, regexdict) 389 | print("----------------") 390 | for obsdir, d in dirpaths: 391 | print(obsdir, d["SEQNUM"], d["VERSION"]) 392 | print("----------------") 393 | # print(archdir.matchPath("","sw${seqnum}.${version}",{},cache.regexdict)) 394 | # print(archdir.matchPath("","sw${seqnum}.023"+"/"+cache.typepatterns['pob.cat']+'${CODINGSUFFIXES}',{},cache.regexdict)) 395 | # print(archdir.matchPath("",cache.hierdict['DIRPATTERN']+"/"+cache.typepatterns['pob.cat']+'${CODINGSUFFIXES}',{},cache.regexdict)) 396 | if len(sys.argv) <= 1: 397 | # dive("http://swift.gsfc.nasa.gov/SDC/data/local/data1/data/", 2) 398 | # dive("file:///tmp", 3) 399 | # dive("ftp://anonymous:palmer%40lanl.gov@heasarc.gsfc.nasa.gov/swift/data/", 2) 400 | # print(swiftCache() 401 | pass 402 | else: 403 | dive(sys.argv[1], 2) 404 | 405 | 406 | if __name__ == "__main__": 407 | main() 408 | -------------------------------------------------------------------------------- /swiftbat/recent_bcttb.fits.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lanl/swiftbat_python/8abd7ab2909af2bd35b92e356b6516e4534730e1/swiftbat/recent_bcttb.fits.gz -------------------------------------------------------------------------------- /swiftbat/sfmisc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Skyfield imports, various utilities 3 | """ 4 | 5 | """ 6 | Copyright (c) 2018, Triad National Security, LLC. All rights reserved. 7 | 8 | This program was produced under U.S. Government contract 9 | 89233218CNA000001 for Los Alamos National Laboratory (LANL), 10 | which is operated by Triad National Security, LLC for the U.S. 11 | Department of Energy/National Nuclear Security Administration. 12 | 13 | All rights in the program are reserved by Triad National 14 | Security, LLC, and the U.S. Department of Energy/National 15 | Nuclear Security Administration. The Government is granted for 16 | itself and others acting on its behalf a nonexclusive, paid-up, 17 | irrevocable worldwide license in this material to reproduce, 18 | prepare derivative works, distribute copies to the public, 19 | perform publicly and display publicly, and to permit others to 20 | do so. 21 | 22 | Additionally, redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 23 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 24 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 25 | 3. Neither the name of Triad National Security, LLC, Los Alamos National Laboratory, LANL, the U.S. Government, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 26 | 27 | THIS SOFTWARE IS PROVIDED BY TRIAD NATIONAL SECURITY, LLC AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL TRIAD NATIONAL SECURITY, LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | """ 30 | 31 | 32 | import skyfield.api as sfapi 33 | import astropy as ap 34 | import numpy as np 35 | import datetime 36 | from functools import lru_cache as __lru_cache 37 | from dateutil.parser import parse as parsedate 38 | 39 | load = sfapi.Loader("~/.skyfield", verbose=False) 40 | sfts = load.timescale(builtin=True) 41 | 42 | 43 | @__lru_cache(0) 44 | def loadsfephem(): 45 | return load("de421.bsp") 46 | 47 | 48 | @__lru_cache(0) 49 | def __units_in_days(units): 50 | # prefered units in days 51 | if units is None: 52 | return 1.0 53 | units = units.strip().lower() 54 | if "days".startswith(units.lower()): 55 | u = 1.0 56 | elif "seconds".startswith(units): 57 | u = 1 / 86400.0 58 | elif "minutes".startswith(units): 59 | u = 1 / (24 * 60) 60 | elif "hours".startswith(units): 61 | u = 1 / 24 62 | elif "years".startswith(units): 63 | u = 365.24217 64 | else: 65 | raise NotImplementedError(f"Don't know how to convert {units} to days") 66 | return u 67 | 68 | 69 | def __dt_in_days(dt, units=None): 70 | if np.isscalar(dt): 71 | if isinstance(dt, (float, int)): 72 | return dt * __units_in_days(units) 73 | elif isinstance(dt, datetime.timedelta): 74 | return dt.total_seconds() / 86400 # Don't do a unit conversion 75 | elif isinstance(dt, ap.TimeDelta): 76 | return dt.sec / 86400 # Don't do a unit conversion' 77 | else: 78 | raise NotImplementedError(f"Don't know how to convert {type(dt)} to days") 79 | else: 80 | return np.array([__dt_in_days(dt_, units) for dt_ in dt]) 81 | 82 | 83 | def _asutc(t: datetime.datetime) -> datetime.datetime: 84 | try: 85 | return [_asutc(t_) for t_ in t] 86 | except: 87 | pass 88 | 89 | # Convert naive or aware timezone to UTC 90 | if t.tzinfo is not None and t.tzinfo.utcoffset(None) is not None: 91 | # timezone-aware. Convert to UTC 92 | return t.astimezone(datetime.timezone.utc) 93 | else: # Naive time 94 | # Add tzinfo to a naive time that represents a UTC 95 | return t.replace(tzinfo=datetime.timezone.utc) 96 | 97 | 98 | def sftime( 99 | t, plus=None, units="days", stepby=None, nsteps=None, stepto=None, endpoint=True 100 | ): 101 | """Convert other times to 102 | 103 | Args: 104 | t (_type_): _description_ 105 | plus (_type_, optional): _description_. Defaults to None. 106 | units (str, optional): _description_. Defaults to "days". 107 | stepby (_type_, optional): _description_. Defaults to None. 108 | nsteps (_type_, optional): _description_. Defaults to None. 109 | stepto (_type_, optional): _description_. Defaults to None. 110 | """ 111 | if isinstance(t, str): 112 | t = [t] 113 | scalar = True 114 | else: 115 | try: 116 | t[0] 117 | scalar = False 118 | except (IndexError, TypeError): 119 | scalar = True 120 | t = [t] 121 | 122 | if not isinstance(t[0], sfapi.Time): 123 | if isinstance(t[0], datetime.datetime): 124 | t = sfts.from_datetimes(_asutc(t)) 125 | elif isinstance(t[0], ap.time.Time): 126 | t = sfts.from_astropy(t[0]) 127 | elif isinstance(t[0], float): 128 | t = sfts.tai_jd(t) 129 | elif hasattr(t[0], "datetime"): # Pyephem 130 | t = sfts.from_datetimes([_asutc(t_.datetime()) for t_ in t]) 131 | elif isinstance(t[0], str): 132 | t = sfts.from_datetimes([_asutc(parsedate(t_)) for t_ in t]) 133 | else: 134 | raise NotImplementedError( 135 | f"Don't know how to change {type(t[0])} into skyfield time" 136 | ) 137 | 138 | t_tai = np.array([t_.tai for t_ in t]) 139 | 140 | if plus is not None: 141 | t_tai += __dt_in_days(plus, units=units) 142 | 143 | nstepkw = (stepby is not None) + (nsteps is not None) + (stepto is not None) 144 | if nstepkw != 0: 145 | scalar = False 146 | 147 | if stepby is not None: 148 | stepby = __dt_in_days(stepby, units=units) 149 | if stepto is None: 150 | if len(t_tai) >= 2: 151 | t_tai, stepto_tai = t_tai[:-1], t_tai[-1] 152 | nstepkw += 1 153 | else: 154 | stepto_tai = sftime(stepto).tai 155 | if len(t_tai) != 1: 156 | raise RuntimeError( 157 | "Only one starting point allowed for stepped time arrays" 158 | ) 159 | if nstepkw != 2: 160 | raise RuntimeError("Need two (or zero) of {stepby, stepto, nsteps}") 161 | if stepby is None: 162 | dt_tai = np.linspace( 163 | 0, stepto_tai - t_tai[0], num=nsteps, endpoint=endpoint 164 | ) 165 | elif nsteps is None: 166 | dt_tai = np.arange(0, stepto_tai - t_tai[0], stepby) 167 | else: 168 | dt_tai = np.arange(0, nsteps) * stepby 169 | t_tai = t_tai[0] + dt_tai 170 | 171 | if scalar and len(t_tai) == 1: 172 | t_tai = t_tai[0] 173 | return sfts.tai_jd(t_tai) 174 | -------------------------------------------------------------------------------- /swiftbat/swinfo.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | """ 4 | swinfo 5 | More utilities for dealing with Swift Data 6 | David Palmer palmer@lanl.gov 7 | 8 | 9 | """ 10 | 11 | """ 12 | Copyright (c) 2018, Triad National Security, LLC. All rights reserved. 13 | 14 | This program was produced under U.S. Government contract 15 | 89233218CNA000001 for Los Alamos National Laboratory (LANL), 16 | which is operated by Triad National Security, LLC for the U.S. 17 | Department of Energy/National Nuclear Security Administration. 18 | 19 | All rights in the program are reserved by Triad National 20 | Security, LLC, and the U.S. Department of Energy/National 21 | Nuclear Security Administration. The Government is granted for 22 | itself and others acting on its behalf a nonexclusive, paid-up, 23 | irrevocable worldwide license in this material to reproduce, 24 | prepare derivative works, distribute copies to the public, 25 | perform publicly and display publicly, and to permit others to 26 | do so. 27 | 28 | Additionally, redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 29 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 30 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 31 | 3. Neither the name of Triad National Security, LLC, Los Alamos National Laboratory, LANL, the U.S. Government, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 32 | 33 | THIS SOFTWARE IS PROVIDED BY TRIAD NATIONAL SECURITY, LLC AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL TRIAD NATIONAL SECURITY, LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | 35 | """ 36 | 37 | 38 | import os 39 | import sys 40 | import functools 41 | from pathlib import Path 42 | import re 43 | import datetime 44 | import shutil 45 | import glob 46 | import gzip 47 | import traceback 48 | import getopt 49 | import time 50 | from . import swutil 51 | from .clockinfo import utcf 52 | from . import sftime, sfts, loadsfephem 53 | import astropy.units as u 54 | from astropy.io import fits 55 | from astropy import coordinates 56 | from astropy.coordinates import ICRS, SkyCoord, Angle as apAngle 57 | import numpy as np 58 | from io import StringIO 59 | from functools import lru_cache as __lru_cache 60 | import swifttools.swift_too as swto 61 | from urllib.request import urlopen, quote 62 | from swiftbat import generaldir 63 | import skyfield.api as sf_api 64 | from skyfield.trigonometry import position_angle_of as sf_position_angle_of 65 | from skyfield.positionlib import ICRF as sf_ICRF 66 | import sqlite3 # Good sqlite3 tutorial at http://www.w3schools.com/sql/default.asp 67 | from typing import List, Tuple 68 | 69 | 70 | split_translate = str.maketrans("][, \t;", " ") 71 | # Skyfield interfaces 72 | _sf_load = sf_api.Loader("~/.skyfield", verbose=False) 73 | _sf_ts = _sf_load.timescale(builtin=True) 74 | 75 | 76 | @__lru_cache(0) 77 | def loadsfephem(): 78 | return _sf_load("de421.bsp") 79 | 80 | 81 | _sf_timescale = sf_api.load.timescale(builtin=True) 82 | 83 | 84 | def adapt_boolean(bol): 85 | if bol: 86 | return "True" 87 | else: 88 | return "False" 89 | 90 | 91 | def convert_boolean(bolStr): 92 | if str(bolStr) == "True": 93 | return bool(True) 94 | elif str(bolStr) == "False": 95 | return bool(False) 96 | else: 97 | raise ValueError("Unknown value of bool attribute '%s'" % bolStr) 98 | 99 | 100 | sqlite3.register_adapter(bool, adapt_boolean) 101 | sqlite3.register_converter("boolean", convert_boolean) 102 | 103 | execdir = os.path.dirname(os.path.abspath(os.path.realpath(__file__))) 104 | 105 | basecatalog = os.path.join(execdir, "catalog") 106 | fitscatalog = os.path.join(execdir, "recent_bcttb.fits.gz") 107 | # FIXME should be handled by dotswift 108 | catalogdir = "/opt/data/Swift/analysis/sourcelist" 109 | newcatalogfile = os.path.join(catalogdir, "newcatalog") 110 | cataloglist = [ 111 | os.path.join(catalogdir, "trumpcatalog"), 112 | os.path.join(catalogdir, "grbcatalog"), 113 | os.path.join(catalogdir, "catalog"), 114 | newcatalogfile, 115 | ] 116 | 117 | 118 | # define machineReadable=False 119 | 120 | ydhms = "%Y-%j-%H:%M:%S" 121 | 122 | # TLEpattern = ["ftp://heasarc.gsfc.nasa.gov/swift/data/obs/%Y_%m/",".*","auxil","SWIFT_TLE_ARCHIVE.*.gz"] 123 | TLEpattern = [ 124 | "http://heasarc.gsfc.nasa.gov/FTP/swift/data/obs/%Y_%m/", 125 | ".*", 126 | "auxil", 127 | "SWIFT_TLE_ARCHIVE.*.gz", 128 | ] 129 | 130 | tlefile = "/tmp/latest_swift_tle.gz" 131 | tlebackup = os.path.expanduser("~/.swift/recent_swift_tle.gz") 132 | 133 | radecnameRE = re.compile(r"""^(?P[0-9.]+)_+(?P([-+]*[0-9.]+))$""") 134 | 135 | ipntimeRE = re.compile( 136 | r"""'(?P[^']+)'\s*'(?P[ 0-9]+)/(?P[ 0-9]+)/(?P[ 0-9]+)'\s+(?P[0-9.]+)""" 137 | ) 138 | 139 | 140 | verbose = False # Verbose controls diagnositc output 141 | terse = False # Terse controls the format of ordinary output 142 | 143 | 144 | # Old fashioned 145 | def simbadnames(query): 146 | """Given a source name, or other SIMBAD query, generates a list of identifier matches 147 | See http://simbad.u-strasbg.fr/simbad/sim-fscript 148 | """ 149 | url_ = urlopen( 150 | """http://simbad.u-strasbg.fr/simbad/sim-script?submit=submit+script&script=format+object+%%22+%%25IDLIST%%22%%0D%%0Aquery++%s""" 151 | % (quote(query),), 152 | None, 153 | 60, 154 | ) 155 | names = [] 156 | while url_: 157 | l = url_.readline().decode() 158 | if re.match("""^::error::*""", l): 159 | raise ValueError(query) 160 | if re.match("""^::data::*""", l): 161 | break 162 | for l in url_.readlines(): 163 | s = l.decode().strip().split() 164 | if len(s) > 0: 165 | if s[0] == "NAME": 166 | s = s[1:] 167 | names.append(" ".join(s)) 168 | return names 169 | 170 | 171 | # Astroquery 172 | def simbadlocation(objectname): 173 | """ 174 | Location according to 175 | :param objectname: 176 | :return: (ra_deg, dec_deg) 177 | """ 178 | from astroquery.simbad import Simbad 179 | 180 | try: 181 | table = Simbad.query_object(objectname) 182 | if len(table) != 1: 183 | raise RuntimeError(f"No unique match for {objectname}") 184 | co = coordinates.SkyCoord( 185 | table["RA"][0], table["DEC"][0], unit=(u.hour, u.deg), frame="fk5" 186 | ) 187 | return (co.ra.degree, co.dec.degree) 188 | except Exception as e: 189 | raise RuntimeError(f"{e}") 190 | 191 | 192 | class orbit: 193 | def __init__(self): 194 | self.getTLE() 195 | 196 | def getTLE(self): 197 | # TLE in this case is Two Line Element, not 3 198 | global verbose 199 | if verbose: 200 | print("Updating TLE") 201 | self.updateTLE() 202 | if verbose: 203 | print("Reading TLE from %s" % (tlefile)) 204 | allTLE = gzip.open( 205 | tlefile, "rt" if sys.version_info.major == 3 else "r" 206 | ).readlines() 207 | # Some TLE files seem to have additional blank lines 208 | # Actually, what they have is \r\n in some cases and \n in others 209 | # Nevertheless, put this in in case they change the format 210 | allTLE = [l.strip() for l in allTLE if len(l.strip()) == 69] 211 | nTLE = len(allTLE) // 2 212 | self._tleByTime = [ 213 | ( 214 | datetime.datetime(2000 + int(allTLE[2 * i][18:20]), 1, 1, 0, 0, 0) 215 | + datetime.timedelta(float(allTLE[2 * i][20:32]) - 1), 216 | ("Swift", allTLE[2 * i], allTLE[2 * i + 1]), 217 | ) 218 | for i in range(nTLE) 219 | ] 220 | self.pickTLE(datetime.datetime.utcnow()) 221 | 222 | def pickTLE(self, t): 223 | if self._tleByTime[-1][0] < t: 224 | self._tle = self._tleByTime[-1][1] 225 | self._tletimes = [ 226 | self._tleByTime[-1][0], 227 | datetime.datetime(datetime.MAXYEAR, 1, 1), 228 | ] 229 | else: 230 | for w in range(len(self._tleByTime) - 1, -1, -1): 231 | if self._tleByTime[w][0] < t or w == 0: 232 | self._tle = self._tleByTime[w][1] 233 | self._tletimes = [self._tleByTime[w][0], self._tleByTime[w + 1][0]] 234 | break 235 | 236 | def getSatellite(self, t) -> Tuple[sf_api.EarthSatellite, sf_api.Time]: 237 | """Produces skyfield.EarthSatellite and its location for Swift at the given time 238 | 239 | Args: 240 | t (_type_): _description_ 241 | 242 | Returns: 243 | _type_: _description_ 244 | """ 245 | global verbose 246 | # print(t, self._tletimes) 247 | if t < self._tletimes[0] or self._tletimes[1] < t: 248 | if verbose: 249 | print("Picking TLE for time %s" % (t,)) 250 | self.pickTLE(t) 251 | if verbose: 252 | print( 253 | "Picked TLE for %s < %s < %s" 254 | % (self._tletimes[0], t, self._tletimes[1]) 255 | ) 256 | sat = sf_api.EarthSatellite(self._tle[1], self._tle[2], name=self._tle[0]) 257 | sft = sftime(t) 258 | return sat, sat.at(sft) 259 | 260 | def usetledb(self, catnum=28485): 261 | """Use spacetrack tles from database (not publicly available)""" 262 | import tledb # pyright: ignore[reportMissingImports] 263 | 264 | tles = tledb.getTLEs(catnums=[catnum], all_in_time=True) 265 | self._tleByTime = [(t.epoch, t.threelines(split=True)) for t in tles] 266 | return self.getSatellite(self._earthcenter.date.datetime()) 267 | 268 | def satname(self): 269 | return self._tleByTime[0][1][0] 270 | 271 | def updateTLE(self): 272 | global verbose 273 | try: 274 | # time.clock() is not what was wanted, since that doesn't give you the clock time 275 | checksecs = time.mktime( 276 | (datetime.datetime.now() + datetime.timedelta(days=-1)).timetuple() 277 | ) 278 | if os.stat(tlefile).st_mtime > checksecs: 279 | return # The TLE file exists and is less than a day old 280 | except: 281 | pass 282 | tlematch = re.compile(TLEpattern[-1]) 283 | for url_month in ( 284 | datetime.datetime.utcnow().strftime(TLEpattern[0]), 285 | (datetime.datetime.utcnow() - datetime.timedelta(30)).strftime( 286 | TLEpattern[0] 287 | ), 288 | ): 289 | try: 290 | if verbose: 291 | print("Trying to get TLE from %s" % (url_month,)) 292 | httpdir = generaldir.httpDir(url_month) 293 | obsmonth = httpdir.dirs() 294 | obsmonth.reverse() 295 | for obs in obsmonth: 296 | auxfiles = httpdir.files(obs + "/auxil") 297 | for f in auxfiles: 298 | if tlematch.match(f): 299 | if verbose: 300 | print("Copying TLEs from " + obs + "/auxil/" + f) 301 | httpdir.copyToFile(obs + "/auxil/" + f, tlefile) 302 | try: 303 | shutil.copyfile(tlefile, tlebackup) 304 | except: 305 | pass 306 | return 307 | except Exception as e: 308 | pass 309 | try: 310 | if verbose: 311 | print("Using old verion of Swift TLE from " + tlebackup) 312 | shutil.copyfile(tlebackup, tlefile) 313 | shutil.copystat(tlebackup, tlefile) 314 | return 315 | except: 316 | raise LookupError("Could not find a recent TLE file") 317 | 318 | 319 | # Source, initialized from a data string from the catalog files 320 | 321 | 322 | def batExposure(theta, phi): 323 | """ 324 | Given theta,phi in radians, returns (open_coded_area_in_cm^2, cosfactor) 325 | 326 | theta = distance from center of FOV (boresight) in radians 327 | phi = angle around boresight in radians. 328 | This is moderate accuracy, but does not take into account, e.g. dead detectors. 329 | """ 330 | theta = apAngle(theta, u.rad) 331 | phi = apAngle(phi, u.rad) 332 | 333 | if np.cos(theta) < 0: 334 | return (0.0, np.cos(theta)) 335 | # BAT dimensions 336 | detL = 286 * 4.2e-3 # Amynote: each det element is has length 4.2e-3 m 337 | detW = 173 * 4.2e-3 # Amynote: each det element is has length 4.2e-3 m 338 | maskL = 487 * 5.0e-3 # Using the 5 mm mask cells, true size is 95.9" x 47.8" 339 | maskW = 243 * 5.0e-3 340 | efl = 1.00 341 | # Calculate dx, but reverse it if necessary to put projected detector to left of mask center 342 | dx = (maskL - detL) / 2 - efl * np.tan(theta) * abs(np.cos(phi)) 343 | dy = (maskW - detW) / 2 + efl * np.tan(theta) * np.sin(phi) 344 | # Boundaries of the detector as clipped to rectangle of mask 345 | x1 = max(0, -dx) 346 | x2 = min(detL, maskL - dx) 347 | deltaX = x2 - x1 348 | y1 = max(0, -dy) 349 | y2 = min(detW, maskW - dy) 350 | deltaY = y2 - y1 351 | # Now adjust for the cut corners, which first attacks the upper left detector corner 352 | # as described by the (if-necessary-reversed) coord. system 353 | xint = ( 354 | (y1 + dy) - maskW / 2 - (dx + x1) 355 | ) # line of corner clip extends through (x1 + xint, y1) 356 | if deltaX < 0 or deltaY < 0: 357 | area = 0 358 | elif xint <= -deltaY: # no clipping 359 | area = deltaX * deltaY 360 | elif xint <= 0: # upper left corner clipped along left edge 361 | if deltaY <= deltaX - xint: # clip from left edge, to top edge 362 | area = deltaX * deltaY - ((deltaY + xint) ** 2) / 2 363 | else: # clip from left edge to right edge 364 | area = deltaX * -xint + (deltaX**2) / 2 365 | elif xint <= deltaX: # clipped at bottom edge 366 | if xint <= deltaX - deltaY: # clipped bottom and top 367 | area = (deltaX - xint) * deltaY - (deltaY**2) / 2 368 | else: # clipped bottom and right 369 | area = ((deltaX - xint) ** 2) / 2 370 | else: 371 | area = 0 372 | # if you want to see what the corners do: area = max(0,deltaX * deltaY) - area 373 | # multiply by 1e4 for cm^2, 1/2 for open area 374 | return (area * 1e4 / 2, np.cos(theta)) 375 | 376 | 377 | def detid2xy(detids): 378 | """ 379 | Convert detector ids to x,y positions 380 | 381 | This is tricky. You can understand it, but it isn't worth it. 382 | 383 | :param detids: detector ids (ints 0...32767) 384 | :type detids: scalar int or convertible to a numpy array 385 | :return: x,y 386 | :rtype: x and y are each numpy arrays or scalars the same size as the original detids 387 | """ 388 | scalar = np.isscalar(detids) 389 | detids = np.asarray(detids, dtype=np.int16) 390 | 391 | # 'mod' is a 128-detector half-DM 392 | block_and_mod, det_in_mod = np.divmod(detids, np.int16(128)) 393 | block, mod_in_block = np.divmod(block_and_mod, np.int16(16)) 394 | 395 | # x in {0..285}, y in {0..172} 396 | # origin lower left is block 8, hdm 9, det 0 397 | # 16 modules across (each 16 detectors wide) with 15 gaps of 2 -> 286 x values 398 | # 16 modules high (each 8 detectors high) with gaps of 3 -> 173 y values 399 | 400 | # Module frame imod, jmod 401 | # 127 119 111 103 95 87 79 71 56 48 40 32 24 16 8 0 402 | # 126 70 57 1 403 | # 125 69 58 2 ^ 404 | # 124 68 59 3 | 405 | # 123 67 60 4 jmod 406 | # 122 66 61 5 407 | # 121 65 62 6 imod -> 408 | # 120 112 104 96 88 80 72 64 63 55 47 39 31 23 15 7 409 | # bit7 is left or right half 410 | bit6, bit5_0 = np.divmod(det_in_mod, np.int16(64)) 411 | bit6_3, bit2_0 = np.divmod( 412 | det_in_mod, np.int16(8) 413 | ) # bits 6-3 determine the imod, bits 2-0 jmod 414 | imod = np.int16(15) - bit6_3 415 | jmod = np.where(bit6, bit2_0, np.int16(7) - bit2_0) 416 | 417 | # for block 0-7, half-DMs are arranged 418 | # 1 9 419 | # 0 8 420 | # 3 11 ^ 421 | # 2 10 | 422 | # 5 13 | 423 | # 4 12 jblock 424 | # 7 15 425 | # 6 14 iblock ----> 426 | 427 | bit10, bit9_7 = np.divmod(mod_in_block, np.int16(8)) 428 | # 16 detectors + 2-space gap for second column 429 | iblock = imod + np.int16(18) * bit10 430 | jmodrow = (np.array([6, 7, 4, 5, 2, 3, 0, 1]) * 11).astype( 431 | np.int16 432 | ) # 8 detectors and 3-space gap for each row of modules 433 | jblock = jmod + jmodrow[bit9_7] 434 | 435 | # blocks are arranged: 436 | # 0 1 2 3 4 5 6 7 437 | # 8 9 10 11 12 13 14 15 438 | # and blocks 8-15 are rotated 180 degrees 439 | bit15, bit14_11 = np.divmod(block, np.int16(8)) 440 | # 8-15 0-7 441 | y = np.where(bit15, np.int16(85) - jblock - 1, np.int16(88) + jblock) 442 | x = np.where(bit15, np.int16(34) - iblock - 1, iblock) + np.int16(36) * bit14_11 443 | if scalar: 444 | x = np.int16(x) 445 | y = np.int16(y) 446 | return x, y 447 | 448 | 449 | @functools.lru_cache(maxsize=0) 450 | def xy2detidmap(): 451 | """ 452 | Produce a detector map filled with detector IDs 453 | 454 | -1 for unpopulated detector locations 455 | 456 | :return: detids[y, x] 457 | :rtype: uint16, shape = (173, 286) 458 | """ 459 | detids = np.arange(0, 2**15) 460 | x, y = detid2xy(detids) 461 | result = np.full((y.max() + 1, x.max() + 1), np.int16(-1)) 462 | result[y, x] = detids 463 | return result 464 | 465 | 466 | def xy2detid(x, y): 467 | dmap = xy2detidmap() 468 | return dmap[y, x] 469 | 470 | 471 | def loadsourcecat(): 472 | """Read in source catalog 473 | 474 | FIXME, should be more automatic 475 | """ 476 | global sourcecat 477 | global verbose 478 | try: 479 | id(sourcecat) 480 | except NameError: 481 | if os.path.exists(cataloglist[3]): 482 | if verbose: 483 | print("Loading catalogs:\n %s" % "\n ".join(cataloglist[0:3])) 484 | sourcecat = sourcelist( 485 | cataloglist[0:3] + [fitscatalog], verbose=verbose 486 | ) # Exclude newcatalog 487 | else: 488 | if verbose: 489 | print("Using old catalog %s" % (basecatalog,)) 490 | sourcecat = sourcelist((basecatalog, fitscatalog), verbose=verbose) 491 | if verbose: 492 | print("Loaded") 493 | # print(" ".join(sourcecat.allsources.keys())) 494 | 495 | 496 | class source: 497 | def __init__(self, initstring=None, **kwargs): 498 | """ 499 | A source location with the ability to calculate BAT-relative angles and exposure 500 | 501 | :param initstring: String from source table ('|' delimited), or object recognized by Simbad 502 | :param kwargs: {ra:ra_deg, dec:dec_deg, , } 503 | """ 504 | # This is where the Sun was implemented 505 | # if initstring in ephem.__dict__: 506 | # self.needs_computing = True 507 | # self.computable = ephem.__dict__[initstring]() 508 | # elif... 509 | if "ra" in kwargs and "dec" in kwargs: 510 | if initstring: 511 | raise RuntimeError("Give ra=,dec= or an initstring but not both") 512 | self.set_radec(kwargs["ra"], kwargs["dec"]) 513 | self.name = kwargs.get("name", "unnamed") 514 | self.catnum = kwargs.get("catnum", 0) 515 | else: 516 | self.needs_computing = False 517 | if "|" in initstring: 518 | s = initstring.split("|") 519 | self.name = s[3].strip().replace(" ", "_") 520 | self.catnum = int(s[5]) 521 | self.set_radec(float(s[9]), float(s[10])) 522 | else: 523 | try: 524 | ra, dec = simbadlocation(initstring) 525 | self.set_radec(ra, dec) 526 | self.name = initstring 527 | self.catnum = -1 528 | except: 529 | raise NameError( 530 | f"Source name {initstring} not recognized by Simbad" 531 | ) 532 | 533 | def set_radec(self, ra_deg, dec_deg): 534 | self.skyloc = SkyCoord( 535 | ICRS(ra=apAngle(ra_deg, u.deg), dec=apAngle(dec_deg, u.deg)) 536 | ) 537 | 538 | @property 539 | def ra_deg(self): 540 | return self.skyloc.ra.degree 541 | 542 | @property 543 | def dec_deg(self): 544 | return self.skyloc.dec.degree 545 | 546 | @classmethod 547 | def source(cls, ra, dec, name="anonymous", catnum=-1) -> str: 548 | """The source as a '|'-delimted string as used in ASCII catalog tables 549 | 550 | Args: 551 | ra (float): degrees 552 | dec (float): degrees 553 | name (str, optional): _description_. Defaults to "anonymous". 554 | catnum (int, optional): _description_. Defaults to -1. 555 | 556 | Returns: 557 | str: _description_ 558 | """ 559 | return cls( 560 | f"|||{name}||{catnum}||||{apAngle(ra, u.deg).degree}|{apAngle(dec, u.deg).degree}" 561 | ) 562 | 563 | def sf_position(self) -> sf_ICRF: 564 | """Position of source as a skyfield.Position""" 565 | return sf_api.position_of_radec( 566 | ra_hours=self.ra_deg / 15, dec_degrees=self.dec_deg 567 | ) 568 | 569 | def exposure(self, ra, dec, roll): 570 | # returns (projected_area, cos(theta)) 571 | (thetangle, phi) = self.thetangle_phi(ra, dec, roll) 572 | if thetangle > apAngle(90, u.deg): 573 | return 0.0, 0.0 574 | return batExposure(thetangle, phi) 575 | 576 | def thetangle_phi( 577 | self, ra: float, dec: float, roll: float 578 | ) -> Tuple[(apAngle, apAngle)]: 579 | """_Source position in instrument FOV given instrument pointing direction 580 | returns (thetangle,phi) where thetangle is the angular distance from 581 | the boresight and phi is the angle from the phi=0 axis of BAT. 582 | 583 | theta = tan(thetangle) gives the theta we use, which is the projected distance to a flat plane, 584 | but it is not useful for thetangle > 90 degrees 585 | 586 | Args: 587 | ra (float): ra of spacecraft boresight in degrees 588 | dec (float): dec of spacecraft boresight in degrees 589 | roll (float): roll is 'roll left' (CCW as seen from behind looking out along boresight) in degrees 590 | Returns: 591 | (theta:degree, phi:apAngle): location of source in spacecraft spherical coordinates 592 | """ 593 | # Use astropy coordinates 594 | boreloc = SkyCoord(ICRS(ra=apAngle(ra, u.deg), dec=apAngle(dec, u.deg))) 595 | thetangle = boreloc.separation(self.skyloc).to(u.deg) 596 | # posang is CCW, phi is CCW from +Y so (posang - roll - 90deg) gives phi 597 | phi = ( 598 | ( 599 | coordinates.position_angle( 600 | boreloc.ra, boreloc.dec, self.skyloc.ra, self.skyloc.dec 601 | ) 602 | - apAngle(roll, u.deg) 603 | - apAngle(90, u.deg) 604 | ) 605 | .wrap_at(180 * u.deg) 606 | .to(u.deg) 607 | ) 608 | return (thetangle, phi) 609 | 610 | def compute(self, t): 611 | """Default implementation does nothing 612 | 613 | Args: 614 | t (time): _description_ 615 | """ 616 | pass 617 | 618 | def __str__(self): 619 | return "|||%s||%d||||%f|%f|" % ( 620 | self.name, 621 | self.catnum, 622 | self.skyloc.ra.degree, 623 | self.skyloc.dec.degree, 624 | ) 625 | 626 | 627 | class sunsource(source): 628 | def __init__(self, t=None, bodyname="Sun"): 629 | self.body = loadsfephem()[bodyname] 630 | self.earth = loadsfephem()["Earth"] 631 | skyloc = self._skyloc_at(t) 632 | super().__init__(ra=skyloc.ra.degree, dec=skyloc.dec.degree, name=bodyname) 633 | 634 | def _skyloc_at(self, t=None) -> ICRS: 635 | # Cached so it only downloads the DE422 once and only reads it once per run 636 | if t is None: 637 | t = datetime.datetime.now(tz=datetime.timezone.utc) 638 | t = sftime(t) 639 | app = self.earth.at(t).observe(self.body).apparent() 640 | ra, dec, _ = app.radec() 641 | skyloc = SkyCoord( 642 | ICRS(ra=apAngle(ra._degrees, u.deg), dec=apAngle(dec._degrees, u.deg)) 643 | ) 644 | return skyloc 645 | 646 | def compute(self, t): 647 | self.skyloc = self._skyloc_at(t) 648 | 649 | 650 | # This is a truncated version of catalog. It should become the parent of catalog someday (FIXME 2008-09-17) 651 | class sourcelist: 652 | def __init__(self, filelist=None, verbose=False): 653 | if filelist is None: 654 | filelist = [ 655 | os.path.join(catalogdir, "catalog"), 656 | os.path.join(catalogdir, "grbcatalog"), 657 | ] 658 | self.allsources = {} 659 | for file in filelist: 660 | # print(file) 661 | try: 662 | self.addFromFile(file, verbose=verbose) 663 | except: 664 | traceback.print_exc() 665 | pass 666 | 667 | def addFromFile(self, file, verbose=False): 668 | if verbose: 669 | print(file) 670 | if ".fits" in file: 671 | for row in fits.getdata(file): 672 | aSource = source.source( 673 | ra=row.field("RA_OBJ"), 674 | dec=row.field("DEC_OBJ"), 675 | name=row.field("NAME"), 676 | ) 677 | if verbose: 678 | print(aSource) 679 | # print(line,aSource.name) 680 | self.allsources[self.canonName(aSource.name)] = aSource 681 | else: 682 | for line in open(file).readlines(): 683 | line = line.strip() 684 | if ( 685 | "+------+------------+" in line 686 | or "| ROW| TIME| NAME|" in line 687 | or line.startswith("#") 688 | or line == "" 689 | ): 690 | continue 691 | try: 692 | aSource = source(line) 693 | if verbose: 694 | print(aSource) 695 | # print(line,aSource.name) 696 | self.allsources[self.canonName(aSource.name)] = aSource 697 | except: 698 | pass 699 | 700 | def byName(self, name): 701 | try: 702 | return self.allsources[self.canonName(name)] 703 | except: 704 | raise NameError 705 | 706 | def byCatnum(self, c): 707 | for s in self.allsources: 708 | if s.catnum == c: 709 | return s 710 | return None 711 | 712 | def canonName(self, name): 713 | return name.lower().replace(" ", "_")[0:15].replace("_", "") 714 | 715 | 716 | def hasLength(x): 717 | try: 718 | len(x) 719 | return True 720 | except: 721 | return False 722 | 723 | 724 | def parseNotice(lines): 725 | """ 726 | Given a source of lines, read the lines as a GRB notice and return (ra_deg,dec_deg,met_swift) 727 | Where met_swift is adjusted by the Swift UTCF even if the instrument is not Swift. 728 | If ra and dec are not included in the notice, they are set to None 729 | """ 730 | ra_deg = None 731 | dec_deg = None 732 | strippers = "\xc2\xa0 \t\n" # some strange characters when Mail.app saves mail 733 | for l in lines: 734 | l = swutil.removeNonAscii(l) 735 | ipntime = ipntimeRE.search(l) 736 | if ipntime: 737 | g = ipntime.groupdict() 738 | ss = float(g["s"]) 739 | h = int(ss / 3600) 740 | ss -= h * 3600 741 | m = int(ss / 60) 742 | ss -= m * 60 743 | ymdT = "20%s-%s-%sT" % (g["y"], g["m"], g["d"]) 744 | hms = "%02d:%02d:%05.3f" % (h, m, ss) 745 | else: 746 | ls = l.split() 747 | if len(ls) > 1: 748 | if ls[0] in ["GRB_RA:", "SRC_RA:"]: 749 | ra_deg = float(ls[1].split("d")[0].strip(strippers)) 750 | elif ls[0] in ["GRB_DEC:", "SRC_DEC:"]: 751 | dec_deg = float(ls[1].split("d")[0].strip(strippers)) 752 | elif ls[0] in ["GRB_DATE:", "DISCOVERY_DATE:"]: 753 | ymdT = "20%s-%s-%sT" % tuple(ls[5].strip(strippers).split("/")) 754 | elif ls[0] in ["GRB_TIME:", "DISCOVERY_TIME:"]: 755 | hms = ls[3].strip(strippers + "{}") 756 | 757 | if ls[0] in ["GRB_RA:", "SRC_RA:", "EVENT_RA:"]: 758 | ra_deg = float(ls[1].split("d")[0].strip(strippers)) 759 | elif ls[0] in ["GRB_DEC:", "SRC_DEC:", "EVENT_DEC:"]: 760 | dec_deg = float(ls[1].split("d")[0].strip(strippers)) 761 | elif ls[0] in [ 762 | "GRB_DATE:", 763 | "EVENT_DATE:", 764 | "DISCOVERY_DATE:", 765 | "TRIGGER_DATE:", 766 | ]: 767 | ymdT = "20%s-%s-%sT" % tuple(ls[5].strip(strippers).split("/")) 768 | elif ls[0] in [ 769 | "GRB_TIME:", 770 | "DISCOVERY_TIME:", 771 | "EVENT_TIME:", 772 | "TRIGGER_TIME:", 773 | ]: 774 | hms = ls[3].strip(strippers + "{}") 775 | met = swutil.string2met(ymdT + hms, nocomplaint=True, correct=True) 776 | return (ra_deg, dec_deg, met) 777 | 778 | 779 | def lineofsight(satpos, source): 780 | """Calculate the 781 | (elevation_above_Earth_limb_degrees, losheight_m, description) 782 | of the line of sight from the satellite position to the source. 783 | minimum_elevation_above_sea_level is 0 if LOS intersects Earth, sat.elevation if LOS is upwards 784 | """ 785 | atm_thickness_m = 100e3 # How deep in the atmosphere you should report attenuation 786 | # Using Skyfield positions for separation, thereafter raw radians 787 | zenangle_rad = satpos.separation_from(source.sf_position()).radians 788 | _, _, satradius = satpos.radec() 789 | satradius_m = satradius.m 790 | earthrad_m = u.earthRad.to(u.m) 791 | # Hard earth horizon (equatorial radius) 792 | zenhoriz_rad = np.pi / 2 + np.arccos(earthrad_m / satradius_m) 793 | if zenangle_rad < np.pi / 2: 794 | losheight_m = satradius_m - earthrad_m 795 | description = "up" 796 | elif zenangle_rad > zenhoriz_rad: 797 | losheight_m = 0.0 798 | description = "down" 799 | else: 800 | losheight_m = (satradius_m * np.cos(zenangle_rad - np.pi / 2)) - earthrad_m 801 | if losheight_m > atm_thickness_m: 802 | description = "depressed but unattenuated" 803 | else: 804 | description = "attenuated by atmosphere at %.0f km" % ( 805 | losheight_m / 1000.0, 806 | ) 807 | return (np.rad2deg(zenhoriz_rad - zenangle_rad), losheight_m, description) 808 | 809 | 810 | class PointingEntry: 811 | def __init__( 812 | self, tstart, tslewend, tend, ra, dec, roll, obs, segment, sourcename, planned 813 | ): 814 | self._tstart = tstart 815 | self._tslewend = tslewend 816 | if tend == None: # How to handle the stubs 817 | self._tend = tstart + datetime.timedelta(seconds=90 * 60) 818 | else: 819 | self._tend = tend 820 | self._ra = ra 821 | self._dec = dec 822 | self._roll = roll 823 | self._obs = obs 824 | self._segment = segment 825 | self._sourcename = sourcename 826 | self._planned = planned 827 | 828 | @classmethod 829 | def listfromquery(cls, query: swto.ObsQuery) -> List["PointingEntry"]: 830 | result = [] 831 | for entry in query: 832 | # query.to_utctime() 833 | pointent = cls( 834 | entry.begin, 835 | entry.begin + entry.slewtime, 836 | entry.end, 837 | entry.ra, 838 | entry.dec, 839 | entry.roll, 840 | int(entry.obsid[:-3]), 841 | entry.seg, 842 | entry.targname, 843 | False, 844 | ) 845 | result.append(pointent) 846 | return result 847 | 848 | @classmethod 849 | def listfortimes(cls, times: list[datetime.datetime], allinrange=False): 850 | if allinrange: 851 | begin = min(times) 852 | end = max(times) 853 | queries = [swto.ObsQuery(begin=begin, end=end)] 854 | else: 855 | queries = [swto.ObsQuery(t) for t in times] 856 | 857 | result = [] 858 | for query in queries: 859 | if query.submit(): 860 | result.extend(cls.listfromquery(query)) 861 | return result 862 | 863 | def __str__(self): 864 | if self._planned: 865 | if self._planned == True: 866 | plannedstring = "(Preplanned)" 867 | else: 868 | plannedstring = "(planned %s)" % self._planned 869 | else: 870 | plannedstring = "" 871 | return "%08d%03d %s - %s - %s [%7.3f, %7.3f, %7.3f] %s %s" % ( 872 | self._obs, 873 | self._segment, 874 | self._tstart.strftime("%Y-%j-%H:%M:%S"), 875 | self._tslewend.strftime("%H:%M:%S"), 876 | self._tend.strftime("%H:%M:%S"), 877 | self._ra, 878 | self._dec, 879 | self._roll, 880 | self._sourcename, 881 | plannedstring, 882 | ) 883 | 884 | 885 | def usage(progname): 886 | print("Usage: %s MET [MET....]" % progname) 887 | print(" -v --verbose diagnostic information") 888 | print(" --terse produce terse output") 889 | print(" -o --orbit satellite position") 890 | print( 891 | " -s --source sourcename source visibility. Sourcename can be '123.4_-56.7' for RA_dec" 892 | ) 893 | print(" -S --sun --Sun Sun visibility") 894 | print(" --visible Only when the source is in the FOV") 895 | print( 896 | " -x --excelvis source visibility list in CSV Excelable format (grep vis)" 897 | ) 898 | # print(" -m --machine machine-convenient format with printouts on single lines") 899 | print(' -p --position "ra_deg dec_deg" position visibility') 900 | print( 901 | " -t --timerange use all pointings in the range of times" 902 | ) 903 | print( 904 | " --steptime seconds cover the range fo times with this interval" 905 | ) 906 | print(" -c --clipboard use times in the current clipboard") 907 | print( 908 | " -f --format '%Y-%m-%dT%H:%M:%S' use given time format. (Example is default)" 909 | ) 910 | print( 911 | " -P --ppst ppstfile use local PPST file instead of getting from web" 912 | ) 913 | print( 914 | ' -a --attitude "ra dec roll" manually include an attitude rather than PPST' 915 | ) 916 | print( 917 | " --notice process GCN notice piped to stdin to extract time and position" 918 | ) 919 | # print(" --METonly print out nothing but the MET") 920 | # print(" --UTConly print out nothing but the UTC") 921 | print(" -h --help this printout") 922 | 923 | 924 | # @swutil.timeout(60) 925 | def swinfo_main(argv=None, debug=None): 926 | global verbose 927 | global terse 928 | global sourcecat 929 | if argv is None: 930 | argv = sys.argv 931 | sources = [] 932 | orbits = False 933 | ppstfile = [] 934 | timerange = False 935 | visible_only = False 936 | timestep = 0 937 | manualAttitudes = [] 938 | excelvis = False 939 | # debug=open("/tmp/swinfo.debug","a") 940 | # debug = None 941 | 942 | # if debug : 943 | # sys.stdout = swutil.TeeFile(debug, sys.stdout) 944 | 945 | if debug: 946 | debug.write( 947 | "%s (%d): %s\n" % (datetime.datetime.now(), os.getpid(), " ".join(argv)) 948 | ) 949 | vebose = True 950 | debug.flush() 951 | 952 | format = swutil.fitstimeformat 953 | try: 954 | opt, timeargs = getopt.gnu_getopt( 955 | argv[1:], 956 | "vos:Sxp:tcf:P:a:h", 957 | [ 958 | "verbose", 959 | "terse", 960 | "orbit", 961 | "source=", 962 | "sun", 963 | "Sun", 964 | "visible", 965 | "excelvis", 966 | "position=", 967 | "timerange", 968 | "steptime=", 969 | "clipboard", 970 | "format=", 971 | "ppst=", 972 | "attitude=", 973 | "notice", 974 | "help", 975 | ], 976 | ) 977 | # print(opt) 978 | # print(timeargs) 979 | metvalues = [swutil.string2met(a, correct=True) for a in timeargs] 980 | for o, v in opt: # option value 981 | if o in ("-v", "--verbose"): 982 | verbose = True 983 | print(" ".join(argv)) 984 | elif o in ("--terse"): 985 | terse = True 986 | elif o in ("-o", "--orbit"): 987 | orbits = orbit() 988 | # print("Orbit read") 989 | elif o in ("-s", "--source"): 990 | loadsourcecat() 991 | try: 992 | # print(v) 993 | s = sourcecat.byName(v) 994 | # print(s) 995 | sources.append(s) 996 | except: 997 | m = radecnameRE.match(v) 998 | if m: 999 | s = ( 1000 | "|||" 1001 | + v 1002 | + ("||0||||%(rastring)s|%(decstring)s|" % m.groupdict()) 1003 | ) 1004 | sources.append(source(s)) 1005 | else: 1006 | print("Source %s not found in catalog" % v) 1007 | return 1008 | elif o in ("-S", "--sun", "--Sun"): 1009 | s = sunsource() 1010 | sources.append(s) 1011 | elif "--visible".startswith(o): 1012 | visible_only = True 1013 | elif o in ("-t", "--timerange"): 1014 | timerange = True 1015 | elif o in ("--steptime",): 1016 | timestep = datetime.timedelta(seconds=float(v)) 1017 | elif o in ("-c", "--clipboard"): 1018 | for ttry in os.popen("/usr/bin/pbpaste | tr '\r' '\n'"): 1019 | try: 1020 | t = swutil.string2met(ttry, nocomplaint=True, correct=True) 1021 | if t: 1022 | metvalues.append(t) 1023 | except: 1024 | pass 1025 | elif o in ("-f", "--format"): 1026 | format = v 1027 | elif o in ("-p", "--position"): 1028 | try: 1029 | ra, dec = [float(s) for s in v.translate(split_translate).split()] 1030 | string = "|||%s||%d||||%f|%f|" % (v, len(sources), ra, dec) 1031 | newsource = source(string) 1032 | sources.append(newsource) 1033 | except: 1034 | print("Could not add source at position " + v) 1035 | return 1036 | elif o in ("-P", "--ppst"): 1037 | ppstfile.append(v) 1038 | elif o in ("-a", "-attitude"): 1039 | vsplit = v.translate(split_translate).split() 1040 | try: 1041 | ra = float(vsplit[0]) 1042 | dec = float(vsplit[1]) 1043 | roll = float(vsplit[2]) 1044 | except: 1045 | print("Could not get ra,dec,roll out of argument '%s'" % (v,)) 1046 | usage(argv[0]) 1047 | return 1048 | ztime = datetime.datetime(2000, 1, 1, 0, 0, 0) 1049 | manualAttitudes.append( 1050 | PointingEntry( 1051 | ztime, 1052 | ztime, 1053 | ztime, 1054 | ra, 1055 | dec, 1056 | roll, 1057 | 0, 1058 | 0, 1059 | "(from command line)", 1060 | False, 1061 | ) 1062 | ) 1063 | elif o in ("--notice",): 1064 | (ra, dec, met) = parseNotice(sys.stdin) 1065 | metvalues.append(met) 1066 | if ra != None and dec != None: 1067 | string = "|||%.2f_%.2f||%d||||%f|%f|" % ( 1068 | ra, 1069 | dec, 1070 | len(sources), 1071 | ra, 1072 | dec, 1073 | ) 1074 | newsource = source(string) 1075 | sources.append(newsource) 1076 | elif o in ("-x", "--excelvis"): 1077 | excelvis = True 1078 | elif o in ("-h", "--help"): 1079 | usage(argv[0]) 1080 | return 1081 | 1082 | if debug: 1083 | debug.write("Arguments processed\n") 1084 | 1085 | # thePointingTable = pointingTable(ppstfile) 1086 | 1087 | if debug: 1088 | debug.write("Pointing table received \n") 1089 | 1090 | if timerange: 1091 | pytimes = ( 1092 | swutil.met2datetime(min(metvalues), correct=True), 1093 | swutil.met2datetime(max(metvalues), correct=True), 1094 | ) 1095 | print("Time range = %s - %s " % pytimes) 1096 | # print(thePointingTable.getPointings(pytimes)) 1097 | # Get the METs of the middles of the observations 1098 | pointings = PointingEntry.listfortimes(pytimes, allinrange=True) 1099 | metvalues = [ 1100 | swutil.datetime2met( 1101 | p._tstart 1102 | + datetime.timedelta( 1103 | seconds=(p._tend - p._tstart).total_seconds() / 2.0 1104 | ), 1105 | correct=True, 1106 | ) 1107 | for p in pointings 1108 | ] 1109 | 1110 | if timestep: 1111 | pytimes = ( 1112 | swutil.met2datetime(min(metvalues), correct=True), 1113 | swutil.met2datetime(max(metvalues), correct=True), 1114 | ) 1115 | print( 1116 | "Time range = %s - %s by %f seconds" 1117 | % (pytimes + (timestep.total_seconds(),)) 1118 | ) 1119 | 1120 | metvalues = [ 1121 | swutil.datetime2met(pytimes[0] + i * timestep, correct=True) 1122 | for i in range( 1123 | 1 1124 | + int( 1125 | np.ceil( 1126 | (pytimes[1] - pytimes[0]).total_seconds() 1127 | / timestep.total_seconds() 1128 | ) 1129 | ) 1130 | ) 1131 | ] 1132 | # Get the METs of the middles of the observations 1133 | 1134 | if excelvis: 1135 | print("vis,Source,slewstart,slewend,obsend,exposure,elevstart,elevend") 1136 | # Excel visibility output requires orbit information 1137 | # to calculate elevation at start of observation=end of slew, end of observation 1138 | if not orbits: 1139 | orbits = orbit() 1140 | 1141 | if verbose and sources: 1142 | print(sources) 1143 | 1144 | if debug: 1145 | debug.write("About to loop through metvalues: %s\n" % (metvalues,)) 1146 | debug.flush() 1147 | 1148 | for t in metvalues: 1149 | pointprint = StringIO() 1150 | # even if visible_only, print if no sources 1151 | visible = len(sources) == 0 1152 | utcf_ = utcf(t, verbose) 1153 | udelta = datetime.timedelta(seconds=utcf_) 1154 | if hasLength(utcf_): 1155 | ulist = utcf_ 1156 | tlist = t 1157 | pytime = [swutil.met2datetime(t_, correct=True) for t_ in t] 1158 | pystarttime = pytime[0] 1159 | else: 1160 | ulist = [utcf_] 1161 | tlist = [t] 1162 | pytime = swutil.met2datetime(t, correct=True) 1163 | pystarttime = pytime 1164 | for i in range(len(ulist)): 1165 | if not terse: 1166 | print("Time(MET + UTCF -> UT): ", file=pointprint, end="") 1167 | print( 1168 | "%.3f + %.3f -> %s" 1169 | % ( 1170 | tlist[i] + 5e-4, 1171 | ulist[i], 1172 | swutil.met2fitsString( 1173 | tlist[i], milliseconds=True, correct=True, format=format 1174 | ), 1175 | ), 1176 | file=pointprint, 1177 | ) 1178 | if not terse: 1179 | print( 1180 | swutil.met2fitsString( 1181 | tlist[i], 1182 | milliseconds=True, 1183 | correct=True, 1184 | format="YYMMDD_SOD: %y%m%d_%q DOY=%j", 1185 | ), 1186 | file=pointprint, 1187 | ) 1188 | if manualAttitudes: 1189 | pointings = manualAttitudes 1190 | else: 1191 | pointings = PointingEntry.listfortimes([pytime]) 1192 | if orbits: 1193 | if debug: 1194 | debug.write( 1195 | "About to get the satellite position for %s\n" % (pystarttime,) 1196 | ) 1197 | debug.flush() 1198 | sat, satpos = orbits.getSatellite(pystarttime) 1199 | satra, satdec, satradius = satpos.radec() 1200 | satlat, satlon = sf_api.wgs84.latlon_of(satpos) 1201 | if debug: 1202 | debug.write("Got the satellite position\n") 1203 | debug.flush() 1204 | if terse: 1205 | print( 1206 | "Satellite zenith RA=%.2f, dec=%.2f, lat=%.2f N, lon=%.2f E" 1207 | % ( 1208 | satra._degrees, 1209 | satdec.degrees, 1210 | satlat.degrees, 1211 | satlon.degrees, 1212 | ) 1213 | ) 1214 | else: 1215 | print( 1216 | "Swift Zenith(RA,dec): %.2f, %.2f" 1217 | % (satra._degrees, satdec.degrees) 1218 | ) 1219 | print( 1220 | "Swift Location(lon,lat,alt): %.2f E, %.2f N, %.0f km" 1221 | % ( 1222 | satlon.degrees, 1223 | satlat.degrees, 1224 | (satradius.km - u.earthRad.to("km")), 1225 | ) 1226 | ) 1227 | for p in pointings: 1228 | if not excelvis: 1229 | if terse: 1230 | print(p, file=pointprint) 1231 | else: 1232 | print( 1233 | "Obs Sequence Number: %08d%03d" 1234 | % (p._obs, p._segment), 1235 | file=pointprint, 1236 | ) 1237 | print( 1238 | "Obs Target Name: %s%s" 1239 | % (p._sourcename, (" (planned)" if p._planned else "")), 1240 | file=pointprint, 1241 | ) 1242 | print( 1243 | "Obs Date and Times: %s - %s" 1244 | % ( 1245 | (p._tstart + udelta).strftime("%Y-%m-%d %H:%M:%S"), 1246 | (p._tend + udelta).strftime("%H:%M:%S"), 1247 | ), 1248 | file=pointprint, 1249 | ) 1250 | print( 1251 | "Obs Pointing(ra,dec,roll): %.3f, %.3f, %.2f" 1252 | % (p._ra, p._dec, p._roll), 1253 | file=pointprint, 1254 | ) 1255 | for s in sources: 1256 | try: 1257 | s.compute(pystarttime) 1258 | theta, phi = s.thetangle_phi(p._ra, p._dec, p._roll) 1259 | # print("exp,cosangle = ",s.exposure(p._ra, p._dec, p._roll) ) 1260 | exp, cosangle = s.exposure(p._ra, p._dec, p._roll) 1261 | visible = visible or (exp > 0) 1262 | if orbits: 1263 | (elev, _, description) = lineofsight(satpos, s) 1264 | relhoriz = " (%s)" % (description,) 1265 | else: 1266 | relhoriz = "" 1267 | if terse: 1268 | print( 1269 | "%s: (theta,phi) = (%.4f, %.4f); exposure = %.0f cm^2%s" 1270 | % ( 1271 | s.name, 1272 | theta.deg, 1273 | phi.wrap_at(180 * u.deg).deg, 1274 | exp * cosangle, 1275 | relhoriz, 1276 | ), 1277 | file=pointprint, 1278 | ) 1279 | elif excelvis: 1280 | (elevstart, _, descriptionstart) = lineofsight( 1281 | orbits.getSatellite(p._tslewend)[1], s 1282 | ) 1283 | (elevend, _, descriptionend) = lineofsight( 1284 | orbits.getSatellite(p._tend)[1], s 1285 | ) 1286 | print( 1287 | "vis,%s,%s,%s,%s,%.0f,%.1f,%.1f" 1288 | % ( 1289 | s.name, 1290 | p._tstart.strftime("%Y-%m-%d %H:%M:%S"), 1291 | p._tslewend.strftime("%Y-%m-%d %H:%M:%S"), 1292 | p._tend.strftime("%Y-%m-%d %H:%M:%S"), 1293 | exp * cosangle, 1294 | elevstart, 1295 | elevend, 1296 | ), 1297 | file=pointprint, 1298 | ) 1299 | else: 1300 | print( 1301 | "%s_imageloc (boresight_dist, angle): (%.0f, %.0f)" 1302 | % (s.name, theta.deg, phi.wrap_at(180 * u.deg).deg), 1303 | file=pointprint, 1304 | ) 1305 | print( 1306 | "%s_exposure (cm^2 cos adjusted): %.0f" 1307 | % (s.name, exp * cosangle), 1308 | file=pointprint, 1309 | ) 1310 | if len(relhoriz) > 3: 1311 | print( 1312 | "%s_altitude: %.0f (%s)" 1313 | % (s.name, elev, description), 1314 | file=pointprint, 1315 | ) 1316 | except: 1317 | traceback.print_exc() 1318 | # traceback.print_tb(sys.exc_info()[2]) 1319 | print(str(s.catnum) + "ran into trouble in calculations") 1320 | return 1321 | if visible or not visible_only: 1322 | sys.stdout.write(pointprint.getvalue()) 1323 | except getopt.GetoptError: 1324 | usage(argv[0]) 1325 | except: 1326 | usage(argv[0]) 1327 | traceback.print_exc() 1328 | if debug: 1329 | traceback.print_exc(None, debug) 1330 | # traceback.print_tb(sys.exc_info()[2]) 1331 | if debug: 1332 | debug.write("%s: swinfo main() completed\n" % (datetime.datetime.now(),)) 1333 | debug.flush() 1334 | 1335 | 1336 | def checkParse(): 1337 | """Go through all the parsed mail messages to see which ones weren't parsed properly""" 1338 | for f in glob.glob("/tmp/latesttrigger.*.processed"): 1339 | parsed = parseNotice(open(f).readlines()) 1340 | if len(parsed) != 3 or parsed[2] < 3e8: 1341 | print("%s %s" % (f, parsed)) 1342 | 1343 | 1344 | if __name__ == "__main__": 1345 | # for i in range(len(sys.argv)) : 1346 | # print("%d: %s" % (i, sys.argv[i])) 1347 | # Turn on debugging 1348 | debug = None 1349 | debug = open("/tmp/swinfo.debug", "a") 1350 | # if debug: 1351 | # swutil.dumponsignal(fname="/tmp/swinfo.debug") 1352 | # main = swutil.TeeStdoutDecorator(main, debug) 1353 | if len(sys.argv) > 1: 1354 | swinfo_main(sys.argv, debug=debug) 1355 | else: 1356 | swinfo_main([l for l in os.popen("/usr/bin/pbpaste | tr '\r' '\n'")]) 1357 | -------------------------------------------------------------------------------- /swiftbat/swutil.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | """ 4 | swutil 5 | Utilities for dealing with Swift Data 6 | Python datatime objects are UTC, MET is spacecraft MET which needs application of UTCF to get datetime 7 | David Palmer palmer@lanl.gov 8 | 9 | """ 10 | 11 | """ 12 | Copyright (c) 2018, Triad National Security, LLC. 13 | All rights reserved. 14 | 15 | This program was produced under U.S. Government contract 16 | 89233218CNA000001 for Los Alamos National Laboratory (LANL), 17 | which is operated by Triad National Security, LLC for the U.S. 18 | Department of Energy/National Nuclear Security Administration. 19 | 20 | All rights in the program are reserved by Triad National 21 | Security, LLC, and the U.S. Department of Energy/National 22 | Nuclear Security Administration. The Government is granted for 23 | itself and others acting on its behalf a nonexclusive, paid-up, 24 | irrevocable worldwide license in this material to reproduce, 25 | prepare derivative works, distribute copies to the public, 26 | perform publicly and display publicly, and to permit others to 27 | do so. 28 | 29 | Additionally, redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 30 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 31 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 32 | 3. Neither the name of Triad National Security, LLC, Los Alamos National Laboratory, LANL, the U.S. Government, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 33 | 34 | THIS SOFTWARE IS PROVIDED BY TRIAD NATIONAL SECURITY, LLC AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL TRIAD NATIONAL SECURITY, LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | 36 | This code was developed using funding from the National Aeronautics and Space Administration (NASA). 37 | 38 | """ 39 | 40 | import os 41 | import sys 42 | import re 43 | import datetime 44 | from .clockinfo import utcf 45 | import io 46 | 47 | # 2004-12-05T19:43:27 48 | refitstime = re.compile( 49 | r"(?P[0-9]{4,4})-(?P[0-9]{1,2})-(?P[0-9]{1,2})([T ]+(?P[0-9]{1,2}):(?P[0-9]{2,2})(:(?P[0-9]{2,2})([.](?P[0-9]*))?)?)?", 50 | re.I, 51 | ) 52 | # 2004:329:12:15:07 53 | redoytime = re.compile( 54 | r"(?P[0-9]{4,4}):(?P[0-9]{3,3})(:(?P[0-9]{2,2}):(?P[0-9]{2,2})(:(?P[0-9]{2,2})([.](?P[0-9]*))?)?)?" 55 | ) 56 | # JD2454192.8273 57 | rejdtime = re.compile(r"JD[ ]*24(?P[0-9]+(.[0-9]*)?)", re.I) 58 | # MJD14192.5273 59 | remjdtime = re.compile(r"MJD[ ]*(?P[0-9]+(.[0-9]*)?)", re.I) 60 | # 140308_13764 IPN seconds of day type format 61 | reIPNsod = re.compile( 62 | r"(?P[0-9]{2,4})-?(?P[0-9]{2})-?(?P[0-9]{2})_(T?)(?P[0-9]{5}(\.[0-9]*)?)" 63 | ) 64 | # General time with multiple options for date and time 65 | # Using slashes to separate ymd is disallowed because it could also 66 | # be m/d/y or d/m/y. '-' or nothing may be (consistently) used 67 | # ydoy , y-doy or y:doy allowed with y 2 or 4 dig, doy always 3 dig 68 | reday_s = ( 69 | r"""((?P(\d{2})|(\d{4}))""" # 2 or 4 digit year 70 | r"""(""" # either DOY or Month,day 71 | r"""([:-]?(?P\d{3}))|""" 72 | r"""((?P-?)(?P[0-9]{1,2})(?P=ymdsep)(?P[0-9]{1,2}))))""" 73 | ) 74 | # sod must be 5 integer digits plus optional decimal and fraction 75 | retime_s = ( 76 | r"[ _]*?(([ _ST](?P[0-9]{5}(\.[0-9]*)?))" 77 | r"|([ _T](?P[0-9]{2})" 78 | r"(?P:?)(?P[0-9]{2})" 79 | r"((?P=tsep)(?P[0-9]{2}(\.[0-9]*)?))?))" 80 | ) 81 | reGeneral = re.compile(reday_s + retime_s, re.I) 82 | # 2004:329:12:15:07) 83 | 84 | swiftepoch = datetime.datetime(year=2001, month=1, day=1, hour=0, minute=0, second=0) 85 | swiftlaunchtime = datetime.datetime(year=2004, month=11, day=20, hour=17, minute=16) 86 | mjdepoch = datetime.datetime(year=1858, month=11, day=17, hour=0, minute=0, second=0) 87 | # mytruncjd is jd with the 24 stripped off the head, but without the 0.5 day adjustment 88 | mytruncjdepoch = mjdepoch - datetime.timedelta(days=0.5) 89 | _jd_mjd_diff = 2400000.5 90 | 91 | fitstimeformat = r"%Y-%m-%dT%H:%M:%S" # for strftime 92 | yearDOYsecstimeformat = r"%Y_%j_%q" # %q -> SOD in 00000 (non-standard) 93 | 94 | 95 | def any2datetime(arg, correct=True, mjd=False, jd=False): 96 | """Change the argument into a (naive) datetime 97 | 98 | Understands: 99 | strings as accepted by string2datetime 100 | astropy, skyfield, or pyephem dates 101 | int/floats representing a Swift MET (uses the 'correct' argument for utcf) 102 | unless mjd or jd set (for Mod. Julian Day and Julian Day, respectively) 103 | iterables of any of these, returning a list of datetimes 104 | """ 105 | if isinstance(arg, str): 106 | return string2datetime(arg, correct=correct) 107 | for convname in ("to_datetime", "utc_datetime", "datetime"): 108 | # astropy, skyfield, pyephem 109 | if hasattr(arg, convname): 110 | return getattr(arg, convname)(arg) 111 | if isinstance(arg, (float, int)): 112 | if mjd: 113 | return mjd2datetime(arg) 114 | elif jd: 115 | return mjd2datetime(arg - _jd_mjd_diff) 116 | else: 117 | return met2datetime(arg, correct=correct) 118 | try: 119 | return [any2datetime(arg_, correct=True, mjd=mjd, jd=jd) for arg_ in arg] 120 | except TypeError: # Not iterable (assuming thrown by 'for') 121 | pass 122 | 123 | 124 | def string2datetime(s, nocomplaint=False, correct=False): 125 | """ 126 | Convert a string (which may be a Swift MET) to a datetime 127 | 128 | In the case of an MET, is corrected for UTCF if correct==True 129 | :param s: string with time information 130 | :param nocomplaint: Don't complain if something is wrong 131 | :param correct: Include the UTCF when converting an MET to UTC 132 | :return: UTC 133 | :rtype: datetime.datetime 134 | """ 135 | try: 136 | m = reGeneral.match(s) 137 | if m: 138 | mgd = m.groupdict() 139 | try: 140 | year = int(mgd["year"]) 141 | if year < 100: 142 | # Use 1960-2060 for 2-digit years 143 | year += 2000 if (year < 60) else 1900 144 | if mgd["doy"] is not None: 145 | doy = int(mgd["doy"]) 146 | date = datetime.date(year=year) + datetime.timedelta(days=doy - 1) 147 | else: 148 | month = int(mgd["month"]) 149 | day = int(mgd["day"]) 150 | date = datetime.date(year=year, month=month, day=day) 151 | if mgd["sod"] is not None: 152 | addseconds = float(mgd["sod"]) 153 | tod = datetime.time(0) 154 | else: 155 | hour = int(mgd["hour"]) 156 | minute = int(mgd["minute"]) 157 | try: 158 | addseconds = float(mgd["second"]) 159 | except: 160 | addseconds = 0.0 161 | tod = datetime.time(hour=hour, minute=minute) 162 | d = datetime.datetime.combine(date, tod) + datetime.timedelta( 163 | seconds=addseconds 164 | ) 165 | return d 166 | except Exception as e: 167 | print(e) 168 | print("Time parser failed for {} giving {}".format(s, mgd)) 169 | m = reIPNsod.match(s) 170 | if m: # 140308_13764 171 | mgd = m.groupdict() 172 | year = int(mgd["year"]) 173 | if year < 100: 174 | # Use 1960-2060 for 2-digit years 175 | year += 2000 if (year < 60) else 1900 176 | month = int(mgd["month"]) 177 | day = int(mgd["day"]) 178 | sod = float(mgd["sod"]) 179 | d = datetime.datetime(year=year, month=month, day=day) + datetime.timedelta( 180 | seconds=sod 181 | ) 182 | return d 183 | m = refitstime.match(s) 184 | if m: # 2004-12-05T19:43:27 185 | mgd = m.groupdict() 186 | if mgd["fracsecond"] == None: 187 | mgd["microsecond"] = 0 188 | else: 189 | mgd["microsecond"] = int(1e6 * float("0." + mgd["fracsecond"])) 190 | for k in mgd.keys(): 191 | if mgd[k]: 192 | mgd[k] = int(mgd[k]) 193 | else: 194 | mgd[k] = 0 195 | d = datetime.datetime( 196 | year=mgd["year"], 197 | month=mgd["month"], 198 | day=mgd["day"], 199 | hour=mgd["hour"], 200 | minute=mgd["minute"], 201 | second=mgd["second"], 202 | microsecond=mgd["microsecond"], 203 | ) 204 | return d 205 | m = redoytime.match(s) 206 | if m: # 2004:329:12:15:07 207 | mgd = m.groupdict() 208 | if mgd["fracsecond"] == None: 209 | mgd["microsecond"] = 0 210 | else: 211 | mgd["microsecond"] = int(1e6 * float("0." + mgd["fracsecond"])) 212 | for k in mgd.keys(): 213 | if mgd[k]: 214 | mgd[k] = int(mgd[k]) 215 | else: 216 | mgd[k] = 0 217 | d = datetime.datetime( 218 | year=mgd["year"], 219 | month=1, 220 | day=1, 221 | hour=mgd["hour"], 222 | minute=mgd["minute"], 223 | second=mgd["second"], 224 | microsecond=mgd["microsecond"], 225 | ) 226 | d += datetime.timedelta(days=mgd["doy"] - 1) 227 | return d 228 | m = remjdtime.match(s) 229 | if m: # MJD 14192.5273 230 | d = mjdepoch + datetime.timedelta(days=float(m.groupdict()["mjd"])) 231 | return d 232 | m = rejdtime.match(s) 233 | if ( 234 | m 235 | ): # JD2454192.8273 Note that the 'JD24' is found by the regex, leaving the truncated JD 236 | d = mytruncjdepoch + datetime.timedelta( 237 | days=float(m.groupdict()["mytruncjd"]) 238 | ) 239 | return d 240 | # None of the patterns match, try treating it as a straight number of seconds 241 | met = float(s) 242 | if correct: 243 | utcf_ = utcf(met, not nocomplaint, False) 244 | met += utcf_ 245 | return met2datetime(met) 246 | except: 247 | if nocomplaint: 248 | return None 249 | else: 250 | print( 251 | "Invalid time '%s'. Valid formats: 2004-12-05T19:43:27, 2004:329:12:15:07, 123456789.01234, JD2454192.8273, MJD14192.5273, 140308_13764" 252 | % (s), 253 | file=sys.stderr, 254 | ) 255 | raise 256 | 257 | 258 | def string2met(s, nocomplaint=False, correct=False): 259 | d = string2datetime(s, nocomplaint, correct=correct) 260 | if d == None: 261 | return 0 262 | else: 263 | return datetime2met(d, correct=correct) 264 | 265 | 266 | # FIXME replace with .total_seconds() throughout 267 | def timedelta2seconds(td): 268 | return td.days * 86400.0 + (td.seconds * 1.0 + td.microseconds * 1e-6) 269 | 270 | 271 | def met2datetime(met, correct=False): 272 | if correct: 273 | met += utcf(met, False, False) 274 | return swiftepoch + datetime.timedelta(seconds=met) 275 | 276 | 277 | def datetime2mjd(dt): 278 | return (dt - mjdepoch).total_seconds() / 86400 279 | 280 | 281 | def mjd2datetime(mjd): 282 | return mjdepoch + datetime.timedelta(days=mjd) 283 | 284 | 285 | def datetime2met(dt, correct=False): 286 | met = timedelta2seconds(dt - swiftepoch) 287 | if correct: 288 | met -= utcf(met, False, False) 289 | return met 290 | 291 | 292 | def met2fitsString(met, milliseconds=False, correct=False, format=fitstimeformat): 293 | d = met2datetime(met, correct=correct) 294 | if milliseconds: 295 | ms_string = ".%03d" % min(int(d.microsecond / 1000.0 + 0.5), 999) 296 | else: 297 | ms_string = "" 298 | qspformat = format.split("%q") # Handle the %q -> seconds of day extension 299 | if len(qspformat) > 1: 300 | sod_string = ( 301 | "%05d" % (((60 * d.hour + d.minute) * 60) + d.second,) 302 | ) + ms_string 303 | s = sod_string.join([d.strftime(subformat) for subformat in qspformat]) 304 | else: 305 | s = d.strftime(format) + ms_string 306 | return s 307 | 308 | 309 | def met2mjd(met, correct=False): 310 | if correct: 311 | met += utcf(met, False, False) 312 | deltamjd = swiftepoch - mjdepoch 313 | mjd = met / 86400.0 + deltamjd.days + deltamjd.seconds / 86400.0 314 | return mjd 315 | 316 | 317 | def removeNonAscii(s): 318 | """Useful utilitiy to fix up, e.g. email files""" 319 | # http://stackoverflow.com/questions/1342000/how-to-replace-non-ascii-characters-in-string 320 | return "".join(i for i in s if ord(i) < 128) 321 | 322 | 323 | def findInSearchPath(envname, pathlist, basename): 324 | """ 325 | Look for file: If ${envname} in in environment, return it, else search 326 | each directory in the path list for a file or directory named basename 327 | """ 328 | try: 329 | return os.environ[envname] 330 | except: 331 | pass 332 | if type(pathlist) is str: 333 | pathlist = pathlist.split(":") 334 | for p in pathlist: 335 | try: 336 | stat = os.stat(os.path.join(p, basename)) 337 | return os.path.join(p, basename) 338 | except: 339 | pass 340 | raise ValueError("Could not find %s in paths : %s" % (basename, pathlist)) 341 | 342 | 343 | class TeeFile(io.TextIOWrapper): 344 | def __init__(self, *args): 345 | self._outfiles = args 346 | self._flush = True 347 | 348 | def write(self, towrite): 349 | for f in self._outfiles: 350 | f.write(towrite) 351 | if self._flush: 352 | f.flush() 353 | 354 | 355 | # Decorator for timeout 356 | # http://www.saltycrane.com/blog/2010/04/using-python-timeout-decorator-uploading-s3/ 357 | # from http://code.activestate.com/recipes/307871-timing-out-function/ 358 | import signal 359 | 360 | 361 | class TimeoutError(Exception): 362 | def __init__(self, value="Timed Out"): 363 | self.value = value 364 | 365 | def __str__(self): 366 | return repr(self.value) 367 | 368 | 369 | def timeout(seconds_before_timeout): 370 | def decorate(f): 371 | def handler(signum, frame): 372 | raise TimeoutError() 373 | 374 | def new_f(*args, **kwargs): 375 | old = signal.signal(signal.SIGALRM, handler) 376 | signal.alarm(seconds_before_timeout) 377 | try: 378 | result = f(*args, **kwargs) 379 | finally: 380 | signal.signal(signal.SIGALRM, old) 381 | signal.alarm(0) 382 | return result 383 | 384 | new_f.func_name = f.func_name 385 | return new_f 386 | 387 | return decorate 388 | 389 | 390 | # Decorator to tee stdout to an open file 391 | 392 | 393 | def TeeStdoutDecorator(fn, __teefile): 394 | def inner(*args, **kwargs): 395 | ostdout = sys.stdout 396 | try: 397 | sys.stdout = TeeFile(__teefile, sys.stdout) 398 | ret = fn(*args, **kwargs) 399 | sys.stdout = ostdout 400 | except: 401 | sys.stdout = ostdout 402 | raise 403 | return ret 404 | 405 | return inner 406 | 407 | 408 | def testTeeStdoutDecorator(): 409 | def testTeeStdoutUndecorated(x): 410 | print(x) 411 | print(1 / x) 412 | 413 | testTeeStdout = TeeStdoutDecorator( 414 | testTeeStdoutUndecorated, open("/tmp/teetest", "a") 415 | ) 416 | testTeeStdout(5) 417 | testTeeStdout(0) 418 | 419 | 420 | # http://stackoverflow.com/questions/132058/showing-the-stack-trace-from-a-running-python-application 421 | # 422 | def dumponsignal(fname="/tmp/python_trace"): 423 | import threading, sys, traceback 424 | 425 | def dumpstacks(signal, frame): 426 | id2name = dict([(th.ident, th.name) for th in threading.enumerate()]) 427 | code = [] 428 | for threadId, stack in sys._current_frames().items(): 429 | code.append("\n# Thread: %s(%d)" % (id2name.get(threadId, ""), threadId)) 430 | for filename, lineno, name, line in traceback.extract_stack(stack): 431 | code.append('File: "%s", line %d, in %s' % (filename, lineno, name)) 432 | if line: 433 | code.append(" %s" % (line.strip())) 434 | print("\n".join(code)) 435 | open(fname, "a").write("\n".join(code)) 436 | 437 | import signal 438 | 439 | signal.signal(signal.SIGQUIT, dumpstacks) 440 | 441 | 442 | def main(argv): 443 | for s in argv: 444 | print( 445 | "%-30s -> %23s (corrected) = MET %f" 446 | % (s, string2datetime(s, False, True), datetime2met(string2datetime(s))) 447 | ) 448 | 449 | 450 | if __name__ == "__main__": 451 | if len(sys.argv) > 1: 452 | main(sys.argv[1:]) 453 | else: 454 | main( 455 | [ 456 | "20041225_S12345.67", 457 | "2004-12-25_12345", 458 | "20041225_12345", 459 | "2004-12-25_12345.67", 460 | "2004-12-05T19:43:27.23", 461 | "2004-12-25", 462 | "2004:329:12:15:07.45", 463 | "123456789.01234", 464 | "20041225_S12345.67", 465 | ] 466 | ) 467 | -------------------------------------------------------------------------------- /tests/sampleevents.np: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lanl/swiftbat_python/8abd7ab2909af2bd35b92e356b6516e4534730e1/tests/sampleevents.np -------------------------------------------------------------------------------- /tests/testclock.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import unittest 3 | 4 | import swiftbat 5 | from swiftbat.clockinfo import clockErrData 6 | from pathlib import Path 7 | import tempfile 8 | import datetime 9 | 10 | 11 | class ClockTestCase(unittest.TestCase): 12 | def test_utcf(self): 13 | testvector = [ 14 | (swiftbat.string2met("2018-08-29 23:31:01", correct=False), -21.50867) 15 | ] 16 | clockerrdata = clockErrData() 17 | clockerrfile = Path(clockerrdata._clockfile) 18 | self.assertTrue(clockerrfile.exists()) 19 | for t, utcf_expected in testvector: 20 | utcf_returned = swiftbat.utcf(t) 21 | self.assertAlmostEqual(utcf_returned, utcf_expected, places=3) 22 | 23 | def test_clock_download(self): 24 | clockerrdata = clockErrData() 25 | with tempfile.TemporaryDirectory() as tempclockdir: 26 | clockerrdata.updateclockfiles(tempclockdir, ifolderthan_days=0) 27 | file = next(Path(tempclockdir).glob("swclock*.fits")) 28 | self.assertTrue(file.exists()) 29 | 30 | 31 | if __name__ == "__main__": 32 | unittest.main() 33 | -------------------------------------------------------------------------------- /tests/testswinfo.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import unittest 3 | 4 | import swiftbat 5 | 6 | 7 | class SwInfoTestCase(unittest.TestCase): 8 | def test_source(self): 9 | s1020 = swiftbat.source(ra=10, dec=20) 10 | sunnow = swiftbat.sunsource() 11 | sun = swiftbat.sunsource("2023-03-21 06:00") # Near the Vernal equinox 12 | crab = swiftbat.source("Crab") 13 | cygx1 = swiftbat.source("Cyg_x-1") 14 | for source in [s1020, sun, crab, cygx1]: 15 | print(f"{source.name:10s} {source.ra_deg:8.3f} {source.dec_deg:8.3f}") 16 | pass 17 | 18 | def test_swinfo_cli(self): 19 | swiftbat.swinfo_main( 20 | "swinfo_test 2020-05-05T12:34:56 -o -s cyg_X-1 -S".split() 21 | ) 22 | """ 23 | swinfo 2020-05-05T12:34:56 -o -s cyg_X-1 -S 24 | Swift Zenith(RA,dec): 232.97, -20.46 25 | Swift Location(lon,lat,alt): -179.31 E, -20.53 N, 549 km 26 | Time(MET + UTCF -> UT): 610374920.889 + -24.888 -> 2020-05-05T12:34:55.999 27 | YYMMDD_SOD: 200505_45295.999 DOY=126 28 | Obs Sequence Number: 00033349058 29 | Obs Target Name: SGR 1935+2154 30 | Obs Date and Times: 2020-05-05 12:32:37 - 13:01:34 31 | Obs Pointing(ra,dec,roll): 293.724, 21.897, 62.77 32 | Cyg_X-1_imageloc (boresight_dist, angle): (14, -133) 33 | Cyg_X-1_exposure (cm^2 cos adjusted): 4230 34 | Cyg_X-1_altitude: 29 (up) 35 | Sun_imageloc (boresight_dist, angle): (101, -85) 36 | Sun_exposure (cm^2 cos adjusted): 0 37 | Sun_altitude: -57 (down) 38 | """ 39 | 40 | 41 | if __name__ == "__main__": 42 | unittest.main() 43 | -------------------------------------------------------------------------------- /tests/testxy.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import swiftbat 4 | import numpy as np 5 | from pathlib import Path 6 | 7 | 8 | class TestXY(unittest.TestCase): 9 | def getsample(self): 10 | """ 11 | Test data from BAT data 12 | 13 | from astropy.io import fits 14 | d = fits.getdata('/tmp/sw01090661000bevshto_uf.evt.gz') 15 | sample = np.array(d[10:1000]) 16 | np.save(open(file, "wb"), sample) 17 | """ 18 | file = Path(__file__).parent.joinpath("sampleevents.np") 19 | sample = np.load(open(file, "rb")) 20 | return sample 21 | 22 | def test_detid2xy_scalar(self): 23 | sample = self.getsample() 24 | for samplerow in sample[0:100]: 25 | x, y = swiftbat.detid2xy(samplerow["DET_ID"]) 26 | assert (x, y) == (samplerow["DETX"], samplerow["DETY"]) 27 | 28 | def test_detid2xy_array(self): 29 | sample = self.getsample() 30 | x, y = swiftbat.detid2xy(sample["DET_ID"]) 31 | assert np.allclose(x, sample["DETX"]) 32 | assert np.allclose(y, sample["DETY"]) 33 | 34 | def test_xy2detid_scalar(self): 35 | sample = self.getsample() 36 | for samplerow in sample[0:100]: 37 | detid = swiftbat.xy2detid(samplerow["DETX"], samplerow["DETY"]) 38 | assert detid == samplerow["DET_ID"] 39 | 40 | def test_xy2detid_array(self): 41 | sample = self.getsample() 42 | detid = swiftbat.xy2detid(sample["DETX"], sample["DETY"]) 43 | assert np.allclose(detid, sample["DET_ID"]) 44 | 45 | 46 | if __name__ == "__main__": 47 | unittest.main() 48 | --------------------------------------------------------------------------------