space-ska 1.0__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.
space_ska-1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Benoit Carry
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
space_ska-1.0/PKG-INFO ADDED
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.1
2
+ Name: space-ska
3
+ Version: 1.0
4
+ Summary: Spectral-Kit for Asteroids
5
+ License: MIT
6
+ Author: Benoit Carry
7
+ Author-email: benoit.carry@oca.eu
8
+ Requires-Python: >=3.12,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Provides-Extra: docs
13
+ Requires-Dist: astropy (>=6.0.1,<7.0.0)
14
+ Requires-Dist: click (>=8.1.7,<9.0.0)
15
+ Requires-Dist: numpy (>=1.26.4,<2.0.0)
16
+ Requires-Dist: pandas (>=2.2.1,<3.0.0)
17
+ Requires-Dist: requests (>=2.31.0,<3.0.0)
18
+ Requires-Dist: rich (>=13.7.1,<14.0.0)
19
+ Description-Content-Type: text/markdown
20
+
21
+ # ska - Spectral-Kit for Asteroids
22
+
23
+ Suites of tools to compute colors from spectra or reflectance spectra using the
24
+ [SVO Filter Service](http://svo2.cab.inta-csic.es/theory/fps/index.php?mode=voservice)
25
+ in different photometric system: Vega, AB, ST.
26
+
27
+
@@ -0,0 +1,6 @@
1
+ # ska - Spectral-Kit for Asteroids
2
+
3
+ Suites of tools to compute colors from spectra or reflectance spectra using the
4
+ [SVO Filter Service](http://svo2.cab.inta-csic.es/theory/fps/index.php?mode=voservice)
5
+ in different photometric system: Vega, AB, ST.
6
+
@@ -0,0 +1,35 @@
1
+ [tool.poetry]
2
+ name = "space-ska"
3
+ version = "1.0"
4
+ description = "Spectral-Kit for Asteroids"
5
+ authors = ["Benoit Carry <benoit.carry@oca.eu>", "Max Mahlke"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ packages = [{'include' = 'ska'}]
9
+
10
+ [tool.poetry.dependencies]
11
+ python = "^3.12"
12
+ requests = "^2.31.0"
13
+ pandas = "^2.2.1"
14
+ click = "^8.1.7"
15
+ rich = "^13.7.1"
16
+ astropy = "^6.0.1"
17
+ numpy = "^1.26.4"
18
+
19
+
20
+ [tool.poetry.extras]
21
+ docs = [
22
+ "furo",
23
+ "sphinx",
24
+ "sphinx-copybutton",
25
+ "sphinx-hoverxref",
26
+ "sphinx-redactor-theme",
27
+ "spinx_design"
28
+ ]
29
+
30
+ [tool.poetry.scripts]
31
+ ska = "ska.cli:cli_ska"
32
+
33
+ [build-system]
34
+ requires = ["poetry-core"]
35
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,25 @@
1
+ """Spectral-Kit for Asteroids."""
2
+
3
+ import os
4
+
5
+ from .filter import Filter # noqa
6
+ from .spectrum import Spectrum # noqa
7
+ from . import svo # noqa
8
+ from .cache import download_sun_vega # noqa
9
+
10
+ __version__ = "1.0.0"
11
+
12
+ # Cache location
13
+ PATH_CACHE = os.path.join(os.path.expanduser("~"), ".cache/ska")
14
+ os.makedirs(PATH_CACHE, exist_ok=True)
15
+
16
+ # SKA Auxiliary data
17
+ PATH_VEGA = os.path.join(PATH_CACHE, "spectrum_vega.csv")
18
+ PATH_SUN = os.path.join(PATH_CACHE, "spectrum_sun.csv")
19
+ PATH_FILTER_LIST = os.path.join(PATH_CACHE, "svo_filters.txt")
20
+
21
+ if not os.path.isfile(PATH_FILTER_LIST):
22
+ svo.download_filter_list()
23
+
24
+ if not os.path.isfile(PATH_VEGA) or not os.path.isfile(PATH_SUN):
25
+ download_sun_vega()
@@ -0,0 +1,88 @@
1
+ """Cache management for ska"""
2
+
3
+ import os
4
+ import glob
5
+ import requests
6
+
7
+ import ska
8
+
9
+
10
+ # ------
11
+ # Functions for cache management
12
+ def clear():
13
+ """Remove the cached filters and the acceptable list"""
14
+
15
+ filter_ids, filter_files = take_inventory()
16
+
17
+ # Remove cached filters
18
+ for f in filter_files:
19
+ os.unlink(os.path.join(ska.PATH_CACHE, f))
20
+
21
+ # Remove list of SVO Filters
22
+ os.unlink(os.path.join(ska.PATH_CACHE, "svo_filters.txt"))
23
+
24
+
25
+ def take_inventory():
26
+ """Create lists of the cached filter VOTables.
27
+
28
+ Returns
29
+ -------
30
+ list of str
31
+ The path to the cached VOTables.
32
+ """
33
+
34
+ # Get all XML in cache
35
+ cached_xmls = set(
36
+ file_ for file_ in glob.glob(os.path.join(ska.PATH_CACHE, "*.xml"))
37
+ )
38
+ cached_ids = set(
39
+ os.path.basename(FILT).replace("_", "/")[:-4] for FILT in cached_xmls
40
+ )
41
+
42
+ return cached_ids, cached_xmls
43
+
44
+
45
+ def update_filter_list():
46
+ # TBD doc / handle issue
47
+ ska.svo.download_filter_list()
48
+
49
+
50
+ def update_filters(ids, force=False):
51
+ """Update the cached filters (VOTable files).
52
+
53
+ Parameters
54
+ ----------
55
+ ids : list
56
+ List of SVO IDs corresponding to the filters to update.
57
+ """
58
+
59
+ # Download filters
60
+ for f in ids:
61
+ ska.svo.download_filter(f, force=force)
62
+
63
+
64
+ # Get Vega and Sun
65
+ def download_sun_vega():
66
+ """Download the spectra of the Sun and Vega"""
67
+
68
+ try:
69
+
70
+ # Get Solar Spectrum
71
+ r = requests.get(
72
+ "https://raw.githubusercontent.com/bcarry/ska/main/data/hst_sun.csv"
73
+ )
74
+ with open(ska.PATH_SUN, "w") as file:
75
+ file.write(r.text)
76
+
77
+ # Get Vega Spectrum
78
+ r = requests.get(
79
+ "https://raw.githubusercontent.com/bcarry/ska/main/data/lte096-4.0-0.5a%2B0.0.BT-NextGen.7.dat.csv"
80
+ )
81
+
82
+ with open(ska.PATH_VEGA, "w") as file:
83
+ file.write(r.text)
84
+
85
+ return True
86
+
87
+ except:
88
+ return False
@@ -0,0 +1,214 @@
1
+ import os
2
+ import sys
3
+
4
+ import click
5
+ import rich
6
+
7
+ import ska
8
+
9
+
10
+ @click.group()
11
+ @click.version_option(version=ska.__version__, message="%(version)s")
12
+ def cli_ska():
13
+ """CLI for Spectral-Kit for Asteroids."""
14
+ pass
15
+
16
+
17
+ # --------------------------------------------------------------------------------
18
+ # Status
19
+ @cli_ska.command()
20
+ @click.option(
21
+ "--clear",
22
+ "-c",
23
+ help="Clear cached filters and update the filter list.",
24
+ is_flag=True,
25
+ )
26
+ @click.option(
27
+ "--update", "-u", help="Update cached filters and filter list.", is_flag=True
28
+ )
29
+ def status(clear, update):
30
+ """Echo the status of the cached filters."""
31
+ from rich import prompt
32
+
33
+ from ska import cache
34
+
35
+ # ------
36
+ # Check filter list
37
+ if not os.path.isfile(ska.PATH_FILTER_LIST):
38
+ ska.svo.download_filter_list()
39
+
40
+ # ------
41
+ # Echo inventory
42
+ cached_ids, cached_xmls = cache.take_inventory()
43
+
44
+ rich.print(
45
+ f"""\nContents of {ska.PATH_CACHE}:
46
+
47
+ {len(cached_xmls)} filters\n"""
48
+ )
49
+
50
+ # Update or clear
51
+ if cached_xmls:
52
+ if not clear and not update:
53
+ decision = prompt.Prompt.ask(
54
+ "Update or clear the cached filters and filter list?\n"
55
+ "[blue][0][/blue] No "
56
+ "[blue][1][/blue] Clear cache "
57
+ "[blue][2][/blue] Update data ",
58
+ choices=["0", "1", "2"],
59
+ show_choices=False,
60
+ default="0",
61
+ )
62
+ else:
63
+ decision = "none"
64
+
65
+ if clear or decision == "1":
66
+ rich.print("\nClearing the cached filters and filter list..")
67
+ cache.clear()
68
+
69
+ elif update or decision == "2":
70
+ rich.print(cached_ids)
71
+ rich.print("\nDownload filters from SVO Filter Service..")
72
+ cache.update_filters(cached_ids, force=True)
73
+
74
+
75
+ # --------------------------------------------------------------------------------
76
+ # Fuzzy search among filters ID
77
+ @cli_ska.command()
78
+ def id():
79
+ """Fuzzy-search SVO filter index."""
80
+
81
+ import shutil
82
+ import subprocess
83
+
84
+ PATH_EXECUTABLE = shutil.which("fzf")
85
+
86
+ if PATH_EXECUTABLE is None:
87
+ rich.print(
88
+ "Interactive selection is not possible as the fzf tool is not installed.\n"
89
+ )
90
+ sys.exit()
91
+
92
+ FZF_OPTIONS = []
93
+
94
+ # Open fzf subprocess
95
+ process = subprocess.Popen(
96
+ [shutil.which("fzf"), *FZF_OPTIONS],
97
+ stdin=subprocess.PIPE,
98
+ stdout=subprocess.PIPE,
99
+ stderr=None,
100
+ )
101
+
102
+ FILTERS = ska.svo.load_filter_list()
103
+ for filter in FILTERS:
104
+ line = filter.encode(sys.getdefaultencoding()) + b"\n"
105
+ process.stdin.write(line)
106
+ process.stdin.flush()
107
+
108
+ # Run process and wait for user selection
109
+ process.stdin.close()
110
+ process.wait()
111
+
112
+ # Extract selected line
113
+ try:
114
+ choice = [line for line in process.stdout][0].decode()
115
+ filt = ska.Filter(choice.strip())
116
+ filt.display_summary()
117
+
118
+ except IndexError: # no choice was made, c-c c-c
119
+ sys.exit()
120
+ return choice
121
+
122
+
123
+ # --------------------------------------------------------------------------------
124
+ # Color computation
125
+ @cli_ska.command()
126
+ @click.argument("file")
127
+ @click.argument("filter1")
128
+ @click.argument("filter2")
129
+ @click.option(
130
+ "--phot_sys", default="Vega", help="Photometric system: Vega (default) | ST | AB"
131
+ )
132
+ @click.option(
133
+ "--reflectance",
134
+ "-r",
135
+ is_flag=True,
136
+ default=False,
137
+ help="Multiply the input reflectance by Solar spectrum.",
138
+ )
139
+ def color(file, filter1, filter2, phot_sys, reflectance):
140
+ """Compute the color between two filters"""
141
+
142
+ import pandas as pd
143
+
144
+ # TBD: interactive selection filters with fzf
145
+
146
+ # Load filters
147
+ f_1 = ska.Filter(filter1)
148
+ f_2 = ska.Filter(filter2)
149
+
150
+ # Read spectrum
151
+ spectrum = ska.Spectrum(file)
152
+
153
+ # Compute color
154
+ if reflectance:
155
+ color = spectrum.reflectance_to_color(f_1, f_2, phot_sys=phot_sys)
156
+ # color = skatools.reflectance_to_color(spectrum, f_1, f_2, phot_sys=phot_sys)
157
+ else:
158
+ color = spectrum.compute_color(f_1, f_2, phot_sys=phot_sys)
159
+ # color = skatools.compute_color(spectrum, f_1, f_2, phot_sys=phot_sys)
160
+ click.echo(f"{color:4.2f}")
161
+
162
+
163
+ # --------------------------------------------------------------------------------
164
+ # Solar Colors
165
+ @cli_ska.command()
166
+ @click.argument("filter1")
167
+ @click.argument("filter2")
168
+ @click.option(
169
+ "--phot_sys",
170
+ default="Vega",
171
+ help="Photometric system ([green]Vega[/green] | ST | AB)",
172
+ )
173
+ def solarcolor(filter1, filter2, phot_sys):
174
+ """Compute the color of the Sun between two filters"""
175
+
176
+ # Load filters
177
+ f_1 = ska.Filter(filter1)
178
+ f_2 = ska.Filter(filter2)
179
+
180
+ # Compute color
181
+ color = f_1.solar_color(f_2, phot_sys=phot_sys)
182
+ click.echo(f"{color:4.2f}")
183
+
184
+
185
+ # --------------------------------------------------------------------------------
186
+ # Filter basic information
187
+ @cli_ska.command()
188
+ @click.argument("filter")
189
+ def filter(filter):
190
+ """Display the basic properties of the filter"""
191
+
192
+ f = ska.Filter(filter)
193
+ f.display_summary()
194
+
195
+
196
+ # --------------------------------------------------------------------------------
197
+ # Plot filter transmission
198
+ @cli_ska.command()
199
+ @click.argument("filter")
200
+ @click.option("--figure", default=None, help="Name of the figure")
201
+ @click.option(
202
+ "--black", default=False, is_flag=True, help="Figure with a dark background"
203
+ )
204
+ def plot(filter, figure, black):
205
+ """Display the basic properties of the filter"""
206
+
207
+ f = ska.Filter(filter)
208
+
209
+ import matplotlib.pyplot as plt
210
+
211
+ fig, ax = f.plot_transmission(figure, black=black)
212
+
213
+ if not "figure" in locals():
214
+ plt.show()
@@ -0,0 +1,248 @@
1
+ import os
2
+ import sys
3
+ from astropy.io.votable import parse
4
+ import numpy as np
5
+ import pandas as pd
6
+
7
+ import rich
8
+
9
+ import ska
10
+
11
+
12
+ class Filter:
13
+ # --------------------------------------------------------------------------------
14
+ def __init__(self, id):
15
+ """Load a filter.
16
+
17
+ Parameters
18
+ ----------
19
+ id : str
20
+ The filter unique ID (see SVO filter service)
21
+ """
22
+
23
+ # Test validity of filters
24
+ FILTERS = ska.svo.load_filter_list()
25
+ if id not in FILTERS:
26
+ rich.print(
27
+ f"[red]Unknown filter ID {id}[/red]. Use [green]ska filter[/green] command to list available filters"
28
+ )
29
+ sys.exit(1)
30
+ # raise ValueError(f"Unknown filter ID {id}. Use [green]ska filter[/green] to list available filters")
31
+
32
+ self.id = id
33
+ self.path = os.path.join(ska.PATH_CACHE, f"{self.id.replace('/','_')}.xml")
34
+
35
+ # Download if not cached
36
+ if not os.path.isfile(self.path):
37
+ ska.svo.download_filter(self.id)
38
+
39
+ # Parse filter response
40
+ self.VOFilter = parse(self.path)
41
+ data = pd.DataFrame(data=self.VOFilter.get_first_table().array.data)
42
+
43
+ # Select non-zero transmission and convert to micron
44
+ data = data[data.Transmission >= 1e-5]
45
+ data.Wavelength /= 1e4 # to micron
46
+
47
+ # Store attributes
48
+ self.id = id
49
+ self.wave = data.Wavelength
50
+ self.trans = data.Transmission
51
+ self.central_wavelength = (
52
+ self.VOFilter.get_field_by_id("WavelengthCen").value / 1e4
53
+ )
54
+ self.FWHM = self.VOFilter.get_field_by_id("FWHM").value / 1e4
55
+ self.pivot_wavelength = (
56
+ self.VOFilter.get_field_by_id("WavelengthPivot").value / 1e4
57
+ )
58
+
59
+ try:
60
+ self.facility = self.VOFilter.get_field_by_id("Facility").value
61
+ except:
62
+ self.facility = None
63
+
64
+ try:
65
+ self.instrument = self.VOFilter.get_field_by_id("Instrument").value
66
+ except:
67
+ self.instrument = None
68
+
69
+ try:
70
+ self.band = self.VOFilter.get_field_by_id("Band").value
71
+ except:
72
+ self.band = None
73
+
74
+ # --------------------------------------------------------------------------------
75
+ def display_summary(self):
76
+ import rich
77
+
78
+ rich.print(f"\n[bright_cyan]Filter ID :[/bright_cyan] {self.id}")
79
+
80
+ if self.facility is not None:
81
+ rich.print(f"[bright_cyan]Facility :[/bright_cyan] {self.facility:s}")
82
+
83
+ if self.instrument is not None:
84
+ rich.print(f"[bright_cyan]Instrument:[/bright_cyan] {self.instrument:s}")
85
+
86
+ if self.band is not None:
87
+ rich.print(f"[bright_cyan]Band :[/bright_cyan] {self.band:s}")
88
+
89
+ rich.print(
90
+ f"[bright_cyan]Central λ :[/bright_cyan] [green]{self.central_wavelength:.3f}[/green] [bright_cyan](micron)[/bright_cyan]"
91
+ )
92
+ rich.print(
93
+ f"[bright_cyan]FWHM :[/bright_cyan] [green]{self.FWHM:.3f}[/green] [bright_cyan](micron)[/bright_cyan]"
94
+ )
95
+ rich.print(
96
+ f"[bright_cyan]Pivot λ :[/bright_cyan] [green]{self.pivot_wavelength:.3f}[/green] [bright_cyan](micron)[/bright_cyan]"
97
+ )
98
+
99
+ # --------------------------------------------------------------------------------
100
+ def compute_flux(self, spectrum):
101
+ """Computes the flux of a spectrum in a given band.
102
+
103
+ Parameters
104
+ ----------
105
+ spectrum : pd.DataFrame
106
+ Wavelength: in Angstrom
107
+ Flux: Flux density (erg/cm2/s/ang)
108
+
109
+ Returns
110
+ -------
111
+ float
112
+ The computed mean flux density
113
+ """
114
+
115
+ # Wavelength range to integrate over
116
+ lambda_int = np.arange(self.wave.min(), self.wave.max(), 0.0005)
117
+
118
+ # Detector type
119
+ # Photon counter
120
+ if self.VOFilter.get_field_by_id("DetectorType") == 1:
121
+ factor = lambda_int
122
+ # Energy counter
123
+ else:
124
+ factor = lambda_int * 0 + 1
125
+
126
+ # Interpolate over the transmission range
127
+ interpol_transmission = np.interp(lambda_int, self.wave, self.trans)
128
+
129
+ interpol_spectrum = np.interp(lambda_int, spectrum.Wavelength, spectrum.Flux)
130
+
131
+ # Compute the flux by integrating over wavelength.
132
+ nom = np.trapz(
133
+ interpol_spectrum * interpol_transmission * factor,
134
+ lambda_int,
135
+ )
136
+ denom = np.trapz(interpol_transmission * factor, lambda_int)
137
+ flux = nom / denom
138
+ return flux
139
+
140
+ # --------------------------------------------------------------------------------
141
+ def solar_color(self, filter, phot_sys="Vega", vega=None):
142
+ """Compute the color of the Sun between current and provided filter
143
+
144
+ Parameters
145
+ ==========
146
+ filter: str
147
+ The filter unique ID (see SVO filter service)
148
+
149
+ phot_sys : str
150
+ Photometric system in which to report the color (default=AB)
151
+
152
+ vega : ska.Spectrum
153
+
154
+ Returns
155
+ =======
156
+ float
157
+ The solar color
158
+ """
159
+ if not isinstance(filter, ska.Filter):
160
+ filter = ska.Filter(filter)
161
+
162
+ # Extract Solar Fluxes
163
+ sun_1 = self.VOFilter.get_field_by_id("Fsun").value
164
+ sun_2 = filter.VOFilter.get_field_by_id("Fsun").value
165
+
166
+ # Convert to magnitude
167
+ mag_1 = -2.5 * np.log10(sun_1)
168
+ mag_2 = -2.5 * np.log10(sun_2)
169
+ colorST = mag_1 - mag_2
170
+
171
+ # Solar color in ST photometric system
172
+ if phot_sys == "ST":
173
+ return colorST
174
+
175
+ # Solar color in Vega photometric system
176
+ elif phot_sys == "Vega":
177
+ # Read Vega spectrum if not provided
178
+ if not "vega" in locals():
179
+ vega = ska.Spectrum(ska.PATH_VEGA)
180
+ else:
181
+ if not isinstance(vega, ska.Spectrum):
182
+ vega = ska.Spectrum(ska.PATH_VEGA)
183
+
184
+ # Compute color of Vega
185
+ vega_ST = vega.compute_color(self, filter, phot_sys="ST")
186
+ return colorST - vega_ST
187
+
188
+ # Solar color in ST photometric system
189
+ else:
190
+ pivot_1 = self.VOFilter.get_field_by_id("WavelengthPivot").value
191
+ pivot_2 = filter.VOFilter.get_field_by_id("WavelengthPivot").value
192
+ return colorST - 5 * np.log10(pivot_1 / pivot_2)
193
+
194
+ # --------------------------------------------------------------------------------
195
+ def plot_transmission(self, figure=None, black=False):
196
+ """Create a plot of the transmission.
197
+
198
+ Parameters
199
+ ----------
200
+ figure : str
201
+ Path to save a figure
202
+
203
+ Returns
204
+ -------
205
+ figure, axe
206
+ Matplotlib figure and axe
207
+ """
208
+
209
+ # Define figure
210
+ import matplotlib.pyplot as plt
211
+
212
+ if black:
213
+ plt.style.use("dark_background")
214
+ else:
215
+ plt.style.use("default")
216
+ fig, ax = plt.subplots()
217
+
218
+ # Plot transmission
219
+ ax.plot(self.wave, self.trans, label=self.id)
220
+
221
+ # Central wavelength and FWHM
222
+ ax.axvline(
223
+ self.central_wavelength,
224
+ color="gray",
225
+ linestyle="--",
226
+ label=r"$\lambda_c$ = {:.2f} $\mu$m".format(self.central_wavelength),
227
+ )
228
+ ax.plot(
229
+ self.central_wavelength + self.FWHM / 2 * np.array([-1, 1]),
230
+ [self.trans.max() / 2, self.trans.max() / 2],
231
+ linestyle="dotted",
232
+ color="gray",
233
+ label=r"FWHM = {:.2f} $\mu$m".format(self.FWHM),
234
+ )
235
+
236
+ # Add labels
237
+ ax.set_xlabel("Wavelength (micron)")
238
+ ax.set_ylabel("Transmission")
239
+ ax.legend(loc="lower right")
240
+ ax.set_ylim(bottom=0)
241
+ fig.tight_layout()
242
+
243
+ # Save to file
244
+ if figure is not None:
245
+ # fig.savefig(figure, dpi=180, facecolor="w", edgecolor="w")
246
+ fig.savefig(figure, dpi=180)
247
+
248
+ return fig, ax
@@ -0,0 +1,243 @@
1
+ import os
2
+ import sys
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+
7
+ import rich
8
+ import ska
9
+
10
+
11
+ class Spectrum:
12
+ # --------------------------------------------------------------------------------
13
+ def __init__(self, input=None):
14
+ """ """
15
+
16
+ # Store attributes
17
+ self.Wavelength = None
18
+ self.Flux = None
19
+ self.Reflectance = False
20
+
21
+ if "input" in locals():
22
+
23
+ # Initialize from a file
24
+ if isinstance(input, str):
25
+ self.from_csv(input)
26
+
27
+ # Initialize from a numpy.ndarray
28
+ if isinstance(input, np.ndarray):
29
+ self.from_numpy(input)
30
+
31
+ # Initialize from a pandas.DataFrame
32
+ if isinstance(input, pd.DataFrame):
33
+ self.from_dataframe(input)
34
+
35
+ # --------------------------------------------------------------------------------
36
+ def copy(self):
37
+ from copy import deepcopy
38
+
39
+ return deepcopy(self)
40
+
41
+ # --------------------------------------------------------------------------------
42
+ def from_csv(self, file):
43
+ if not os.path.isfile(file):
44
+ rich.print(f"[red]Spectrum file {file} not found.[/red].")
45
+ sys.exit(1)
46
+
47
+ # Read spectrum
48
+ try:
49
+ spectrum = pd.read_csv(file)
50
+ except:
51
+ rich.print(f"[red]Cannot read spectrum file {file}.[/red].")
52
+ sys.exit(1)
53
+
54
+ self.from_dataframe(spectrum)
55
+
56
+ # --------------------------------------------------------------------------------
57
+ def from_numpy(self, arr, reflectance=False):
58
+ if not isinstance(arr, np.ndarray):
59
+ rich.print(f"[red]Input is not a numpy array.[/red]")
60
+ sys.exit(1)
61
+
62
+ if arr.shape[1] < 2:
63
+ rich.print(f"[red]Input array has less than 2 columns.[/red]")
64
+ sys.exit(1)
65
+
66
+ # Store attributes
67
+ order = np.argsort(arr[:, 0])
68
+ self.Wavelength = arr[order, 0]
69
+ self.Flux = arr[order, 1]
70
+ self.Reflectance = reflectance
71
+
72
+ # --------------------------------------------------------------------------------
73
+ def from_dataframe(self, df):
74
+
75
+ # Test Wavelength column
76
+ check_wave = True
77
+ if not "Wavelength" in df.columns:
78
+ check_wave = False
79
+ rich.print(f"[red]Column 'Wavelength' missing from input.[/red]")
80
+
81
+ # Test Flux or Reflectance column
82
+ check_flux = True
83
+ check_refl = True
84
+ if not "Flux" in df.columns:
85
+ check_flux = False
86
+ if not "Reflectance" in df.columns:
87
+ check_refl = False
88
+
89
+ if not (check_flux | check_refl):
90
+ rich.print(f"[red]Column 'Flux' or 'Reflectance' missing from input.[/red]")
91
+ sys.exit(1)
92
+
93
+ if not (check_wave & (check_flux | check_refl)):
94
+ sys.exit(1)
95
+
96
+ # Store attributes
97
+ order = np.argsort(df.Wavelength)
98
+ self.Wavelength = np.array(df.loc[order, "Wavelength"].values)
99
+ if check_flux:
100
+ self.Flux = np.array(df.loc[order, "Flux"].values)
101
+ if check_refl:
102
+ self.Flux = np.array(df.loc[order, "Reflectance"].values)
103
+ self.Reflectance = True
104
+
105
+ # --------------------------------------------------------------------------------
106
+ def compute_color(self, filter1, filter2, phot_sys="Vega", vega=None):
107
+ """Computes filter_1-filter_2 color of spectrum in the requested system.
108
+
109
+ Parameters
110
+ ==========
111
+ filter_1: ska.Filter
112
+
113
+ filter_2: ska.Filter
114
+
115
+ phot_sys : str
116
+ Photometric system in which to report the color (default=Vega)
117
+
118
+ vega : ska.Spectrum
119
+ The spectrum of Vega
120
+
121
+ Returns
122
+ =======
123
+ float
124
+ The requested color
125
+ """
126
+
127
+ # Compute fluxes in each filter
128
+ flux1 = filter1.compute_flux(self)
129
+ flux2 = filter2.compute_flux(self)
130
+
131
+ # Magnitude in AB photometric system
132
+ if phot_sys == "AB":
133
+ # Get Pivot wavelength for both filters
134
+ pivot_1 = filter1.VOFilter.get_field_by_id("WavelengthPivot").value
135
+ pivot_2 = filter2.VOFilter.get_field_by_id("WavelengthPivot").value
136
+
137
+ # Compute and return the color
138
+ return -2.5 * np.log10(flux1 / flux2) - 5 * np.log10(pivot_1 / pivot_2)
139
+
140
+ # Magnitude in Vega photometric system
141
+ elif phot_sys == "Vega":
142
+ # Read Vega spectrum if not provided
143
+ if vega is None:
144
+ vega = ska.Spectrum(ska.PATH_VEGA)
145
+
146
+ # Compute fluxes of Vega in each filter
147
+ flux1_vega = filter1.compute_flux(vega)
148
+ flux2_vega = filter2.compute_flux(vega)
149
+
150
+ # Compute and return the color
151
+ return -2.5 * (np.log10(flux1 / flux1_vega) - np.log10(flux2 / flux2_vega))
152
+
153
+ # Magnitude in ST photometric system
154
+ elif phot_sys == "ST":
155
+ return -2.5 * np.log10(flux1 / flux2)
156
+
157
+ # --------------------------------------------------------------------------------
158
+ def reflectance_to_flux(self, sun=None):
159
+ """Convert reflectance to flux by multiply by Solar spectrum.
160
+
161
+ Parameters
162
+ ==========
163
+ sun : ska.Spectrum
164
+ Spectrum of the Sun
165
+
166
+ Returns
167
+ =======
168
+ ska.Spectrum
169
+ A copy of the input Spectrum, in flux units
170
+ """
171
+
172
+ # Test if the input is a reflectance spectrum
173
+ if not self.Reflectance:
174
+ rich.print(f"[red]Input spectrum is not a reflectance spectrum.[/red]")
175
+
176
+ # Read spectrum of the Sun if not provided
177
+ if not isinstance(sun, ska.Spectrum):
178
+ sun = ska.Spectrum(ska.PATH_SUN)
179
+
180
+ # Interpolate spectrum of the Sun
181
+ interpol_spectrum = np.interp(self.Wavelength, sun.Wavelength, sun.Flux)
182
+
183
+ # Mulitply reflectance by Solar spectrum
184
+ spectrum = self.copy()
185
+ spectrum.Flux = self.Flux * interpol_spectrum
186
+ spectrum.Reflectance = False
187
+ return spectrum
188
+
189
+ # --------------------------------------------------------------------------------
190
+ def reflectance_to_color(
191
+ self, filter1, filter2, phot_sys="Vega", vega=None, sun=None
192
+ ):
193
+ """Computes filter_1-filter_2 color for a reflectance spectrum.
194
+
195
+ Parameters
196
+ ==========
197
+ filter_1: ska.Filter
198
+
199
+ filter_2: ska.Filter
200
+
201
+ phot_sys : str
202
+ Photometric system in which to report the color (default=Vega)
203
+
204
+ vega : ska.Spectrum
205
+ Spectrum of Vega
206
+
207
+ sun : ska.Spectrum
208
+ Spectrum of the Sun
209
+
210
+ Returns
211
+ =======
212
+ float
213
+ The requested color
214
+ """
215
+
216
+ # Integration grid is built from the transmission curve
217
+ lambda_min = np.min([filter1.wave.min(), filter2.wave.min()])
218
+ lambda_max = np.max([filter1.wave.max(), filter2.wave.max()])
219
+
220
+ # Wavelength range to integrate over
221
+ lambda_int = np.arange(lambda_min, lambda_max, 0.0005)
222
+
223
+ # Read spectrum of the Sun if not provided
224
+ if not isinstance(sun, ska.Spectrum):
225
+ sun = ska.Spectrum(ska.PATH_SUN)
226
+
227
+ # Interpolate spectrum of the Sun
228
+ interpol_spectrum = np.interp(lambda_int, sun.Wavelength, sun.Flux)
229
+ interp_sun = pd.DataFrame({"Wavelength": lambda_int, "Flux": interpol_spectrum})
230
+ interp_sun = interp_sun.astype("float")
231
+
232
+ # Interpolate reflectance spectrum
233
+ interpol_spectrum = np.interp(lambda_int, self.Wavelength, self.Flux)
234
+ interp_spectrum = ska.Spectrum(
235
+ pd.DataFrame(
236
+ {"Wavelength": lambda_int, "Flux": interpol_spectrum * interp_sun.Flux}
237
+ )
238
+ )
239
+
240
+ # Compute color of the reflectance*Sun spectrum
241
+ return interp_spectrum.compute_color(
242
+ filter1, filter2, phot_sys=phot_sys, vega=vega
243
+ )
@@ -0,0 +1,118 @@
1
+ import io
2
+ import os
3
+ import sys
4
+ import requests
5
+ from astropy.io.votable import parse
6
+ import rich
7
+
8
+ import ska
9
+
10
+
11
+ def download_filter_list():
12
+ """Retrieve the list of filter IDs from SVO Filter Service
13
+ http://svo2.cab.inta-csic.es/theory/fps/
14
+
15
+ Returns
16
+ =======
17
+ list
18
+ The list of filter IDS
19
+ """
20
+
21
+ try:
22
+
23
+ # Main SVO filter list
24
+ r = requests.get(
25
+ "https://svo.cab.inta-csic.es/files/svo/Public/HowTo/FPS/FPS_info.xml"
26
+ )
27
+ SVOFilters = parse(io.BytesIO(r.content))
28
+ main_id = SVOFilters.get_first_table().to_table().to_pandas().filterID.to_list()
29
+
30
+ # Secondary SVO filter list
31
+ r = requests.get(
32
+ "https://svo.cab.inta-csic.es/files/svo/Public/HowTo/FPS/others.xml"
33
+ )
34
+ SVOFilters = parse(io.BytesIO(r.content))
35
+ other_id = SVOFilters.get_first_table().to_table().to_pandas()["__ID"].to_list()
36
+
37
+ # Merge and Write to disk
38
+ filter_id = main_id + other_id
39
+ with open(ska.PATH_FILTER_LIST, "w") as file:
40
+ for f in filter_id:
41
+ file.write(f"{f}\n")
42
+ return True
43
+
44
+ except:
45
+ # raise Exception("Error downloading filter list")
46
+ rich.print(f"[red]Error downloading filter {id} VOTable[/red].")
47
+ return False
48
+
49
+
50
+ def load_filter_list():
51
+ """Read all filter IDs from a cache list
52
+
53
+ Returns
54
+ =======
55
+ list
56
+ The list of filter IDS
57
+
58
+ """
59
+ if not os.path.isfile(ska.PATH_FILTER_LIST):
60
+ download_filter_list()
61
+
62
+ with open(ska.PATH_FILTER_LIST, "r") as file:
63
+ FILTERS = [filt.strip() for filt in file]
64
+ return FILTERS
65
+
66
+
67
+ def download_filter(id, force=False):
68
+ """Download a filter VOTable from SVO Filter Service
69
+ http://svo2.cab.inta-csic.es/theory/fps/index.php?mode=voservice
70
+
71
+ Parameters
72
+ ==========
73
+ id : str
74
+ The unique SVO filter identifier to be downloaded
75
+
76
+ force : bool
77
+ If True, the filter VOTable will be downloaded even if it is already cached
78
+
79
+ Returns
80
+ =======
81
+ str
82
+ The path to the filter VOTable file
83
+ """
84
+
85
+ # Test if the filter ID is valid
86
+ FILTERS = load_filter_list()
87
+ if id not in FILTERS:
88
+ rich.print(
89
+ f"[red]Unknown filter ID {id}[/red]. Use [green]ska filter[/green] to list available filters"
90
+ )
91
+ sys.exit(1)
92
+ # raise ValueError(f"Unknown filter ID {id}. Use ska filter to list available filters")
93
+
94
+ # SVO Base URL for queries
95
+ url = f"http://svo2.cab.inta-csic.es/theory/fps3/fps.php?"
96
+
97
+ # Output name for the filter VOTable
98
+ out = os.path.join(ska.PATH_CACHE, id.replace("/", "_") + ".xml")
99
+
100
+ # Download VOTable
101
+ if (not os.path.isfile(out)) or force:
102
+ try:
103
+ # Request the filter VOTable
104
+ r = requests.get(url, params={"ID": id})
105
+ SVOFilter = parse(io.BytesIO(r.content))
106
+ filter_info = SVOFilter.get_first_table()
107
+
108
+ # Write it to disk
109
+ # os.makedirs(ska.PATH_CACHE, exist_ok=True)
110
+ SVOFilter.to_xml(out)
111
+
112
+ except:
113
+ rich.print(f"[red]Error downloading filter {id} VOTable[/red].")
114
+ sys.exit(1)
115
+ # raise Exception("Error downloading filter VOTable")
116
+
117
+ # Return path to filter VOTable
118
+ return out