timetag-explorer 1.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. timetag_explorer-1.0.1/LICENSE +29 -0
  2. timetag_explorer-1.0.1/PKG-INFO +129 -0
  3. timetag_explorer-1.0.1/README.md +95 -0
  4. timetag_explorer-1.0.1/pyproject.toml +48 -0
  5. timetag_explorer-1.0.1/setup.cfg +4 -0
  6. timetag_explorer-1.0.1/tests/test_dataset.py +55 -0
  7. timetag_explorer-1.0.1/tests/test_frames.py +28 -0
  8. timetag_explorer-1.0.1/tests/test_gdcorr.py +26 -0
  9. timetag_explorer-1.0.1/tests/test_package.py +43 -0
  10. timetag_explorer-1.0.1/tests/test_planet.py +71 -0
  11. timetag_explorer-1.0.1/tests/test_regions.py +38 -0
  12. timetag_explorer-1.0.1/tests/test_selection_script.py +36 -0
  13. timetag_explorer-1.0.1/timetag_explorer/__init__.py +21 -0
  14. timetag_explorer-1.0.1/timetag_explorer/analysis.py +93 -0
  15. timetag_explorer-1.0.1/timetag_explorer/config.py +25 -0
  16. timetag_explorer-1.0.1/timetag_explorer/constants.py +57 -0
  17. timetag_explorer-1.0.1/timetag_explorer/dataset.py +459 -0
  18. timetag_explorer-1.0.1/timetag_explorer/frames.py +282 -0
  19. timetag_explorer-1.0.1/timetag_explorer/gdcorr.py +41 -0
  20. timetag_explorer-1.0.1/timetag_explorer/horizons.py +166 -0
  21. timetag_explorer-1.0.1/timetag_explorer/log.py +20 -0
  22. timetag_explorer-1.0.1/timetag_explorer/planet.py +246 -0
  23. timetag_explorer-1.0.1/timetag_explorer/regions.py +67 -0
  24. timetag_explorer-1.0.1/timetag_explorer/selection_script.py +494 -0
  25. timetag_explorer-1.0.1/timetag_explorer/transforms.py +578 -0
  26. timetag_explorer-1.0.1/timetag_explorer.egg-info/PKG-INFO +129 -0
  27. timetag_explorer-1.0.1/timetag_explorer.egg-info/SOURCES.txt +28 -0
  28. timetag_explorer-1.0.1/timetag_explorer.egg-info/dependency_links.txt +1 -0
  29. timetag_explorer-1.0.1/timetag_explorer.egg-info/requires.txt +8 -0
  30. timetag_explorer-1.0.1/timetag_explorer.egg-info/top_level.txt +1 -0
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Sean Lockwood
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: timetag-explorer
3
+ Version: 1.0.1
4
+ Summary: The science code behind TIME-TAG Explorer: HST/STIS FUV TIME-TAG processing, reference frames, planet maps and selection scripts
5
+ Author: Sean Lockwood
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://explorer.planetarylightshow.com
8
+ Project-URL: Documentation, https://timetag-explorer.readthedocs.io
9
+ Project-URL: Source, https://github.com/sean-lockwood/timetag-explorer
10
+ Project-URL: Issues, https://github.com/sean-lockwood/timetag-explorer/issues
11
+ Project-URL: Changelog, https://github.com/sean-lockwood/timetag-explorer/blob/main/CHANGELOG.md
12
+ Keywords: astronomy,HST,STIS,TIME-TAG,FUV,aurora
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Scientific/Engineering :: Astronomy
23
+ Requires-Python: >=3.11
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: numpy>=2.0
27
+ Requires-Dist: astropy>=7.0
28
+ Requires-Dist: matplotlib>=3.9
29
+ Requires-Dist: astroquery>=0.4.10
30
+ Provides-Extra: docs
31
+ Requires-Dist: sphinx>=7.2; extra == "docs"
32
+ Requires-Dist: sphinx-rtd-theme>=2.0; extra == "docs"
33
+ Dynamic: license-file
34
+
35
+ # timetag-explorer
36
+
37
+ [![Tests](https://github.com/sean-lockwood/timetag-explorer/actions/workflows/ci.yml/badge.svg)](https://github.com/sean-lockwood/timetag-explorer/actions/workflows/ci.yml)
38
+ [![PyPI](https://img.shields.io/pypi/v/timetag-explorer)](https://pypi.org/project/timetag-explorer/)
39
+ [![Documentation](https://app.readthedocs.org/projects/timetag-explorer/badge/?version=latest)](https://timetag-explorer.readthedocs.io)
40
+
41
+ The science code behind [TIME-TAG Explorer](https://explorer.planetarylightshow.com), a web app for exploring
42
+ HST/STIS FUV TIME-TAG observations (aurorae on Jupiter and Saturn, and other targets that move or vary).
43
+
44
+ This repository holds **only the calculations**: reading a TIME-TAG file, the processing steps, the transformations
45
+ between reference frames and planet maps, the exposure-time maps, and the generator of the standalone "selection
46
+ scripts". The web server around it (sign-in, pages, plots and mouse selection) is a separate, private project, and
47
+ this package knows nothing about it. Everything the app computes about the data comes from here, so this is the part
48
+ that scientific review should look at.
49
+
50
+ > **Status: first release, not yet validated.** [VALIDATION.md](https://github.com/sean-lockwood/timetag-explorer/blob/main/VALIDATION.md) lists each step with its reference and
51
+ > its review status. The geometric distortion correction is **disabled** in this release: `gdcorr.py` holds
52
+ > placeholder (zero) coefficients until values from a public reference file replace them.
53
+
54
+ ## Install
55
+
56
+ Python 3.11 or newer.
57
+
58
+ ```
59
+ pip install timetag-explorer
60
+ ```
61
+
62
+ To work on the code, clone the repository and `pip install -e .` in it. Documentation:
63
+ <https://timetag-explorer.readthedocs.io>.
64
+
65
+ ## A first look
66
+
67
+ ```python
68
+ from timetag_explorer import load_dataset, Corrections, Box, describe_selection
69
+
70
+ ds = load_dataset('od8k57itq_tag.fits') # a TIME-TAG file from MAST; its _spt.fits file, if next to it, is used too
71
+ corr = Corrections(randcorr=True, flatcorr=False, timecorr=True) # which processing steps are on
72
+
73
+ edges, rate = ds.light_curve(3.0) # count rate in 3 s bins
74
+ image = ds.image(time_sel=(100.0, 250.0), frame='image', corr=corr) # counts/s per 4x4 hi-res-pixel bin
75
+ region = Box(900, 1100, 900, 1100) # detector pixels
76
+ edges, region_rate = ds.light_curve(3.0, region=region, corr=corr)
77
+ print(describe_selection(ds, corr, 'image', (100.0, 250.0), region))
78
+ ```
79
+
80
+ Rest frames and planet maps (`frame='rest'`, `'planet'`, `'map_rect'`, ...) need the exposure's SPT file next to the
81
+ TIME-TAG file; the planet ones also ask JPL Horizons for positions (through `astroquery`) and keep the answers in
82
+ `config.HORIZONS_DIR`. The flat field is optional: put `combined_flat.fits`
83
+ ([download](https://www.planetarylightshow.com/static/data/combined_flat.fits);
84
+ [how it was made](https://www.planetarylightshow.com/aurora_processing.html#Flat-Fielding)) where `config.FLAT_FILE` points,
85
+ or set the environment variables `TAG_EXPLORER_FLAT` and `TAG_EXPLORER_HORIZONS`.
86
+
87
+ `build_selection_script(ds, time_sel, region, frame, corr)` returns a standalone Python script that reproduces a
88
+ selection on the original TIME-TAG file and writes flag columns into a copy of it.
89
+
90
+ ## What is where
91
+
92
+ | Module | Contents |
93
+ |---|---|
94
+ | `regions` | `Box`, `Circle`, `Polygon`: which detector pixels a region holds |
95
+ | `frames` | the processing switches (`Corrections`), reference frames, header parsing (pointing, slew, WCS) |
96
+ | `horizons` | planet and moon positions from JPL Horizons, and the local cache of them |
97
+ | `planet` | Jupiter and Saturn orientation (IAU rotation model, light time) and the sky / surface / map projections |
98
+ | `transforms` | detector pixels to frames and maps, exposure-time maps, the distortion lookup table |
99
+ | `dataset` | `TagDataset`: events, slicing in time, binning, light curves, count-rate images |
100
+ | `analysis` | areas, mean brightness and the exposure for a selection |
101
+ | `selection_script` | the generator of selection scripts (and its version, `SELVER`) |
102
+ | `gdcorr` | the geometric distortion polynomial (placeholder coefficients for now) |
103
+ | `constants`, `config`, `log` | shared numbers; file locations; logging |
104
+
105
+ ## Tests and validation
106
+
107
+ ```
108
+ pip install -e .
109
+ for t in tests/test_*.py; do python "$t"; done # quick: synthetic data, no network
110
+ python validation/check_selection_scripts.py OFAP44G5Q # slow: runs generated scripts on a real exposure (downloads it)
111
+ ```
112
+
113
+ The first set runs on every pull request. The second runs every week on GitHub Actions. See
114
+ [VALIDATION.md](https://github.com/sean-lockwood/timetag-explorer/blob/main/VALIDATION.md) for what each step is checked against.
115
+
116
+ ## Contributing
117
+
118
+ Questions and concerns about the science are welcome as [issues](https://github.com/sean-lockwood/timetag-explorer/issues); proposed changes as pull requests.
119
+ Please read [CONTRIBUTING.md](https://github.com/sean-lockwood/timetag-explorer/blob/main/CONTRIBUTING.md) first. Requests for access to the web app go to the address on the
120
+ [app's front page](https://explorer.planetarylightshow.com/about), not here.
121
+
122
+ ## Citing
123
+
124
+ Use the "Cite this repository" button on GitHub, or [CITATION.cff](https://github.com/sean-lockwood/timetag-explorer/blob/main/CITATION.cff). A DOI will be added once the first
125
+ release is archived.
126
+
127
+ ## License
128
+
129
+ BSD 3-Clause. See [LICENSE](https://github.com/sean-lockwood/timetag-explorer/blob/main/LICENSE).
@@ -0,0 +1,95 @@
1
+ # timetag-explorer
2
+
3
+ [![Tests](https://github.com/sean-lockwood/timetag-explorer/actions/workflows/ci.yml/badge.svg)](https://github.com/sean-lockwood/timetag-explorer/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/timetag-explorer)](https://pypi.org/project/timetag-explorer/)
5
+ [![Documentation](https://app.readthedocs.org/projects/timetag-explorer/badge/?version=latest)](https://timetag-explorer.readthedocs.io)
6
+
7
+ The science code behind [TIME-TAG Explorer](https://explorer.planetarylightshow.com), a web app for exploring
8
+ HST/STIS FUV TIME-TAG observations (aurorae on Jupiter and Saturn, and other targets that move or vary).
9
+
10
+ This repository holds **only the calculations**: reading a TIME-TAG file, the processing steps, the transformations
11
+ between reference frames and planet maps, the exposure-time maps, and the generator of the standalone "selection
12
+ scripts". The web server around it (sign-in, pages, plots and mouse selection) is a separate, private project, and
13
+ this package knows nothing about it. Everything the app computes about the data comes from here, so this is the part
14
+ that scientific review should look at.
15
+
16
+ > **Status: first release, not yet validated.** [VALIDATION.md](https://github.com/sean-lockwood/timetag-explorer/blob/main/VALIDATION.md) lists each step with its reference and
17
+ > its review status. The geometric distortion correction is **disabled** in this release: `gdcorr.py` holds
18
+ > placeholder (zero) coefficients until values from a public reference file replace them.
19
+
20
+ ## Install
21
+
22
+ Python 3.11 or newer.
23
+
24
+ ```
25
+ pip install timetag-explorer
26
+ ```
27
+
28
+ To work on the code, clone the repository and `pip install -e .` in it. Documentation:
29
+ <https://timetag-explorer.readthedocs.io>.
30
+
31
+ ## A first look
32
+
33
+ ```python
34
+ from timetag_explorer import load_dataset, Corrections, Box, describe_selection
35
+
36
+ ds = load_dataset('od8k57itq_tag.fits') # a TIME-TAG file from MAST; its _spt.fits file, if next to it, is used too
37
+ corr = Corrections(randcorr=True, flatcorr=False, timecorr=True) # which processing steps are on
38
+
39
+ edges, rate = ds.light_curve(3.0) # count rate in 3 s bins
40
+ image = ds.image(time_sel=(100.0, 250.0), frame='image', corr=corr) # counts/s per 4x4 hi-res-pixel bin
41
+ region = Box(900, 1100, 900, 1100) # detector pixels
42
+ edges, region_rate = ds.light_curve(3.0, region=region, corr=corr)
43
+ print(describe_selection(ds, corr, 'image', (100.0, 250.0), region))
44
+ ```
45
+
46
+ Rest frames and planet maps (`frame='rest'`, `'planet'`, `'map_rect'`, ...) need the exposure's SPT file next to the
47
+ TIME-TAG file; the planet ones also ask JPL Horizons for positions (through `astroquery`) and keep the answers in
48
+ `config.HORIZONS_DIR`. The flat field is optional: put `combined_flat.fits`
49
+ ([download](https://www.planetarylightshow.com/static/data/combined_flat.fits);
50
+ [how it was made](https://www.planetarylightshow.com/aurora_processing.html#Flat-Fielding)) where `config.FLAT_FILE` points,
51
+ or set the environment variables `TAG_EXPLORER_FLAT` and `TAG_EXPLORER_HORIZONS`.
52
+
53
+ `build_selection_script(ds, time_sel, region, frame, corr)` returns a standalone Python script that reproduces a
54
+ selection on the original TIME-TAG file and writes flag columns into a copy of it.
55
+
56
+ ## What is where
57
+
58
+ | Module | Contents |
59
+ |---|---|
60
+ | `regions` | `Box`, `Circle`, `Polygon`: which detector pixels a region holds |
61
+ | `frames` | the processing switches (`Corrections`), reference frames, header parsing (pointing, slew, WCS) |
62
+ | `horizons` | planet and moon positions from JPL Horizons, and the local cache of them |
63
+ | `planet` | Jupiter and Saturn orientation (IAU rotation model, light time) and the sky / surface / map projections |
64
+ | `transforms` | detector pixels to frames and maps, exposure-time maps, the distortion lookup table |
65
+ | `dataset` | `TagDataset`: events, slicing in time, binning, light curves, count-rate images |
66
+ | `analysis` | areas, mean brightness and the exposure for a selection |
67
+ | `selection_script` | the generator of selection scripts (and its version, `SELVER`) |
68
+ | `gdcorr` | the geometric distortion polynomial (placeholder coefficients for now) |
69
+ | `constants`, `config`, `log` | shared numbers; file locations; logging |
70
+
71
+ ## Tests and validation
72
+
73
+ ```
74
+ pip install -e .
75
+ for t in tests/test_*.py; do python "$t"; done # quick: synthetic data, no network
76
+ python validation/check_selection_scripts.py OFAP44G5Q # slow: runs generated scripts on a real exposure (downloads it)
77
+ ```
78
+
79
+ The first set runs on every pull request. The second runs every week on GitHub Actions. See
80
+ [VALIDATION.md](https://github.com/sean-lockwood/timetag-explorer/blob/main/VALIDATION.md) for what each step is checked against.
81
+
82
+ ## Contributing
83
+
84
+ Questions and concerns about the science are welcome as [issues](https://github.com/sean-lockwood/timetag-explorer/issues); proposed changes as pull requests.
85
+ Please read [CONTRIBUTING.md](https://github.com/sean-lockwood/timetag-explorer/blob/main/CONTRIBUTING.md) first. Requests for access to the web app go to the address on the
86
+ [app's front page](https://explorer.planetarylightshow.com/about), not here.
87
+
88
+ ## Citing
89
+
90
+ Use the "Cite this repository" button on GitHub, or [CITATION.cff](https://github.com/sean-lockwood/timetag-explorer/blob/main/CITATION.cff). A DOI will be added once the first
91
+ release is archived.
92
+
93
+ ## License
94
+
95
+ BSD 3-Clause. See [LICENSE](https://github.com/sean-lockwood/timetag-explorer/blob/main/LICENSE).
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "timetag-explorer"
7
+ dynamic = ["version"]
8
+ description = "The science code behind TIME-TAG Explorer: HST/STIS FUV TIME-TAG processing, reference frames, planet maps and selection scripts"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "BSD-3-Clause"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Sean Lockwood" }]
14
+ keywords = ["astronomy", "HST", "STIS", "TIME-TAG", "FUV", "aurora"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Science/Research",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Topic :: Scientific/Engineering :: Astronomy",
26
+ ]
27
+ dependencies = [
28
+ "numpy>=2.0",
29
+ "astropy>=7.0",
30
+ "matplotlib>=3.9", # only for its point-in-polygon test
31
+ "astroquery>=0.4.10", # JPL Horizons, for the planet frames and maps
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ docs = ["sphinx>=7.2", "sphinx-rtd-theme>=2.0"]
36
+
37
+ [project.urls]
38
+ Homepage = "https://explorer.planetarylightshow.com"
39
+ Documentation = "https://timetag-explorer.readthedocs.io"
40
+ Source = "https://github.com/sean-lockwood/timetag-explorer"
41
+ Issues = "https://github.com/sean-lockwood/timetag-explorer/issues"
42
+ Changelog = "https://github.com/sean-lockwood/timetag-explorer/blob/main/CHANGELOG.md"
43
+
44
+ [tool.setuptools.dynamic]
45
+ version = { attr = "timetag_explorer.__version__" }
46
+
47
+ [tool.setuptools.packages.find]
48
+ include = ["timetag_explorer*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,55 @@
1
+ """TagDataset on small synthetic exposures: counts, rates, exposure, regions and the processing steps."""
2
+ import support # noqa: F401
3
+ import numpy as np
4
+ from support import OFF, dataset
5
+
6
+ from timetag_explorer.constants import EDGE_TRIM_PIXELS, SPATIAL_BIN
7
+ from timetag_explorer.frames import Corrections
8
+ from timetag_explorer.regions import Box
9
+
10
+ # 1000 events, 10 per second for 100 s, all in one pixel
11
+ n = 1000
12
+ t = np.linspace(0, 99.9, n)
13
+ ds = dataset(t, np.full(n, 100), np.full(n, 100))
14
+ assert ds.exposure() == 100.0 and abs(ds.tmax - 99.9) < 1e-3
15
+ img = ds.image(None, 'image', OFF)
16
+ assert abs(img.sum() - 10.0) < 1e-9, img.sum() # counts per second
17
+ img10 = ds.image((0.0, 10.0), 'image', OFF)
18
+ assert abs(img10.sum() - 10.0) < 1e-9 # a time window gives the rate in that window
19
+ edges, rate = ds.light_curve(10.0, None, 'image', OFF)
20
+ assert np.allclose(rate, 10.0) and len(rate) == 10
21
+
22
+ # a gap in the good-time intervals: its bins have no exposure (NaN) and the others are still right
23
+ gap = dataset(t[(t < 40) | (t >= 60)], np.full(800, 100), np.full(800, 100), gti=((0, 40), (60, 100)))
24
+ assert gap.exposure() == 80.0 and abs(gap.exposure(30, 70) - 20.0) < 1e-9
25
+ _, r = gap.light_curve(10.0, None, 'image', OFF)
26
+ assert np.isnan(r[4]) and np.isnan(r[5]) and np.allclose(r[[0, 1, 2, 3, 6, 7, 8, 9]], 10.0)
27
+
28
+ # a region: counts inside it only
29
+ x = np.where(np.arange(n) % 2 == 0, 100, 300) # every other event in each of two places
30
+ two = dataset(t, x, np.full(n, 100))
31
+ inside = two.light_curve(10.0, Box(90, 110, 90, 110), 'image', OFF)[1]
32
+ assert np.allclose(inside, 5.0)
33
+ assert abs(two.image(None, 'image', OFF).sum() - 10.0) < 1e-9
34
+ mask = two.region_mask(Box(90, 110, 90, 110), 'image', OFF)
35
+ assert mask.sum() == 500 and mask.dtype == bool
36
+
37
+ # flat-field weights multiply the counts
38
+ weighted = dataset(t, np.full(n, 100), np.full(n, 100), weight=np.full(n, 2.0))
39
+ assert abs(weighted.image(None, 'image', Corrections(randcorr=False, geocorr=False, flatcorr=True, timecorr=False)).sum() - 20.0) < 1e-6
40
+ assert abs(weighted.image(None, 'image', OFF).sum() - 10.0) < 1e-9 # switched off: unweighted
41
+
42
+ # the vignetted border is discarded only with the exposure-time correction
43
+ edge = dataset(t, np.full(n, EDGE_TRIM_PIXELS - 20), np.full(n, 100))
44
+ assert abs(edge.image(None, 'image', OFF).sum() - 10.0) < 1e-9
45
+ assert edge.image(None, 'image', Corrections(randcorr=False, geocorr=False, flatcorr=False, timecorr=True)).sum() == 0.0
46
+
47
+ # the random sub-pixel offset moves events by less than a pixel, keeps the total, and repeats exactly
48
+ rand = Corrections(randcorr=True, geocorr=False, flatcorr=False, timecorr=False)
49
+ a = dataset(t, np.full(n, 100), np.full(n, 100)).image(None, 'image', rand)
50
+ b = dataset(t, np.full(n, 100), np.full(n, 100)).image(None, 'image', rand)
51
+ assert np.array_equal(a, b) and abs(a.sum() - 10.0) < 1e-9 and a.shape[0] * SPATIAL_BIN <= 2048
52
+
53
+ # a window outside the data
54
+ assert ds.image((200.0, 300.0), 'image', OFF).sum() == 0.0
55
+ print('dataset ok')
@@ -0,0 +1,28 @@
1
+ """Header parsing and the small rules around frames and light-curve bins."""
2
+ import support # noqa: F401
3
+
4
+ from timetag_explorer.constants import DEFAULT_TIME_BIN, MAX_LIGHT_CURVE_BINS, MIN_TIME_BIN
5
+ from timetag_explorer.frames import (FRAMES, FrameInfo, clamp_binsize, is_map, is_north, is_planet, names_from_levels,
6
+ parse_mt_levels)
7
+
8
+ # Motion-tracking levels: parts of one level are joined in order
9
+ header = {'MT_LV1_1': 'TYPE=STD,NAME=JUPI', 'MT_LV1_2': 'TER', 'MT_LV2_1': 'TYPE=POS_ANGLE,ANG=44.0,R_RAD=0.5',
10
+ 'OTHER': 'x'}
11
+ levels = parse_mt_levels(header)
12
+ assert levels[0] == {'TYPE': 'STD', 'NAME': 'JUPITER'}
13
+ assert levels[1] == {'TYPE': 'POS_ANGLE', 'ANG': '44.0', 'R_RAD': '0.5'}
14
+ assert parse_mt_levels({}) == []
15
+ assert isinstance(names_from_levels(levels), tuple)
16
+
17
+ # Light-curve bins are always usable, whatever was typed
18
+ assert clamp_binsize(3) == 3.0
19
+ assert clamp_binsize('nan') == DEFAULT_TIME_BIN and clamp_binsize(None) == DEFAULT_TIME_BIN
20
+ assert clamp_binsize(0) == MIN_TIME_BIN and clamp_binsize(-5) == MIN_TIME_BIN
21
+ assert clamp_binsize(1e-9, tmax=1000.0) == 1000.0 / MAX_LIGHT_CURVE_BINS # not billions of bins
22
+
23
+ # Frame names
24
+ assert is_map('map_rect') and not is_map('planet') and is_planet('planet_north') and is_north('rest_north')
25
+ assert 'image' in FRAMES and all(isinstance(v, str) for v in FRAMES.values())
26
+ info = FrameInfo(orientat=0.0, slew_angle=0.0, slew_rate=0.0)
27
+ assert info.unavailable_reason('image') is None and info.unavailable_reason('map_rect') is not None
28
+ print('frames ok')
@@ -0,0 +1,26 @@
1
+ """The geometric-distortion polynomial, and the placeholder state of this release."""
2
+ import support # noqa: F401
3
+ import numpy as np
4
+
5
+ from timetag_explorer import gdcorr
6
+ from timetag_explorer.frames import DEFAULT_CORRECTIONS, geocorr_available
7
+
8
+ # The polynomial itself: each coefficient multiplies one monomial.
9
+ x, y = 3.0, 5.0
10
+ monomials = [y, x, y * y, x * y, x * x, y ** 3, y * y * x, y * x * x, x ** 3, y ** 4, y ** 3 * x, y * y * x * x,
11
+ y * x ** 3, x ** 4]
12
+ for k, expected in enumerate(monomials):
13
+ c = [0.0] * 14
14
+ c[k] = 1.0
15
+ assert gdcorr.polyd4(x, y, c) == expected, k
16
+
17
+ # Placeholder coefficients: nothing is corrected, and the Explorer knows it.
18
+ assert gdcorr.PLACEHOLDER == (not (any(gdcorr.a) or any(gdcorr.b)))
19
+ if gdcorr.PLACEHOLDER:
20
+ gx, gy = np.meshgrid(np.arange(0, 2048, 64, dtype=float), np.arange(0, 2048, 64, dtype=float))
21
+ xc, yc = gdcorr.gdcorr(gx, gy)
22
+ assert np.array_equal(xc, gx) and np.array_equal(yc, gy)
23
+ assert not geocorr_available() and not DEFAULT_CORRECTIONS.geocorr
24
+ else: # real coefficients: the switch is available and on by default
25
+ assert geocorr_available()
26
+ print('gdcorr ok')
@@ -0,0 +1,43 @@
1
+ """The package stays free of web-server code, and its version numbers agree."""
2
+ import ast
3
+ import re
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import support # noqa: F401
9
+
10
+ root = Path(__file__).resolve().parent.parent
11
+ FORBIDDEN = {'nicegui', 'starlette', 'fastapi', 'uvicorn', 'httpx', 'authlib', 's3fs'}
12
+
13
+ # 1. No module imports a web framework (or anything else the server alone needs).
14
+ for path in sorted((root / 'timetag_explorer').glob('*.py')):
15
+ for node in ast.walk(ast.parse(path.read_text())):
16
+ names = [a.name for a in node.names] if isinstance(node, ast.Import) else (
17
+ [node.module or ''] if isinstance(node, ast.ImportFrom) and node.level == 0 else [])
18
+ for name in names:
19
+ assert name.split('.')[0] not in FORBIDDEN, f'{path.name} imports {name}'
20
+
21
+ # 2. Importing the package does not pull in a web framework or pyplot, and nothing runs at import time.
22
+ out = subprocess.run([sys.executable, '-c',
23
+ 'import sys, timetag_explorer\n'
24
+ 'bad = [m for m in sys.modules if m.split(".")[0] in %r or m == "matplotlib.pyplot"]\n'
25
+ 'print(",".join(bad))' % sorted(FORBIDDEN)],
26
+ capture_output=True, text=True, cwd=root)
27
+ assert out.returncode == 0, out.stderr
28
+ assert out.stdout.strip() == '', out.stdout
29
+
30
+ # 3. One version number, everywhere.
31
+ import timetag_explorer # noqa: E402
32
+
33
+ version = timetag_explorer.__version__
34
+ assert re.fullmatch(r'\d+\.\d+\.\d+', version), version
35
+ cff = (root / 'CITATION.cff').read_text()
36
+ assert re.search(rf'^version: "?{re.escape(version)}"?\s*$', cff, re.M), 'CITATION.cff version differs from __version__'
37
+ assert f'## [{version}]' in (root / 'CHANGELOG.md').read_text(), 'CHANGELOG.md has no entry for this version'
38
+
39
+ # 4. No private data in the package: no personal paths or addresses.
40
+ for path in sorted((root / 'timetag_explorer').glob('*.py')):
41
+ text = path.read_text()
42
+ assert '/home/' not in text and '@gmail.com' not in text, path.name
43
+ print('package ok')
@@ -0,0 +1,71 @@
1
+ """Planet geometry against answers that can be derived independently: round trips, areas of spheres and caps,
2
+ the rotation rate, and the leap seconds."""
3
+ import support # noqa: F401
4
+ import numpy as np
5
+
6
+ from timetag_explorer.planet import (AU_KM, BODIES, LIGHT_TIME_PER_AU, map_bin_area, map_plane_coordinates,
7
+ map_plane_to_surface, limb_outline, planet_orientation, screen_to_surface,
8
+ spheroid_radii, surface_to_screen, tdb_minus_utc)
9
+
10
+ jup = BODIES['JUPITER']
11
+ assert (jup.radius_eq_km, jup.radius_pol_km) == (71492.0, 66854.0) # IAU 2015 radii
12
+ assert spheroid_radii(jup, 0.0) == (1.0, 66854.0 / 71492.0)
13
+
14
+ # Leap seconds: TAI-UTC was 36 s until the end of 2016 and 37 s from 1 January 2017 (MJD 57754); TDB = TT + ~0.
15
+ assert tdb_minus_utc(57753) == 36 + 32.184 and tdb_minus_utc(57754) == 37 + 32.184
16
+ assert tdb_minus_utc(50082) == 29 + 32.184 or tdb_minus_utc(50082) == 30 + 32.184 # before the table: first entry
17
+
18
+ # The sub-observer point is at the centre of the disc, looking straight at it.
19
+ x, y, mu = surface_to_screen(100.0, 0.0, 0.0, 100.0, 1.0, 1.0)
20
+ assert abs(x) < 1e-12 and abs(y) < 1e-12 and abs(mu - 1.0) < 1e-12
21
+ # ... and the limb of a sphere seen from its equator is a circle of radius 1.
22
+ lx, ly = limb_outline(0.0, 1.0, 1.0)
23
+ assert np.allclose(np.hypot(lx, ly), 1.0)
24
+
25
+ # Round trip, surface -> screen -> surface, over the visible hemisphere of an oblate planet seen from 3 N.
26
+ a, b = spheroid_radii(jup, 400.0)
27
+ rng = np.random.default_rng(0)
28
+ lon = rng.uniform(0, 360, 2000)
29
+ lat = rng.uniform(-85, 85, 2000)
30
+ sx, sy, smu = surface_to_screen(lon, lat, 3.0, 120.0, a, b)
31
+ vis = smu > 0.05
32
+ lon2, lat2, mu2, on = screen_to_surface(sx[vis], sy[vis], 3.0, 120.0, a, b)
33
+ assert on.all() and np.allclose(mu2, smu[vis], atol=1e-9)
34
+ assert np.allclose(((lon2 - lon[vis] + 180) % 360) - 180, 0, atol=1e-7) and np.allclose(lat2, lat[vis], atol=1e-7)
35
+ # a point outside the disc is not on the planet
36
+ assert not screen_to_surface(np.array([2.0]), np.array([0.0]), 3.0, 120.0, a, b)[3][0]
37
+
38
+ # Map planes: round trips, and the area of a whole sphere and of a polar cap.
39
+ for frame in ('map_rect', 'map_north', 'map_south'):
40
+ lo = rng.uniform(0, 360, 200)
41
+ la = rng.uniform(45, 85, 200) * (-1 if frame == 'map_south' else 1) if frame != 'map_rect' else rng.uniform(-80, 80, 200)
42
+ u, v, ok = map_plane_coordinates(lo, la, frame, 180.0)
43
+ lo2, la2 = map_plane_to_surface(u, v, frame)
44
+ assert np.allclose(((lo2 - lo + 180) % 360) - 180, 0, atol=1e-7) and np.allclose(la2, la, atol=1e-7), frame
45
+ R = 71492.0
46
+ step = 0.5
47
+ cu = np.arange(0, 360, step) + step / 2
48
+ cv = np.arange(-90, 90, step) + step / 2
49
+ gu, gv = np.meshgrid(cu, cv)
50
+ area = map_bin_area(gu, gv, 'map_rect', step, R, R).sum()
51
+ assert abs(area / (4 * np.pi * R ** 2) - 1) < 1e-4, area
52
+ # north polar map out to 50 degrees of colatitude: a cap of area 2 pi R^2 (1 - cos 50 deg)
53
+ step = 0.25
54
+ c = np.arange(-50, 50, step) + step / 2
55
+ gu, gv = np.meshgrid(c, c)
56
+ inside = np.hypot(gu, gv) < 50.0
57
+ area = map_bin_area(gu[inside], gv[inside], 'map_north', step, R, R).sum()
58
+ cap = 2 * np.pi * R ** 2 * (1 - np.cos(np.radians(50.0)))
59
+ assert abs(area / cap - 1) < 2e-3, (area, cap)
60
+
61
+ # Orientation: Jupiter 4.5 AU away is 71492 km / 4.5 AU across, and System III turns 870.5366 deg per day.
62
+ n = 5
63
+ jd = 2460000.5 + np.arange(n) / 24.0
64
+ positions = np.column_stack([jd, np.full(n, 150.0), np.full(n, 15.0), np.full(n, 4.5)])
65
+ orient = planet_orientation(positions, jup, jd[0] - 2400000.5)
66
+ assert np.allclose(orient.radius_arcsec, 71492.0 / (4.5 * AU_KM) * 206264.80624709636)
67
+ rate = np.diff(orient.lon_obs) / np.diff(orient.t) # deg per second, west longitude
68
+ assert np.allclose(np.abs(rate) * 86400, 870.5366420, rtol=1e-3), rate * 86400
69
+ assert abs(LIGHT_TIME_PER_AU - 499.0048) < 1e-3
70
+ assert np.all(np.abs(orient.lat_obs_c) < 90) and np.all(np.abs(orient.lat_obs_g) < 90)
71
+ print('planet ok')
@@ -0,0 +1,38 @@
1
+ """Region shapes: which pixels they contain, and how they describe themselves."""
2
+ import support # noqa: F401
3
+ import numpy as np
4
+
5
+ from timetag_explorer.regions import Box, Circle, Polygon
6
+
7
+ x, y = np.meshgrid(np.arange(0, 10, dtype=float), np.arange(0, 10, dtype=float))
8
+
9
+ # Box: half-open, so adjacent boxes never count a pixel twice
10
+ box = Box(2, 5, 3, 6)
11
+ m = box.contains(x, y)
12
+ assert m.sum() == 9 and m[3:6, 2:5].all()
13
+ assert box.contains(np.array([2.0]), np.array([3.0]))[0] and not box.contains(np.array([5.0]), np.array([3.0]))[0]
14
+ assert Box(2, 5, 3, 6).describe() == 'box x 2-5, y 3-6'
15
+
16
+ # Circle: the boundary counts as inside
17
+ c = Circle(5, 5, 2)
18
+ assert c.contains(np.array([7.0]), np.array([5.0]))[0] and not c.contains(np.array([7.1]), np.array([5.0]))[0]
19
+ assert abs(c.contains(x, y).sum() - np.pi * 4) <= 3 # a pixel grid approximates pi r^2
20
+ assert Circle(5, 5, 2).describe().startswith('circle centre (5, 5), r = 2')
21
+
22
+ # Polygon: a square given as vertices, and a triangle
23
+ sq = Polygon(((1, 1), (4, 1), (4, 4), (1, 4)))
24
+ inside = sq.contains(np.array([2.5, 0.5, 5.0]), np.array([2.5, 2.5, 2.5]))
25
+ assert list(inside) == [True, False, False]
26
+ tri = Polygon(((0, 0), (8, 0), (0, 8)))
27
+ assert tri.contains(np.array([1.0]), np.array([1.0]))[0] and not tri.contains(np.array([7.0]), np.array([7.0]))[0]
28
+ assert not Polygon(((0, 0), (1, 1))).contains(x, y).any() # fewer than three vertices contain nothing
29
+ assert 'free-form, 4 vertices' in sq.describe()
30
+
31
+ # hashable (they are used in cache keys) and immutable
32
+ assert len({Box(0, 1, 0, 1), Box(0, 1, 0, 1), Circle(0, 0, 1)}) == 2
33
+ try:
34
+ box.x0 = 9
35
+ raise SystemExit('a Box must be immutable')
36
+ except AttributeError:
37
+ pass
38
+ print('regions ok')
@@ -0,0 +1,36 @@
1
+ """The selection-script generator: the script it writes is valid Python, carries its version, and says what it does.
2
+ (Running the scripts on real data is done by validation/check_selection_scripts.py.)"""
3
+ import support # noqa: F401
4
+ import numpy as np
5
+ from support import OFF, dataset
6
+
7
+ import timetag_explorer as tte
8
+ from timetag_explorer.frames import Corrections
9
+ from timetag_explorer.regions import Box, Circle, Polygon
10
+
11
+ n = 200
12
+ ds = dataset(np.linspace(0, 99, n), np.full(n, 600), np.full(n, 700))
13
+ cases = [((10.0, 20.0), None, 'image', OFF),
14
+ (None, Box(500, 700, 600, 800), 'image', OFF),
15
+ ((10.0, 20.0), Circle(600, 700, 50), 'rest', Corrections(randcorr=True, geocorr=False, timecorr=True)),
16
+ (None, Polygon(((500, 500), (700, 500), (600, 700))), 'image', Corrections(randcorr=False, geocorr=True))]
17
+ for time_sel, region, frame, corr in cases:
18
+ try:
19
+ s = tte.build_selection_script(ds, time_sel, region, frame, corr)
20
+ except ValueError as exc: # a rest frame needs the SPT file's pointing information
21
+ assert frame != 'image', exc
22
+ continue
23
+ assert not s.warnings or frame != 'image', s.warnings
24
+ compile(s.code, s.filename, 'exec') # valid Python
25
+ assert tte.SELECTION_SCRIPT_VERSION in s.code and 'SELVER' in s.code
26
+ assert s.filename.endswith('.py') and ds.rootname in s.filename
27
+ assert isinstance(s.packages, (list, tuple)) and s.notes
28
+ assert f"SCRIPT_VERSION = '{tte.SELECTION_SCRIPT_VERSION}'" in s.code
29
+ if time_sel is not None:
30
+ assert f'TIME_WINDOW = ({time_sel[0]}, {time_sel[1]})' in s.code
31
+ if isinstance(region, Box):
32
+ assert f'X0, X1 = {float(region.x0)}, {float(region.x1)}' in s.code
33
+ if region is not None:
34
+ assert region.describe() in s.code # the region is written out in words, too
35
+ assert tte.SELECTION_SCRIPT_VERSION == '1.0'
36
+ print('selection script ok')
@@ -0,0 +1,21 @@
1
+ """TIME-TAG Explorer science code: the calculations behind the web app for HST/STIS FUV TIME-TAG data.
2
+
3
+ This package has no web-server code. It reads a TIME-TAG (and SPT) file, applies the processing steps, transforms
4
+ positions between reference frames and planet maps, and writes the standalone selection scripts.
5
+
6
+ from timetag_explorer import TagDataset, Corrections, Box
7
+ """
8
+ from __future__ import annotations
9
+
10
+ __version__ = '1.0.1'
11
+
12
+ from .analysis import describe_selection
13
+ from .constants import SELECTION_SCRIPT_VERSION
14
+ from .dataset import TagDataset, load_dataset
15
+ from .frames import DEFAULT_CORRECTIONS, FRAMES, Corrections, FrameInfo
16
+ from .regions import Box, Circle, Polygon, Region
17
+ from .selection_script import SelectionScript, build_selection_script
18
+
19
+ __all__ = ['__version__', 'SELECTION_SCRIPT_VERSION', 'TagDataset', 'load_dataset', 'describe_selection',
20
+ 'Corrections', 'DEFAULT_CORRECTIONS', 'FRAMES', 'FrameInfo', 'Box', 'Circle', 'Polygon', 'Region',
21
+ 'SelectionScript', 'build_selection_script']