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
pyreduce/make_shear.py
ADDED
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Calculate the tilt based on a reference spectrum with high SNR, e.g. Wavelength calibration image
|
|
3
|
+
|
|
4
|
+
Authors
|
|
5
|
+
-------
|
|
6
|
+
Nikolai Piskunov
|
|
7
|
+
Ansgar Wehrhahn
|
|
8
|
+
|
|
9
|
+
Version
|
|
10
|
+
--------
|
|
11
|
+
0.9 - NP - IDL Version
|
|
12
|
+
1.0 - AW - Python Version
|
|
13
|
+
|
|
14
|
+
License
|
|
15
|
+
-------
|
|
16
|
+
....
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
|
|
21
|
+
import matplotlib.pyplot as plt
|
|
22
|
+
import numpy as np
|
|
23
|
+
from numpy.polynomial.polynomial import polyval2d
|
|
24
|
+
from scipy import signal
|
|
25
|
+
from scipy.optimize import least_squares
|
|
26
|
+
from tqdm import tqdm
|
|
27
|
+
|
|
28
|
+
from . import util
|
|
29
|
+
from .extract import fix_parameters
|
|
30
|
+
from .util import make_index
|
|
31
|
+
from .util import polyfit2d_2 as polyfit2d
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ProgressPlot: # pragma: no cover
|
|
37
|
+
def __init__(self, ncol, width, title=None):
|
|
38
|
+
plt.ion()
|
|
39
|
+
|
|
40
|
+
fig, (ax1, ax2, ax3) = plt.subplots(ncols=3)
|
|
41
|
+
|
|
42
|
+
plot_title = "Curvature in each order"
|
|
43
|
+
if title is not None:
|
|
44
|
+
plot_title = f"{title}\n{plot_title}"
|
|
45
|
+
fig.suptitle(plot_title)
|
|
46
|
+
|
|
47
|
+
(line1,) = ax1.plot(np.arange(ncol) + 1)
|
|
48
|
+
(line2,) = ax1.plot(0, 0, "d")
|
|
49
|
+
ax1.set_yscale("log")
|
|
50
|
+
|
|
51
|
+
self.ncol = ncol
|
|
52
|
+
self.width = width * 2 + 1
|
|
53
|
+
|
|
54
|
+
self.fig = fig
|
|
55
|
+
self.ax1 = ax1
|
|
56
|
+
self.ax2 = ax2
|
|
57
|
+
self.ax3 = ax3
|
|
58
|
+
self.line1 = line1
|
|
59
|
+
self.line2 = line2
|
|
60
|
+
|
|
61
|
+
def update_plot1(self, vector, peak, offset=0):
|
|
62
|
+
data = np.ones(self.ncol)
|
|
63
|
+
data[offset : len(vector) + offset] = np.clip(vector, 1, None)
|
|
64
|
+
self.line1.set_ydata(data)
|
|
65
|
+
self.line2.set_xdata(peak)
|
|
66
|
+
self.line2.set_ydata(data[peak])
|
|
67
|
+
self.ax1.set_ylim((data.min(), data.max()))
|
|
68
|
+
self.fig.canvas.draw()
|
|
69
|
+
self.fig.canvas.flush_events()
|
|
70
|
+
|
|
71
|
+
def update_plot2(self, img, model, tilt, shear, peak):
|
|
72
|
+
self.ax2.clear()
|
|
73
|
+
self.ax3.clear()
|
|
74
|
+
|
|
75
|
+
self.ax2.imshow(img)
|
|
76
|
+
self.ax3.imshow(model)
|
|
77
|
+
|
|
78
|
+
nrows, _ = img.shape
|
|
79
|
+
middle = nrows // 2
|
|
80
|
+
y = np.arange(-middle, -middle + nrows)
|
|
81
|
+
x = peak + (tilt + shear * y) * y
|
|
82
|
+
y += middle
|
|
83
|
+
|
|
84
|
+
self.ax2.plot(x, y, "r")
|
|
85
|
+
self.ax3.plot(x, y, "r")
|
|
86
|
+
|
|
87
|
+
self.fig.canvas.draw()
|
|
88
|
+
self.fig.canvas.flush_events()
|
|
89
|
+
|
|
90
|
+
def close(self):
|
|
91
|
+
plt.close()
|
|
92
|
+
plt.ioff()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Curvature:
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
orders,
|
|
99
|
+
extraction_width=0.5,
|
|
100
|
+
column_range=None,
|
|
101
|
+
order_range=None,
|
|
102
|
+
window_width=9,
|
|
103
|
+
peak_threshold=10,
|
|
104
|
+
peak_width=1,
|
|
105
|
+
fit_degree=2,
|
|
106
|
+
sigma_cutoff=3,
|
|
107
|
+
mode="1D",
|
|
108
|
+
plot=False,
|
|
109
|
+
plot_title=None,
|
|
110
|
+
peak_function="gaussian",
|
|
111
|
+
curv_degree=2,
|
|
112
|
+
):
|
|
113
|
+
self.orders = orders
|
|
114
|
+
self.extraction_width = extraction_width
|
|
115
|
+
self.column_range = column_range
|
|
116
|
+
if order_range is None:
|
|
117
|
+
order_range = (0, self.nord)
|
|
118
|
+
self.order_range = order_range
|
|
119
|
+
self.window_width = window_width
|
|
120
|
+
self.threshold = peak_threshold
|
|
121
|
+
self.peak_width = peak_width
|
|
122
|
+
self.fit_degree = fit_degree
|
|
123
|
+
self.sigma_cutoff = sigma_cutoff
|
|
124
|
+
self.mode = mode
|
|
125
|
+
self.plot = plot
|
|
126
|
+
self.plot_title = plot_title
|
|
127
|
+
self.curv_degree = curv_degree
|
|
128
|
+
self.peak_function = peak_function
|
|
129
|
+
|
|
130
|
+
if self.mode == "1D":
|
|
131
|
+
# fit degree is an integer
|
|
132
|
+
if not np.isscalar(self.fit_degree):
|
|
133
|
+
self.fit_degree = self.fit_degree[0]
|
|
134
|
+
elif self.mode == "2D":
|
|
135
|
+
# fit degree is a 2 tuple
|
|
136
|
+
if np.isscalar(self.fit_degree):
|
|
137
|
+
self.fit_degree = (self.fit_degree, self.fit_degree)
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def nord(self):
|
|
141
|
+
return self.orders.shape[0]
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def n(self):
|
|
145
|
+
return self.order_range[1] - self.order_range[0]
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def mode(self):
|
|
149
|
+
return self._mode
|
|
150
|
+
|
|
151
|
+
@mode.setter
|
|
152
|
+
def mode(self, value):
|
|
153
|
+
if value not in ["1D", "2D"]:
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"Value for 'mode' not understood. Expected one of ['1D', '2D'] but got {value}"
|
|
156
|
+
)
|
|
157
|
+
self._mode = value
|
|
158
|
+
|
|
159
|
+
def _fix_inputs(self, original):
|
|
160
|
+
orders = self.orders
|
|
161
|
+
extraction_width = self.extraction_width
|
|
162
|
+
column_range = self.column_range
|
|
163
|
+
|
|
164
|
+
nrow, ncol = original.shape
|
|
165
|
+
nord = len(orders)
|
|
166
|
+
|
|
167
|
+
extraction_width, column_range, orders = fix_parameters(
|
|
168
|
+
extraction_width, column_range, orders, nrow, ncol, nord
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
self.column_range = column_range[self.order_range[0] : self.order_range[1]]
|
|
172
|
+
self.extraction_width = extraction_width[
|
|
173
|
+
self.order_range[0] : self.order_range[1]
|
|
174
|
+
]
|
|
175
|
+
self.orders = orders[self.order_range[0] : self.order_range[1]]
|
|
176
|
+
self.order_range = (0, self.n)
|
|
177
|
+
|
|
178
|
+
def _find_peaks(self, vec, cr):
|
|
179
|
+
# This should probably be the same as in the wavelength calibration
|
|
180
|
+
vec -= np.ma.median(vec)
|
|
181
|
+
vec = np.ma.filled(vec, 0)
|
|
182
|
+
height = np.percentile(vec, 68) * self.threshold
|
|
183
|
+
peaks, _ = signal.find_peaks(
|
|
184
|
+
vec, prominence=height, width=self.peak_width, distance=self.window_width
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Remove peaks at the edge
|
|
188
|
+
peaks = peaks[
|
|
189
|
+
(peaks >= self.window_width + 1)
|
|
190
|
+
& (peaks < len(vec) - self.window_width - 1)
|
|
191
|
+
]
|
|
192
|
+
# Remove the offset, due to vec being a subset of extracted
|
|
193
|
+
peaks += cr[0]
|
|
194
|
+
return vec, peaks
|
|
195
|
+
|
|
196
|
+
def _determine_curvature_single_line(self, original, peak, ycen, ycen_int, xwd):
|
|
197
|
+
"""
|
|
198
|
+
Fit the curvature of a single peak in the spectrum
|
|
199
|
+
|
|
200
|
+
This is achieved by fitting a model, that consists of gaussians
|
|
201
|
+
in spectrum direction, that are shifted by the curvature in each row.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
original : array of shape (nrows, ncols)
|
|
206
|
+
whole input image
|
|
207
|
+
peak : int
|
|
208
|
+
column position of the peak
|
|
209
|
+
ycen : array of shape (ncols,)
|
|
210
|
+
row center of the order of the peak
|
|
211
|
+
xwd : 2 tuple
|
|
212
|
+
extraction width above and below the order center to use
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
tilt : float
|
|
217
|
+
first order curvature
|
|
218
|
+
shear : float
|
|
219
|
+
second order curvature
|
|
220
|
+
"""
|
|
221
|
+
_, ncol = original.shape
|
|
222
|
+
|
|
223
|
+
# look at +- width pixels around the line
|
|
224
|
+
# Extract short horizontal strip for each row in extraction width
|
|
225
|
+
# Then fit a gaussian to each row, to find the center of the line
|
|
226
|
+
x = peak + np.arange(-self.window_width, self.window_width + 1)
|
|
227
|
+
x = x[(x >= 0) & (x < ncol)]
|
|
228
|
+
xmin, xmax = x[0], x[-1] + 1
|
|
229
|
+
|
|
230
|
+
# Look above and below the line center
|
|
231
|
+
y = np.arange(-xwd[0], xwd[1] + 1)[:, None] - ycen[xmin:xmax][None, :]
|
|
232
|
+
|
|
233
|
+
x = x[None, :]
|
|
234
|
+
idx = make_index(ycen_int - xwd[0], ycen_int + xwd[1], xmin, xmax)
|
|
235
|
+
img = original[idx]
|
|
236
|
+
img_compressed = np.ma.compressed(img)
|
|
237
|
+
|
|
238
|
+
img -= np.percentile(img_compressed, 1)
|
|
239
|
+
img /= np.percentile(img_compressed, 99)
|
|
240
|
+
img = np.ma.clip(img, 0, 1)
|
|
241
|
+
|
|
242
|
+
sl = np.ma.mean(img, axis=1)
|
|
243
|
+
sl = sl[:, None]
|
|
244
|
+
|
|
245
|
+
peak_func = {"gaussian": gaussian, "lorentzian": lorentzian}
|
|
246
|
+
peak_func = peak_func[self.peak_function]
|
|
247
|
+
|
|
248
|
+
def model(coef):
|
|
249
|
+
A, middle, sig, *curv = coef
|
|
250
|
+
mu = middle + shift(curv)
|
|
251
|
+
mod = peak_func(x, A, mu, sig)
|
|
252
|
+
mod *= sl
|
|
253
|
+
return (mod - img).ravel()
|
|
254
|
+
|
|
255
|
+
def model_compressed(coef):
|
|
256
|
+
return np.ma.compressed(model(coef))
|
|
257
|
+
|
|
258
|
+
A = np.nanpercentile(img_compressed, 95)
|
|
259
|
+
sig = (xmax - xmin) / 4 # TODO
|
|
260
|
+
if self.curv_degree == 1:
|
|
261
|
+
|
|
262
|
+
def shift(curv):
|
|
263
|
+
return curv[0] * y
|
|
264
|
+
elif self.curv_degree == 2:
|
|
265
|
+
|
|
266
|
+
def shift(curv):
|
|
267
|
+
return (curv[0] + curv[1] * y) * y
|
|
268
|
+
else:
|
|
269
|
+
raise ValueError("Only curvature degrees 1 and 2 are supported")
|
|
270
|
+
# res = least_squares(model, x0=[A, middle, sig, 0], loss="soft_l1", bounds=([0, xmin, 1, -10],[np.inf, xmax, xmax, 10]))
|
|
271
|
+
x0 = [A, peak, sig] + [0] * self.curv_degree
|
|
272
|
+
res = least_squares(
|
|
273
|
+
model_compressed, x0=x0, method="trf", loss="soft_l1", f_scale=0.1
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
if self.curv_degree == 1:
|
|
277
|
+
tilt, shear = res.x[3], 0
|
|
278
|
+
elif self.curv_degree == 2:
|
|
279
|
+
tilt, shear = res.x[3], res.x[4]
|
|
280
|
+
else:
|
|
281
|
+
tilt, shear = 0, 0
|
|
282
|
+
|
|
283
|
+
# model = model(res.x).reshape(img.shape) + img
|
|
284
|
+
# vmin = 0
|
|
285
|
+
# vmax = np.max(model)
|
|
286
|
+
|
|
287
|
+
# y = y.ravel()
|
|
288
|
+
# x = res.x[1] - xmin + (tilt + shear * y) * y
|
|
289
|
+
# y += xwd[0]
|
|
290
|
+
|
|
291
|
+
# plt.subplot(121)
|
|
292
|
+
# plt.imshow(img, vmin=vmin, vmax=vmax, origin="lower")
|
|
293
|
+
# plt.plot(xwd[0] + ycen[xmin:xmax], "r")
|
|
294
|
+
# plt.title("Input Image")
|
|
295
|
+
# plt.xlabel("x [pixel]")
|
|
296
|
+
# plt.ylabel("y [pixel]")
|
|
297
|
+
|
|
298
|
+
# plt.subplot(122)
|
|
299
|
+
# plt.imshow(model, vmin=vmin, vmax=vmax, origin="lower")
|
|
300
|
+
# plt.plot(x, y, "r", label="curvature")
|
|
301
|
+
# plt.ylim((-0.5, model.shape[0] - 0.5))
|
|
302
|
+
# plt.title("Model")
|
|
303
|
+
# plt.xlabel("x [pixel]")
|
|
304
|
+
# plt.ylabel("y [pixel]")
|
|
305
|
+
|
|
306
|
+
# plt.show()
|
|
307
|
+
|
|
308
|
+
if self.plot >= 2:
|
|
309
|
+
model = res.fun.reshape(img.shape) + img
|
|
310
|
+
self.progress.update_plot2(img, model, tilt, shear, res.x[1] - xmin)
|
|
311
|
+
|
|
312
|
+
return tilt, shear
|
|
313
|
+
|
|
314
|
+
def _fit_curvature_single_order(self, peaks, tilt, shear):
|
|
315
|
+
try:
|
|
316
|
+
middle = np.median(tilt)
|
|
317
|
+
sigma = np.percentile(tilt, (32, 68))
|
|
318
|
+
sigma = middle - sigma[0], sigma[1] - middle
|
|
319
|
+
mask = (tilt >= middle - 5 * sigma[0]) & (tilt <= middle + 5 * sigma[1])
|
|
320
|
+
peaks, tilt, shear = peaks[mask], tilt[mask], shear[mask]
|
|
321
|
+
|
|
322
|
+
coef_tilt = np.zeros(self.fit_degree + 1)
|
|
323
|
+
res = least_squares(
|
|
324
|
+
lambda coef: np.polyval(coef, peaks) - tilt,
|
|
325
|
+
x0=coef_tilt,
|
|
326
|
+
loss="arctan",
|
|
327
|
+
)
|
|
328
|
+
coef_tilt = res.x
|
|
329
|
+
|
|
330
|
+
coef_shear = np.zeros(self.fit_degree + 1)
|
|
331
|
+
res = least_squares(
|
|
332
|
+
lambda coef: np.polyval(coef, peaks) - shear,
|
|
333
|
+
x0=coef_shear,
|
|
334
|
+
loss="arctan",
|
|
335
|
+
)
|
|
336
|
+
coef_shear = res.x
|
|
337
|
+
|
|
338
|
+
except:
|
|
339
|
+
logger.error(
|
|
340
|
+
"Could not fit the curvature of this order. Using no curvature instead"
|
|
341
|
+
)
|
|
342
|
+
coef_tilt = np.zeros(self.fit_degree + 1)
|
|
343
|
+
coef_shear = np.zeros(self.fit_degree + 1)
|
|
344
|
+
|
|
345
|
+
return coef_tilt, coef_shear, peaks
|
|
346
|
+
|
|
347
|
+
def _determine_curvature_all_lines(self, original, extracted):
|
|
348
|
+
ncol = original.shape[1]
|
|
349
|
+
# Store data from all orders
|
|
350
|
+
all_peaks = []
|
|
351
|
+
all_tilt = []
|
|
352
|
+
all_shear = []
|
|
353
|
+
plot_vec = []
|
|
354
|
+
|
|
355
|
+
for j in tqdm(range(self.n), desc="Order"):
|
|
356
|
+
logger.debug("Calculating tilt of order %i out of %i", j + 1, self.n)
|
|
357
|
+
|
|
358
|
+
cr = self.column_range[j]
|
|
359
|
+
xwd = self.extraction_width[j]
|
|
360
|
+
ycen = np.polyval(self.orders[j], np.arange(ncol))
|
|
361
|
+
ycen_int = ycen.astype(int)
|
|
362
|
+
ycen -= ycen_int
|
|
363
|
+
|
|
364
|
+
# Find peaks
|
|
365
|
+
vec = extracted[j, cr[0] : cr[1]]
|
|
366
|
+
vec, peaks = self._find_peaks(vec, cr)
|
|
367
|
+
|
|
368
|
+
npeaks = len(peaks)
|
|
369
|
+
|
|
370
|
+
# Determine curvature for each line seperately
|
|
371
|
+
tilt = np.zeros(npeaks)
|
|
372
|
+
shear = np.zeros(npeaks)
|
|
373
|
+
mask = np.full(npeaks, True)
|
|
374
|
+
for ipeak, peak in tqdm(
|
|
375
|
+
enumerate(peaks), total=len(peaks), desc="Peak", leave=False
|
|
376
|
+
):
|
|
377
|
+
if self.plot >= 2: # pragma: no cover
|
|
378
|
+
self.progress.update_plot1(vec, peak, cr[0])
|
|
379
|
+
try:
|
|
380
|
+
tilt[ipeak], shear[ipeak] = self._determine_curvature_single_line(
|
|
381
|
+
original, peak, ycen, ycen_int, xwd
|
|
382
|
+
)
|
|
383
|
+
except RuntimeError: # pragma: no cover
|
|
384
|
+
mask[ipeak] = False
|
|
385
|
+
|
|
386
|
+
# Store results
|
|
387
|
+
all_peaks += [peaks[mask]]
|
|
388
|
+
all_tilt += [tilt[mask]]
|
|
389
|
+
all_shear += [shear[mask]]
|
|
390
|
+
plot_vec += [vec]
|
|
391
|
+
return all_peaks, all_tilt, all_shear, plot_vec
|
|
392
|
+
|
|
393
|
+
def fit(self, peaks, tilt, shear):
|
|
394
|
+
if self.mode == "1D":
|
|
395
|
+
coef_tilt = np.zeros((self.n, self.fit_degree + 1))
|
|
396
|
+
coef_shear = np.zeros((self.n, self.fit_degree + 1))
|
|
397
|
+
for j in range(self.n):
|
|
398
|
+
coef_tilt[j], coef_shear[j], _ = self._fit_curvature_single_order(
|
|
399
|
+
peaks[j], tilt[j], shear[j]
|
|
400
|
+
)
|
|
401
|
+
elif self.mode == "2D":
|
|
402
|
+
x = np.concatenate(peaks)
|
|
403
|
+
y = [np.full(len(p), i) for i, p in enumerate(peaks)]
|
|
404
|
+
y = np.concatenate(y)
|
|
405
|
+
z = np.concatenate(tilt)
|
|
406
|
+
coef_tilt = polyfit2d(x, y, z, degree=self.fit_degree, loss="arctan")
|
|
407
|
+
|
|
408
|
+
z = np.concatenate(shear)
|
|
409
|
+
coef_shear = polyfit2d(x, y, z, degree=self.fit_degree, loss="arctan")
|
|
410
|
+
|
|
411
|
+
return coef_tilt, coef_shear
|
|
412
|
+
|
|
413
|
+
def eval(self, peaks, order, coef_tilt, coef_shear):
|
|
414
|
+
if self.mode == "1D":
|
|
415
|
+
tilt = np.zeros(peaks.shape)
|
|
416
|
+
shear = np.zeros(peaks.shape)
|
|
417
|
+
for i in np.unique(order):
|
|
418
|
+
idx = order == i
|
|
419
|
+
tilt[idx] = np.polyval(coef_tilt[i], peaks[idx])
|
|
420
|
+
shear[idx] = np.polyval(coef_shear[i], peaks[idx])
|
|
421
|
+
elif self.mode == "2D":
|
|
422
|
+
tilt = polyval2d(peaks, order, coef_tilt)
|
|
423
|
+
shear = polyval2d(peaks, order, coef_shear)
|
|
424
|
+
return tilt, shear
|
|
425
|
+
|
|
426
|
+
def plot_results(
|
|
427
|
+
self, ncol, plot_peaks, plot_vec, plot_tilt, plot_shear, tilt_x, shear_x
|
|
428
|
+
): # pragma: no cover
|
|
429
|
+
fig, axes = plt.subplots(nrows=self.n // 2 + self.n % 2, ncols=2, squeeze=False)
|
|
430
|
+
|
|
431
|
+
title = "Peaks"
|
|
432
|
+
if self.plot_title is not None:
|
|
433
|
+
title = f"{self.plot_title}\n{title}"
|
|
434
|
+
fig.suptitle(title)
|
|
435
|
+
fig1, axes1 = plt.subplots(
|
|
436
|
+
nrows=self.n // 2 + self.n % 2, ncols=2, squeeze=False
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
title = "1st Order Curvature"
|
|
440
|
+
if self.plot_title is not None:
|
|
441
|
+
title = f"{self.plot_title}\n{title}"
|
|
442
|
+
fig1.suptitle(title)
|
|
443
|
+
fig2, axes2 = plt.subplots(
|
|
444
|
+
nrows=self.n // 2 + self.n % 2, ncols=2, squeeze=False
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
title = "2nd Order Curvature"
|
|
448
|
+
if self.plot_title is not None:
|
|
449
|
+
title = f"{self.plot_title}\n{title}"
|
|
450
|
+
fig2.suptitle(title)
|
|
451
|
+
plt.subplots_adjust(hspace=0)
|
|
452
|
+
|
|
453
|
+
def trim_axs(axs, N):
|
|
454
|
+
"""little helper to massage the axs list to have correct length..."""
|
|
455
|
+
axs = axs.flat
|
|
456
|
+
for ax in axs[N:]:
|
|
457
|
+
ax.remove()
|
|
458
|
+
return axs[:N]
|
|
459
|
+
|
|
460
|
+
t, s = [None for _ in range(self.n)], [None for _ in range(self.n)]
|
|
461
|
+
for j in range(self.n):
|
|
462
|
+
cr = self.column_range[j]
|
|
463
|
+
x = np.arange(cr[0], cr[1])
|
|
464
|
+
order = np.full(len(x), j)
|
|
465
|
+
t[j], s[j] = self.eval(x, order, tilt_x, shear_x)
|
|
466
|
+
|
|
467
|
+
t_lower = min(t.min() * (0.5 if t.min() > 0 else 1.5) for t in t)
|
|
468
|
+
t_upper = max(t.max() * (1.5 if t.max() > 0 else 0.5) for t in t)
|
|
469
|
+
|
|
470
|
+
s_lower = min(s.min() * (0.5 if s.min() > 0 else 1.5) for s in s)
|
|
471
|
+
s_upper = max(s.max() * (1.5 if s.max() > 0 else 0.5) for s in s)
|
|
472
|
+
|
|
473
|
+
for j in range(self.n):
|
|
474
|
+
cr = self.column_range[j]
|
|
475
|
+
peaks = plot_peaks[j]
|
|
476
|
+
vec = np.clip(plot_vec[j], 0, None)
|
|
477
|
+
tilt = plot_tilt[j]
|
|
478
|
+
shear = plot_shear[j]
|
|
479
|
+
x = np.arange(cr[0], cr[1])
|
|
480
|
+
# Figure Peaks found (and used)
|
|
481
|
+
axes[j // 2, j % 2].plot(np.arange(cr[0], cr[1]), vec)
|
|
482
|
+
axes[j // 2, j % 2].plot(peaks, vec[peaks - cr[0]], "X")
|
|
483
|
+
axes[j // 2, j % 2].set_xlim([0, ncol])
|
|
484
|
+
# axes[j // 2, j % 2].set_yscale("log")
|
|
485
|
+
if j not in (self.n - 1, self.n - 2):
|
|
486
|
+
axes[j // 2, j % 2].get_xaxis().set_ticks([])
|
|
487
|
+
|
|
488
|
+
# Figure 1st order
|
|
489
|
+
axes1[j // 2, j % 2].plot(peaks, tilt, "rX")
|
|
490
|
+
axes1[j // 2, j % 2].plot(x, t[j])
|
|
491
|
+
axes1[j // 2, j % 2].set_xlim(0, ncol)
|
|
492
|
+
|
|
493
|
+
axes1[j // 2, j % 2].set_ylim(t_lower, t_upper)
|
|
494
|
+
if j not in (self.n - 1, self.n - 2):
|
|
495
|
+
axes1[j // 2, j % 2].get_xaxis().set_ticks([])
|
|
496
|
+
else:
|
|
497
|
+
axes1[j // 2, j % 2].set_xlabel("x [pixel]")
|
|
498
|
+
if j == self.n // 2 + 1:
|
|
499
|
+
axes1[j // 2, j % 2].set_ylabel("tilt [pixel/pixel]")
|
|
500
|
+
|
|
501
|
+
# Figure 2nd order
|
|
502
|
+
axes2[j // 2, j % 2].plot(peaks, shear, "rX")
|
|
503
|
+
axes2[j // 2, j % 2].plot(x, s[j])
|
|
504
|
+
axes2[j // 2, j % 2].set_xlim(0, ncol)
|
|
505
|
+
|
|
506
|
+
axes2[j // 2, j % 2].set_ylim(s_lower, s_upper)
|
|
507
|
+
if j not in (self.n - 1, self.n - 2):
|
|
508
|
+
axes2[j // 2, j % 2].get_xaxis().set_ticks([])
|
|
509
|
+
else:
|
|
510
|
+
axes2[j // 2, j % 2].set_xlabel("x [pixel]")
|
|
511
|
+
if j == self.n // 2 + 1:
|
|
512
|
+
axes2[j // 2, j % 2].set_ylabel("shear [pixel/pixel**2]")
|
|
513
|
+
|
|
514
|
+
axes1 = trim_axs(axes1, self.n)
|
|
515
|
+
axes2 = trim_axs(axes2, self.n)
|
|
516
|
+
|
|
517
|
+
util.show_or_save("curvature_fit")
|
|
518
|
+
|
|
519
|
+
def plot_comparison(self, original, tilt, shear, peaks): # pragma: no cover
|
|
520
|
+
_, ncol = original.shape
|
|
521
|
+
output = np.zeros((np.sum(self.extraction_width) + self.nord, ncol))
|
|
522
|
+
pos = [0]
|
|
523
|
+
x = np.arange(ncol)
|
|
524
|
+
for i in range(self.nord):
|
|
525
|
+
ycen = np.polyval(self.orders[i], x)
|
|
526
|
+
yb = ycen - self.extraction_width[i, 0]
|
|
527
|
+
yt = ycen + self.extraction_width[i, 1]
|
|
528
|
+
xl, xr = self.column_range[i]
|
|
529
|
+
index = make_index(yb, yt, xl, xr)
|
|
530
|
+
yl = pos[i]
|
|
531
|
+
yr = pos[i] + index[0].shape[0]
|
|
532
|
+
output[yl:yr, xl:xr] = original[index]
|
|
533
|
+
pos += [yr]
|
|
534
|
+
|
|
535
|
+
vmin, vmax = np.percentile(output[output != 0], (5, 95))
|
|
536
|
+
plt.imshow(output, vmin=vmin, vmax=vmax, origin="lower", aspect="auto")
|
|
537
|
+
|
|
538
|
+
for i in range(self.nord):
|
|
539
|
+
for p in peaks[i]:
|
|
540
|
+
ew = self.extraction_width[i]
|
|
541
|
+
x = np.zeros(ew[0] + ew[1] + 1)
|
|
542
|
+
y = np.arange(-ew[0], ew[1] + 1)
|
|
543
|
+
for j, yt in enumerate(y):
|
|
544
|
+
x[j] = p + yt * tilt[i, p] + yt**2 * shear[i, p]
|
|
545
|
+
y += pos[i] + ew[0]
|
|
546
|
+
plt.plot(x, y, "r")
|
|
547
|
+
|
|
548
|
+
locs = np.sum(self.extraction_width, axis=1) + 1
|
|
549
|
+
locs = np.array([0, *np.cumsum(locs)[:-1]])
|
|
550
|
+
locs[:-1] += (np.diff(locs) * 0.5).astype(int)
|
|
551
|
+
locs[-1] += ((output.shape[0] - locs[-1]) * 0.5).astype(int)
|
|
552
|
+
|
|
553
|
+
plt.yticks(locs, range(len(locs)))
|
|
554
|
+
if self.plot_title is not None:
|
|
555
|
+
plt.title(self.plot_title)
|
|
556
|
+
plt.xlabel("x [pixel]")
|
|
557
|
+
plt.ylabel("order")
|
|
558
|
+
util.show_or_save("curvature_comparison")
|
|
559
|
+
|
|
560
|
+
def execute(self, extracted, original):
|
|
561
|
+
logger.info("Determining the Slit Curvature")
|
|
562
|
+
|
|
563
|
+
_, ncol = original.shape
|
|
564
|
+
|
|
565
|
+
self._fix_inputs(original)
|
|
566
|
+
|
|
567
|
+
if self.plot >= 2: # pragma: no cover
|
|
568
|
+
self.progress = ProgressPlot(ncol, self.window_width, title=self.plot_title)
|
|
569
|
+
|
|
570
|
+
peaks, tilt, shear, vec = self._determine_curvature_all_lines(
|
|
571
|
+
original, extracted
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
coef_tilt, coef_shear = self.fit(peaks, tilt, shear)
|
|
575
|
+
|
|
576
|
+
if self.plot >= 2: # pragma: no cover
|
|
577
|
+
self.progress.close()
|
|
578
|
+
|
|
579
|
+
if self.plot: # pragma: no cover
|
|
580
|
+
self.plot_results(ncol, peaks, vec, tilt, shear, coef_tilt, coef_shear)
|
|
581
|
+
|
|
582
|
+
iorder, ipeaks = np.indices(extracted.shape)
|
|
583
|
+
tilt, shear = self.eval(ipeaks, iorder, coef_tilt, coef_shear)
|
|
584
|
+
|
|
585
|
+
if self.plot: # pragma: no cover
|
|
586
|
+
self.plot_comparison(original, tilt, shear, peaks)
|
|
587
|
+
|
|
588
|
+
return tilt, shear
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
# TODO allow other line shapes
|
|
592
|
+
def gaussian(x, A, mu, sig):
|
|
593
|
+
"""
|
|
594
|
+
A: height
|
|
595
|
+
mu: offset from central line
|
|
596
|
+
sig: standard deviation
|
|
597
|
+
"""
|
|
598
|
+
return A * np.exp(-np.power(x - mu, 2.0) / (2 * np.power(sig, 2.0)))
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def lorentzian(x, A, x0, mu):
|
|
602
|
+
"""
|
|
603
|
+
A: height
|
|
604
|
+
x0: offset from central line
|
|
605
|
+
mu: width of lorentzian
|
|
606
|
+
"""
|
|
607
|
+
return A * mu / ((x - x0) ** 2 + 0.25 * mu**2)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|