pyreduce-astro 0.7a4__cp314-cp314-win_amd64.whl
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.
- pyreduce/__init__.py +67 -0
- pyreduce/__main__.py +322 -0
- pyreduce/cli.py +342 -0
- pyreduce/clib/Release/_slitfunc_2d.cp311-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp311-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp312-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp312-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp313-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp313-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp314-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp314-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_2d.obj +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp311-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp311-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp312-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp312-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp313-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp313-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp314-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp314-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_bd.obj +0 -0
- pyreduce/clib/__init__.py +0 -0
- pyreduce/clib/_slitfunc_2d.cp311-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_2d.cp312-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_2d.cp313-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_2d.cp314-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_bd.cp311-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_bd.cp312-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_bd.cp313-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_bd.cp314-win_amd64.pyd +0 -0
- pyreduce/clib/build_extract.py +75 -0
- pyreduce/clib/slit_func_2d_xi_zeta_bd.c +1313 -0
- pyreduce/clib/slit_func_2d_xi_zeta_bd.h +55 -0
- pyreduce/clib/slit_func_bd.c +362 -0
- pyreduce/clib/slit_func_bd.h +17 -0
- pyreduce/clipnflip.py +147 -0
- pyreduce/combine_frames.py +861 -0
- pyreduce/configuration.py +191 -0
- pyreduce/continuum_normalization.py +329 -0
- pyreduce/cwrappers.py +404 -0
- pyreduce/datasets.py +238 -0
- pyreduce/echelle.py +413 -0
- pyreduce/estimate_background_scatter.py +130 -0
- pyreduce/extract.py +1362 -0
- pyreduce/extraction_width.py +77 -0
- pyreduce/instruments/__init__.py +0 -0
- pyreduce/instruments/aj.py +9 -0
- pyreduce/instruments/aj.yaml +51 -0
- pyreduce/instruments/andes.py +102 -0
- pyreduce/instruments/andes.yaml +72 -0
- pyreduce/instruments/common.py +711 -0
- pyreduce/instruments/common.yaml +57 -0
- pyreduce/instruments/crires_plus.py +103 -0
- pyreduce/instruments/crires_plus.yaml +101 -0
- pyreduce/instruments/filters.py +195 -0
- pyreduce/instruments/harpn.py +203 -0
- pyreduce/instruments/harpn.yaml +140 -0
- pyreduce/instruments/harps.py +312 -0
- pyreduce/instruments/harps.yaml +144 -0
- pyreduce/instruments/instrument_info.py +140 -0
- pyreduce/instruments/jwst_miri.py +29 -0
- pyreduce/instruments/jwst_miri.yaml +53 -0
- pyreduce/instruments/jwst_niriss.py +98 -0
- pyreduce/instruments/jwst_niriss.yaml +60 -0
- pyreduce/instruments/lick_apf.py +35 -0
- pyreduce/instruments/lick_apf.yaml +60 -0
- pyreduce/instruments/mcdonald.py +123 -0
- pyreduce/instruments/mcdonald.yaml +56 -0
- pyreduce/instruments/metis_ifu.py +45 -0
- pyreduce/instruments/metis_ifu.yaml +62 -0
- pyreduce/instruments/metis_lss.py +45 -0
- pyreduce/instruments/metis_lss.yaml +62 -0
- pyreduce/instruments/micado.py +45 -0
- pyreduce/instruments/micado.yaml +62 -0
- pyreduce/instruments/models.py +257 -0
- pyreduce/instruments/neid.py +156 -0
- pyreduce/instruments/neid.yaml +61 -0
- pyreduce/instruments/nirspec.py +215 -0
- pyreduce/instruments/nirspec.yaml +63 -0
- pyreduce/instruments/nte.py +42 -0
- pyreduce/instruments/nte.yaml +55 -0
- pyreduce/instruments/uves.py +46 -0
- pyreduce/instruments/uves.yaml +65 -0
- pyreduce/instruments/xshooter.py +39 -0
- pyreduce/instruments/xshooter.yaml +63 -0
- pyreduce/make_shear.py +607 -0
- pyreduce/masks/mask_crires_plus_det1.fits.gz +0 -0
- pyreduce/masks/mask_crires_plus_det2.fits.gz +0 -0
- pyreduce/masks/mask_crires_plus_det3.fits.gz +0 -0
- pyreduce/masks/mask_ctio_chiron.fits.gz +0 -0
- pyreduce/masks/mask_elodie.fits.gz +0 -0
- pyreduce/masks/mask_feros3.fits.gz +0 -0
- pyreduce/masks/mask_flames_giraffe.fits.gz +0 -0
- pyreduce/masks/mask_harps_blue.fits.gz +0 -0
- pyreduce/masks/mask_harps_red.fits.gz +0 -0
- pyreduce/masks/mask_hds_blue.fits.gz +0 -0
- pyreduce/masks/mask_hds_red.fits.gz +0 -0
- pyreduce/masks/mask_het_hrs_2x5.fits.gz +0 -0
- pyreduce/masks/mask_jwst_miri_lrs_slitless.fits.gz +0 -0
- pyreduce/masks/mask_jwst_niriss_gr700xd.fits.gz +0 -0
- pyreduce/masks/mask_lick_apf_.fits.gz +0 -0
- pyreduce/masks/mask_mcdonald.fits.gz +0 -0
- pyreduce/masks/mask_nes.fits.gz +0 -0
- pyreduce/masks/mask_nirspec_nirspec.fits.gz +0 -0
- pyreduce/masks/mask_sarg.fits.gz +0 -0
- pyreduce/masks/mask_sarg_2x2a.fits.gz +0 -0
- pyreduce/masks/mask_sarg_2x2b.fits.gz +0 -0
- pyreduce/masks/mask_subaru_hds_red.fits.gz +0 -0
- pyreduce/masks/mask_uves_blue.fits.gz +0 -0
- pyreduce/masks/mask_uves_blue_binned_2_2.fits.gz +0 -0
- pyreduce/masks/mask_uves_middle.fits.gz +0 -0
- pyreduce/masks/mask_uves_middle_2x2_split.fits.gz +0 -0
- pyreduce/masks/mask_uves_middle_binned_2_2.fits.gz +0 -0
- pyreduce/masks/mask_uves_red.fits.gz +0 -0
- pyreduce/masks/mask_uves_red_2x2.fits.gz +0 -0
- pyreduce/masks/mask_uves_red_2x2_split.fits.gz +0 -0
- pyreduce/masks/mask_uves_red_binned_2_2.fits.gz +0 -0
- pyreduce/masks/mask_xshooter_nir.fits.gz +0 -0
- pyreduce/pipeline.py +619 -0
- pyreduce/rectify.py +138 -0
- pyreduce/reduce.py +2065 -0
- pyreduce/settings/settings_AJ.json +19 -0
- pyreduce/settings/settings_ANDES.json +89 -0
- pyreduce/settings/settings_CRIRES_PLUS.json +89 -0
- pyreduce/settings/settings_HARPN.json +73 -0
- pyreduce/settings/settings_HARPS.json +69 -0
- pyreduce/settings/settings_JWST_MIRI.json +55 -0
- pyreduce/settings/settings_JWST_NIRISS.json +55 -0
- pyreduce/settings/settings_LICK_APF.json +62 -0
- pyreduce/settings/settings_MCDONALD.json +58 -0
- pyreduce/settings/settings_METIS_IFU.json +77 -0
- pyreduce/settings/settings_METIS_LSS.json +77 -0
- pyreduce/settings/settings_MICADO.json +78 -0
- pyreduce/settings/settings_NEID.json +73 -0
- pyreduce/settings/settings_NIRSPEC.json +58 -0
- pyreduce/settings/settings_NTE.json +60 -0
- pyreduce/settings/settings_UVES.json +54 -0
- pyreduce/settings/settings_XSHOOTER.json +78 -0
- pyreduce/settings/settings_pyreduce.json +184 -0
- pyreduce/settings/settings_schema.json +850 -0
- pyreduce/tools/__init__.py +0 -0
- pyreduce/tools/combine.py +117 -0
- pyreduce/trace.py +979 -0
- pyreduce/util.py +1366 -0
- pyreduce/wavecal/MICADO_HK_3arcsec_chip5.npz +0 -0
- pyreduce/wavecal/atlas/thar.fits +4946 -13
- pyreduce/wavecal/atlas/thar_list.txt +4172 -0
- pyreduce/wavecal/atlas/une.fits +0 -0
- pyreduce/wavecal/convert.py +38 -0
- pyreduce/wavecal/crires_plus_J1228_Open_det1.npz +0 -0
- pyreduce/wavecal/crires_plus_J1228_Open_det2.npz +0 -0
- pyreduce/wavecal/crires_plus_J1228_Open_det3.npz +0 -0
- pyreduce/wavecal/harpn_harpn_2D.npz +0 -0
- pyreduce/wavecal/harps_blue_2D.npz +0 -0
- pyreduce/wavecal/harps_blue_pol_2D.npz +0 -0
- pyreduce/wavecal/harps_red_2D.npz +0 -0
- pyreduce/wavecal/harps_red_pol_2D.npz +0 -0
- pyreduce/wavecal/mcdonald.npz +0 -0
- pyreduce/wavecal/metis_lss_l_2D.npz +0 -0
- pyreduce/wavecal/metis_lss_m_2D.npz +0 -0
- pyreduce/wavecal/nirspec_K2.npz +0 -0
- pyreduce/wavecal/uves_blue_360nm_2D.npz +0 -0
- pyreduce/wavecal/uves_blue_390nm_2D.npz +0 -0
- pyreduce/wavecal/uves_blue_437nm_2D.npz +0 -0
- pyreduce/wavecal/uves_middle_2x2_2D.npz +0 -0
- pyreduce/wavecal/uves_middle_565nm_2D.npz +0 -0
- pyreduce/wavecal/uves_middle_580nm_2D.npz +0 -0
- pyreduce/wavecal/uves_middle_600nm_2D.npz +0 -0
- pyreduce/wavecal/uves_middle_665nm_2D.npz +0 -0
- pyreduce/wavecal/uves_middle_860nm_2D.npz +0 -0
- pyreduce/wavecal/uves_red_580nm_2D.npz +0 -0
- pyreduce/wavecal/uves_red_600nm_2D.npz +0 -0
- pyreduce/wavecal/uves_red_665nm_2D.npz +0 -0
- pyreduce/wavecal/uves_red_760nm_2D.npz +0 -0
- pyreduce/wavecal/uves_red_860nm_2D.npz +0 -0
- pyreduce/wavecal/xshooter_nir.npz +0 -0
- pyreduce/wavelength_calibration.py +1871 -0
- pyreduce_astro-0.7a4.dist-info/METADATA +106 -0
- pyreduce_astro-0.7a4.dist-info/RECORD +182 -0
- pyreduce_astro-0.7a4.dist-info/WHEEL +4 -0
- pyreduce_astro-0.7a4.dist-info/entry_points.txt +2 -0
- pyreduce_astro-0.7a4.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Loads configuration files
|
|
2
|
+
|
|
3
|
+
This module loads json configuration files from disk,
|
|
4
|
+
and combines them with the default settings,
|
|
5
|
+
to create one dict that contains all parameters.
|
|
6
|
+
It also checks that all parameters exists, and that
|
|
7
|
+
no new parameters have been added by accident.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
from os.path import dirname, join
|
|
13
|
+
|
|
14
|
+
import jsonschema
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
if int(jsonschema.__version__[0]) < 3: # pragma: no cover
|
|
19
|
+
logger.warning(
|
|
20
|
+
"Jsonschema %s found, but at least 3.0.0 is required to check configuration. Skipping the check.",
|
|
21
|
+
jsonschema.__version__,
|
|
22
|
+
)
|
|
23
|
+
hasJsonSchema = False
|
|
24
|
+
else:
|
|
25
|
+
hasJsonSchema = True
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_configuration_for_instrument(instrument, **kwargs):
|
|
29
|
+
local = dirname(__file__)
|
|
30
|
+
instrument = str(instrument)
|
|
31
|
+
if instrument in ["pyreduce", None]:
|
|
32
|
+
fname = join(local, "settings", "settings_pyreduce.json")
|
|
33
|
+
else:
|
|
34
|
+
fname = join(local, "settings", f"settings_{instrument.upper()}.json")
|
|
35
|
+
|
|
36
|
+
config = load_config(fname, instrument)
|
|
37
|
+
|
|
38
|
+
for kwarg_key, kwarg_value in kwargs.items():
|
|
39
|
+
for key, _value in config.items():
|
|
40
|
+
if isinstance(config[key], dict) and kwarg_key in config[key].keys():
|
|
41
|
+
config[key][kwarg_key] = kwarg_value
|
|
42
|
+
|
|
43
|
+
return config
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load_config(configuration, instrument, j=0):
|
|
47
|
+
if configuration is None:
|
|
48
|
+
logger.info(
|
|
49
|
+
"No configuration specified, using default values for this instrument"
|
|
50
|
+
)
|
|
51
|
+
config = get_configuration_for_instrument(instrument, plot=False)
|
|
52
|
+
elif isinstance(configuration, dict):
|
|
53
|
+
if instrument in configuration.keys():
|
|
54
|
+
config = configuration[str(instrument)]
|
|
55
|
+
elif (
|
|
56
|
+
"__instrument__" in configuration.keys()
|
|
57
|
+
and configuration["__instrument__"] == str(instrument).upper()
|
|
58
|
+
):
|
|
59
|
+
config = configuration
|
|
60
|
+
else:
|
|
61
|
+
raise KeyError("This configuration is for a different instrument")
|
|
62
|
+
elif isinstance(configuration, list):
|
|
63
|
+
config = configuration[j]
|
|
64
|
+
elif isinstance(configuration, str):
|
|
65
|
+
config = configuration
|
|
66
|
+
|
|
67
|
+
if isinstance(config, str):
|
|
68
|
+
logger.info("Loading configuration from %s", config)
|
|
69
|
+
try:
|
|
70
|
+
with open(config) as f:
|
|
71
|
+
config = json.load(f)
|
|
72
|
+
except FileNotFoundError:
|
|
73
|
+
fname = dirname(__file__)
|
|
74
|
+
fname = join(fname, "settings", config)
|
|
75
|
+
with open(fname) as f:
|
|
76
|
+
config = json.load(f)
|
|
77
|
+
|
|
78
|
+
# Combine instrument specific settings, with default values
|
|
79
|
+
settings = read_config()
|
|
80
|
+
settings = update(settings, config)
|
|
81
|
+
|
|
82
|
+
# If it doesn't raise an Exception everything is as expected
|
|
83
|
+
validate_config(settings)
|
|
84
|
+
logger.debug("Configuration succesfully validated")
|
|
85
|
+
|
|
86
|
+
return settings
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def update(dict1, dict2, check=True, name="dict1"):
|
|
90
|
+
"""
|
|
91
|
+
Update entries in dict1 with entries of dict2 recursively,
|
|
92
|
+
i.e. if the dict contains a dict value, values inside the dict will
|
|
93
|
+
also be updated
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
dict1 : dict
|
|
98
|
+
dict that will be updated
|
|
99
|
+
dict2 : dict
|
|
100
|
+
dict that contains the values to update
|
|
101
|
+
check : bool
|
|
102
|
+
If True, will check that the keys from dict2 exist in dict1 already.
|
|
103
|
+
Except for those contained in field "instrument"
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
dict1 : dict
|
|
108
|
+
the updated dict
|
|
109
|
+
|
|
110
|
+
Raises
|
|
111
|
+
------
|
|
112
|
+
KeyError
|
|
113
|
+
If dict2 contains a key that is not in dict1
|
|
114
|
+
"""
|
|
115
|
+
# Instrument is a 'special' section as it may include any number of values
|
|
116
|
+
# In that case we don't want to raise an error for new keys
|
|
117
|
+
exclude = ["instrument"]
|
|
118
|
+
for key, value in dict2.items():
|
|
119
|
+
if check and key not in dict1.keys():
|
|
120
|
+
logger.warning(f"{key} is not contained in {name}")
|
|
121
|
+
if isinstance(value, dict):
|
|
122
|
+
if dict1.get(key) is None:
|
|
123
|
+
dict1[key] = value
|
|
124
|
+
else:
|
|
125
|
+
dict1[key] = update(
|
|
126
|
+
dict1[key], value, check=key not in exclude, name=key
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
dict1[key] = value
|
|
130
|
+
return dict1
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def read_config(fname="settings_pyreduce.json"):
|
|
134
|
+
"""Read the configuration file from disk
|
|
135
|
+
|
|
136
|
+
If no filename is given it will load the default configuration.
|
|
137
|
+
The configuration file must be a json file.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
fname : str, optional
|
|
142
|
+
Filename of the configuration. By default "settings_pyreduce.json",
|
|
143
|
+
i.e. the default configuration
|
|
144
|
+
|
|
145
|
+
Returns
|
|
146
|
+
-------
|
|
147
|
+
config : dict
|
|
148
|
+
The read configuration file
|
|
149
|
+
"""
|
|
150
|
+
this_dir = dirname(__file__)
|
|
151
|
+
fname = join(this_dir, "settings", fname)
|
|
152
|
+
|
|
153
|
+
with open(fname) as file:
|
|
154
|
+
settings = json.load(file)
|
|
155
|
+
return settings
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def validate_config(config):
|
|
159
|
+
"""Test that the input configuration complies with the expected schema
|
|
160
|
+
|
|
161
|
+
Since it requires features from jsonschema 3+, it will only run if that is installed.
|
|
162
|
+
Otherwise show a warning but continue. This is incase some other module needs an earlier,
|
|
163
|
+
jsonschema (looking at you jwst).
|
|
164
|
+
|
|
165
|
+
If the function runs through without raising an exception, the check was succesful or skipped.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
config : dict
|
|
170
|
+
Configurations to check
|
|
171
|
+
|
|
172
|
+
Raises
|
|
173
|
+
------
|
|
174
|
+
ValueError
|
|
175
|
+
If there is a problem with the configuration.
|
|
176
|
+
Usually that means a setting has an unallowed value.
|
|
177
|
+
"""
|
|
178
|
+
if not hasJsonSchema: # pragma: no cover
|
|
179
|
+
# Can't check with old version
|
|
180
|
+
return
|
|
181
|
+
fname = "settings_schema.json"
|
|
182
|
+
this_dir = dirname(__file__)
|
|
183
|
+
fname = join(this_dir, "settings", fname)
|
|
184
|
+
|
|
185
|
+
with open(fname) as f:
|
|
186
|
+
schema = json.load(f)
|
|
187
|
+
try:
|
|
188
|
+
jsonschema.validate(schema=schema, instance=config)
|
|
189
|
+
except jsonschema.ValidationError as ve:
|
|
190
|
+
logger.error("Configuration failed validation check.\n%s", ve.message)
|
|
191
|
+
raise ValueError(ve.message) from ve
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Find the continuum level
|
|
3
|
+
|
|
4
|
+
Currently only splices orders together
|
|
5
|
+
First guess of the continuum is provided by the flat field
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from itertools import chain
|
|
10
|
+
|
|
11
|
+
import matplotlib.pyplot as plt
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from . import util
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# np.seterr("raise")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def splice_orders(spec, wave, cont, sigm, scaling=True, plot=False, plot_title=None):
|
|
22
|
+
"""
|
|
23
|
+
Splice orders together so that they form a continous spectrum
|
|
24
|
+
This is achieved by linearly combining the overlaping regions
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
spec : array[nord, ncol]
|
|
29
|
+
Spectrum to splice, with seperate orders
|
|
30
|
+
wave : array[nord, ncol]
|
|
31
|
+
Wavelength solution for each point
|
|
32
|
+
cont : array[nord, ncol]
|
|
33
|
+
Continuum, blaze function will do fine as well
|
|
34
|
+
sigm : array[nord, ncol]
|
|
35
|
+
Errors on the spectrum
|
|
36
|
+
scaling : bool, optional
|
|
37
|
+
If true, the spectrum/continuum will be scaled to 1 (default: False)
|
|
38
|
+
plot : bool, optional
|
|
39
|
+
If true, will plot the spliced spectrum (default: False)
|
|
40
|
+
|
|
41
|
+
Raises
|
|
42
|
+
------
|
|
43
|
+
NotImplementedError
|
|
44
|
+
If neighbouring orders dont overlap
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
spec, wave, cont, sigm : array[nord, ncol]
|
|
49
|
+
spliced spectrum
|
|
50
|
+
"""
|
|
51
|
+
nord, _ = spec.shape # Number of sp. orders, Order length in pixels
|
|
52
|
+
|
|
53
|
+
if cont is None:
|
|
54
|
+
cont = np.ones_like(spec)
|
|
55
|
+
|
|
56
|
+
# Just to be extra safe that they are all the same
|
|
57
|
+
mask = (
|
|
58
|
+
np.ma.getmaskarray(spec)
|
|
59
|
+
| (np.ma.getdata(spec) == 0)
|
|
60
|
+
| (np.ma.getdata(cont) == 0)
|
|
61
|
+
)
|
|
62
|
+
spec = np.ma.masked_array(spec, mask=mask)
|
|
63
|
+
wave = np.ma.masked_array(np.ma.getdata(wave), mask=mask)
|
|
64
|
+
cont = np.ma.masked_array(np.ma.getdata(cont), mask=mask)
|
|
65
|
+
sigm = np.ma.masked_array(np.ma.getdata(sigm), mask=mask)
|
|
66
|
+
|
|
67
|
+
if scaling:
|
|
68
|
+
# Scale everything to roughly the same size, around spec/blaze = 1
|
|
69
|
+
scale = np.ma.median(spec / cont, axis=1)
|
|
70
|
+
cont *= scale[:, None]
|
|
71
|
+
|
|
72
|
+
if plot: # pragma: no cover
|
|
73
|
+
plt.subplot(411)
|
|
74
|
+
if plot_title is not None:
|
|
75
|
+
plt.suptitle(plot_title)
|
|
76
|
+
plt.title("Before")
|
|
77
|
+
for i in range(spec.shape[0]):
|
|
78
|
+
plt.plot(wave[i], spec[i] / cont[i])
|
|
79
|
+
plt.ylim([0, 2])
|
|
80
|
+
|
|
81
|
+
plt.subplot(412)
|
|
82
|
+
plt.title("Before Error")
|
|
83
|
+
for i in range(spec.shape[0]):
|
|
84
|
+
plt.plot(wave[i], sigm[i] / cont[i])
|
|
85
|
+
plt.ylim((0, np.ma.median(sigm[i] / cont[i]) * 2))
|
|
86
|
+
|
|
87
|
+
# Order with largest signal, everything is scaled relative to this order
|
|
88
|
+
iord0 = np.argmax(np.ma.median(spec / cont, axis=1))
|
|
89
|
+
|
|
90
|
+
# Loop from iord0 outwards, first to the top, then to the bottom
|
|
91
|
+
tmp0 = chain(range(iord0, 0, -1), range(iord0, nord - 1))
|
|
92
|
+
tmp1 = chain(range(iord0 - 1, -1, -1), range(iord0 + 1, nord))
|
|
93
|
+
|
|
94
|
+
# Looping over order pairs
|
|
95
|
+
for iord0, iord1 in zip(tmp0, tmp1, strict=False):
|
|
96
|
+
# Get data for current order
|
|
97
|
+
# Note that those are just references to parts of the original data
|
|
98
|
+
# any changes will also affect spec, wave, cont, and sigm
|
|
99
|
+
s0, s1 = spec[iord0], spec[iord1]
|
|
100
|
+
w0, w1 = wave[iord0], wave[iord1]
|
|
101
|
+
c0, c1 = cont[iord0], cont[iord1]
|
|
102
|
+
u0, u1 = sigm[iord0], sigm[iord1]
|
|
103
|
+
|
|
104
|
+
# Calculate Overlap
|
|
105
|
+
i0 = np.ma.where((w0 >= np.ma.min(w1)) & (w0 <= np.ma.max(w1)))
|
|
106
|
+
i1 = np.ma.where((w1 >= np.ma.min(w0)) & (w1 <= np.ma.max(w0)))
|
|
107
|
+
|
|
108
|
+
# Orders overlap
|
|
109
|
+
if i0[0].size > 0 and i1[0].size > 0:
|
|
110
|
+
# Interpolate the overlapping region onto the wavelength grid of the other order
|
|
111
|
+
tmpS0 = util.bezier_interp(w1, s1, w0[i0])
|
|
112
|
+
tmpB0 = util.bezier_interp(w1, c1, w0[i0])
|
|
113
|
+
tmpU0 = util.bezier_interp(w1, u1, w0[i0])
|
|
114
|
+
|
|
115
|
+
tmpS1 = util.bezier_interp(w0, s0, w1[i1])
|
|
116
|
+
tmpB1 = util.bezier_interp(w0, c0, w1[i1])
|
|
117
|
+
tmpU1 = util.bezier_interp(w0, u0, w1[i1])
|
|
118
|
+
|
|
119
|
+
# Combine the two orders weighted by the relative error
|
|
120
|
+
wgt0 = np.ma.vstack([c0[i0].data / u0[i0].data, tmpB0 / tmpU0]) ** 2
|
|
121
|
+
wgt1 = np.ma.vstack([c1[i1].data / u1[i1].data, tmpB1 / tmpU1]) ** 2
|
|
122
|
+
|
|
123
|
+
s0[i0], utmp = np.ma.average(
|
|
124
|
+
np.ma.vstack([s0[i0], tmpS0]), axis=0, weights=wgt0, returned=True
|
|
125
|
+
)
|
|
126
|
+
c0[i0] = np.ma.average([c0[i0], tmpB0], axis=0, weights=wgt0)
|
|
127
|
+
u0[i0] = c0[i0] * utmp**-0.5
|
|
128
|
+
|
|
129
|
+
s1[i1], utmp = np.ma.average(
|
|
130
|
+
np.ma.vstack([s1[i1], tmpS1]), axis=0, weights=wgt1, returned=True
|
|
131
|
+
)
|
|
132
|
+
c1[i1] = np.ma.average([c1[i1], tmpB1], axis=0, weights=wgt1)
|
|
133
|
+
u1[i1] = c1[i1] * utmp**-0.5
|
|
134
|
+
else: # pragma: no cover
|
|
135
|
+
# TODO: Orders dont overlap
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
if plot: # pragma: no cover
|
|
139
|
+
plt.subplot(413)
|
|
140
|
+
plt.title("After")
|
|
141
|
+
for i in range(nord):
|
|
142
|
+
plt.plot(wave[i], spec[i] / cont[i], label="order=%i" % i)
|
|
143
|
+
plt.ylim((0, 2))
|
|
144
|
+
|
|
145
|
+
plt.subplot(414)
|
|
146
|
+
plt.title("Error")
|
|
147
|
+
for i in range(nord):
|
|
148
|
+
plt.plot(wave[i], sigm[i] / cont[i], label="order=%i" % i)
|
|
149
|
+
plt.ylim((0, np.ma.median(sigm[i] / cont[i]) * 2))
|
|
150
|
+
util.show_or_save("continuum_sigma")
|
|
151
|
+
|
|
152
|
+
return spec, wave, cont, sigm
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class Plot_Normalization: # pragma: no cover
|
|
156
|
+
def __init__(self, wsort, sB, new_wave, contB, iteration=0, title=None):
|
|
157
|
+
plt.ion()
|
|
158
|
+
self.fig = plt.figure()
|
|
159
|
+
self.title = title
|
|
160
|
+
suptitle = f"Iteration: {iteration}"
|
|
161
|
+
if self.title is not None:
|
|
162
|
+
suptitle = f"{self.title}\n{suptitle}"
|
|
163
|
+
self.fig.suptitle(suptitle)
|
|
164
|
+
|
|
165
|
+
self.ax = self.fig.add_subplot(111)
|
|
166
|
+
self.line1 = self.ax.plot(wsort, sB, label="Spectrum")[0]
|
|
167
|
+
self.line2 = self.ax.plot(new_wave, contB, label="Continuum Fit")[0]
|
|
168
|
+
plt.legend()
|
|
169
|
+
|
|
170
|
+
util.show_or_save("continuum_fit")
|
|
171
|
+
|
|
172
|
+
def plot(self, wsort, sB, new_wave, contB, iteration):
|
|
173
|
+
suptitle = f"Iteration: {iteration}"
|
|
174
|
+
if self.title is not None:
|
|
175
|
+
suptitle = f"{self.title}\n{suptitle}"
|
|
176
|
+
self.fig.suptitle(suptitle)
|
|
177
|
+
|
|
178
|
+
self.line1.set_xdata(wsort)
|
|
179
|
+
self.line1.set_ydata(sB)
|
|
180
|
+
self.line2.set_xdata(new_wave)
|
|
181
|
+
self.line2.set_ydata(contB)
|
|
182
|
+
|
|
183
|
+
self.fig.canvas.draw()
|
|
184
|
+
self.fig.canvas.flush_events()
|
|
185
|
+
|
|
186
|
+
def close(self):
|
|
187
|
+
plt.ioff()
|
|
188
|
+
plt.close()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def continuum_normalize(
|
|
192
|
+
spec,
|
|
193
|
+
wave,
|
|
194
|
+
cont,
|
|
195
|
+
sigm,
|
|
196
|
+
iterations=10,
|
|
197
|
+
smooth_initial=1e5,
|
|
198
|
+
smooth_final=5e6,
|
|
199
|
+
scale_vert=1,
|
|
200
|
+
plot=True,
|
|
201
|
+
plot_title=None,
|
|
202
|
+
):
|
|
203
|
+
"""Fit a continuum to a spectrum by slowly approaching it from the top.
|
|
204
|
+
We exploit here that the continuum varies only on large wavelength scales, while individual lines act on much smaller scales
|
|
205
|
+
|
|
206
|
+
TODO automatically find good parameters for smooth_initial and smooth_final
|
|
207
|
+
TODO give variables better names
|
|
208
|
+
|
|
209
|
+
Parameters
|
|
210
|
+
----------
|
|
211
|
+
spec : masked array of shape (nord, ncol)
|
|
212
|
+
Observed input spectrum, masked values describe column ranges
|
|
213
|
+
wave : masked array of shape (nord, ncol)
|
|
214
|
+
Wavelength solution of the spectrum
|
|
215
|
+
cont : masked array of shape (nord, ncol)
|
|
216
|
+
Initial continuum guess, for example based on the blaze
|
|
217
|
+
sigm : masked array of shape (nord, ncol)
|
|
218
|
+
Uncertainties of the spectrum
|
|
219
|
+
iterations : int, optional
|
|
220
|
+
Number of iterations of the algorithm,
|
|
221
|
+
note that runtime roughly scales with the number of iterations squared
|
|
222
|
+
(default: 10)
|
|
223
|
+
smooth_initial : float, optional
|
|
224
|
+
Smoothing parameter in the initial runs, usually smaller than smooth_final (default: 1e5)
|
|
225
|
+
smooth_final : float, optional
|
|
226
|
+
Smoothing parameter of the final run (default: 5e6)
|
|
227
|
+
scale_vert : float, optional
|
|
228
|
+
Vertical scale of the spectrum. Usually 1 if a previous normalization exists (default: 1)
|
|
229
|
+
plot : bool, optional
|
|
230
|
+
Wether to plot the current status and results or not (default: True)
|
|
231
|
+
|
|
232
|
+
Returns
|
|
233
|
+
-------
|
|
234
|
+
cont : masked array of shape (nord, ncol)
|
|
235
|
+
New continuum
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
nord, ncol = spec.shape
|
|
239
|
+
|
|
240
|
+
par2 = 1e-4
|
|
241
|
+
par4 = 0.01 * (1 - np.clip(2, None, 1 / np.sqrt(np.ma.median(spec))))
|
|
242
|
+
|
|
243
|
+
b = np.clip(cont, 1, None)
|
|
244
|
+
mask = ~np.ma.getmaskarray(b)
|
|
245
|
+
for i in range(nord):
|
|
246
|
+
b[i, mask[i]] = util.middle(b[i, mask[i]], 1)
|
|
247
|
+
cont = b
|
|
248
|
+
|
|
249
|
+
# Create new equispaced wavelength grid
|
|
250
|
+
tmp = wave.compressed()
|
|
251
|
+
wmin = np.min(tmp)
|
|
252
|
+
wmax = np.max(tmp)
|
|
253
|
+
dwave = np.abs(tmp[tmp.size // 2] - tmp[tmp.size // 2 - 1]) * 0.5
|
|
254
|
+
nwave = np.ceil((wmax - wmin) / dwave) + 1
|
|
255
|
+
new_wave = np.linspace(wmin, wmax, int(nwave), endpoint=True)
|
|
256
|
+
|
|
257
|
+
# Combine all orders into one big spectrum, sorted by wavelength
|
|
258
|
+
wsort, j, index = np.unique(tmp, return_index=True, return_inverse=True)
|
|
259
|
+
sB = (spec / cont).compressed()[j]
|
|
260
|
+
|
|
261
|
+
# Get initial weights for each point
|
|
262
|
+
weight = util.middle(sB, 0.5, x=wsort - wmin)
|
|
263
|
+
weight = weight / util.middle(weight, 3 * smooth_initial) + np.concatenate(
|
|
264
|
+
([0], 2 * weight[1:-1] - weight[0:-2] - weight[2:], [0])
|
|
265
|
+
)
|
|
266
|
+
weight = np.clip(weight, 0, None)
|
|
267
|
+
# TODO for some reason the interpolation messes up, use linear instead for now
|
|
268
|
+
# weight = util.safe_interpolation(wsort, weight, new_wave)
|
|
269
|
+
weight = np.interp(new_wave, wsort, weight)
|
|
270
|
+
weight /= np.max(weight)
|
|
271
|
+
|
|
272
|
+
# Interpolate Spectrum onto the new grid
|
|
273
|
+
# ssB = util.safe_interpolation(wsort, sB, new_wave)
|
|
274
|
+
ssB = np.interp(new_wave, wsort, sB)
|
|
275
|
+
# Keep the scale of the continuum
|
|
276
|
+
bbb = util.middle(cont.compressed()[j], 1)
|
|
277
|
+
|
|
278
|
+
contB = np.ones_like(ssB)
|
|
279
|
+
if plot: # pragma: no cover
|
|
280
|
+
p = Plot_Normalization(wsort, sB, new_wave, contB, 0, title=plot_title)
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
for i in range(iterations):
|
|
284
|
+
# Find new approximation of the top, smoothed by some parameter
|
|
285
|
+
c = ssB / contB
|
|
286
|
+
for _ in range(iterations):
|
|
287
|
+
_c = util.top(
|
|
288
|
+
c, smooth_initial, eps=par2, weight=weight, lambda2=smooth_final
|
|
289
|
+
)
|
|
290
|
+
c = np.clip(_c, c, None)
|
|
291
|
+
c = (
|
|
292
|
+
util.top(
|
|
293
|
+
c, smooth_initial, eps=par4, weight=weight, lambda2=smooth_final
|
|
294
|
+
)
|
|
295
|
+
* contB
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Scale it and update the weights of each point
|
|
299
|
+
contB = c * scale_vert
|
|
300
|
+
contB = util.middle(contB, 1)
|
|
301
|
+
weight = np.clip(ssB / contB, None, contB / np.clip(ssB, 1, None))
|
|
302
|
+
|
|
303
|
+
# Plot the intermediate results
|
|
304
|
+
if plot: # pragma: no cover
|
|
305
|
+
p.plot(wsort, sB, new_wave, contB, i)
|
|
306
|
+
except ValueError:
|
|
307
|
+
logger.error("Continuum fitting aborted")
|
|
308
|
+
finally:
|
|
309
|
+
if plot: # pragma: no cover
|
|
310
|
+
p.close()
|
|
311
|
+
|
|
312
|
+
# Calculate the new continuum from intermediate values
|
|
313
|
+
# new_cont = util.safe_interpolation(new_wave, contB, wsort)
|
|
314
|
+
new_cont = np.interp(wsort, new_wave, contB)
|
|
315
|
+
mask = np.ma.getmaskarray(cont)
|
|
316
|
+
cont[~mask] = (new_cont * bbb)[index]
|
|
317
|
+
|
|
318
|
+
# Final output plot
|
|
319
|
+
if plot: # pragma: no cover
|
|
320
|
+
plt.plot(wave.ravel(), spec.ravel(), label="spec")
|
|
321
|
+
plt.plot(wave.ravel(), cont.ravel(), label="cont")
|
|
322
|
+
plt.legend(loc="best")
|
|
323
|
+
if plot_title is not None:
|
|
324
|
+
plt.title(plot_title)
|
|
325
|
+
plt.xlabel("Wavelength [A]")
|
|
326
|
+
plt.ylabel("Flux")
|
|
327
|
+
util.show_or_save("continuum_final")
|
|
328
|
+
|
|
329
|
+
return cont
|