Coverage for python / lsst / obs / subaru / arcturus.py: 0%
150 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-23 08:51 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-23 08:51 +0000
1"""Process the scattered light data from Arcturus"""
3import numpy as np
4import multiprocessing
5try:
6 pyplot
7except NameError:
8 import matplotlib.pyplot as pyplot
9 pyplot.interactive(1)
11import lsst.afw.display as afwDisplay
12import lsst.analysis.utils as utils
14position = { # position of Arcturus on 16x16 binned "showCamera" images
15 906142: (8, 8),
16 906144: (8, 116),
17 906146: (6, 228),
18 906148: (5, 341),
19 906150: (5, 455),
20 906152: (8, 566),
21 906154: (9, 682),
22 906156: (8, 798),
23 906158: (8, 917),
24 906160: (8, 1039),
25 906162: (8, 1038 + 108),
26 906164: (8, 1038 + 2*108),
27 906186: (8, 1038 + 3*108),
28 906188: (8, 1038 + 4*108),
29 906190: (8, 1038 + 5*108),
30}
33def showFrames(mos, frame0=1, R=23, subtractSky=True):
34 visits = sorted(position.keys())
36 frame = frame0 - 1
37 for v in visits:
38 frame += 1
40 xc, yc = position[v]
41 xc -= mos[v].getX0()
42 yc -= mos[v].getY0()
44 im = mos[v]
45 if subtractSky:
46 im = im.clone()
47 im[2121:2230, 590:830] = np.nan # QE for this chip is bad
49 ima = im.getArray()
50 im[:] -= np.percentile(ima, 25)
52 disp = afwDisplay.Display(frame=frame)
53 disp.mtv(im, title=v)
54 disp.dot("o", xc, yc, size=R,
55 ctype=afwDisplay.GREEN if yc < mos[v].getHeight() else afwDisplay.RED)
58class MedianFilterImageWorker(object):
60 def __init__(self, mos, medianN):
61 self.mos = mos
62 self.medianN = medianN
64 def __call__(self, v):
65 print("Median filtering visit %d" % v)
66 return v, utils.medianFilterImage(self.mos[-v], self.medianN)
69def prepareFrames(mos, frame0=1, R=100, medianN=23, onlyVisits=[], nJob=1, force=False):
70 """Prepare frames to have their radial profiles measured.
71Subtract the first quartile
72Set a radius R circle about the centre of Arcturus to NaN
73Set a chip with a bad QE value to NaN
74Median filter with a medianN x medianN filter
76The results are set as mos[-visit]
78If onlyVisits is specified, only process those chips [n.b. frame0 is still
79obeyed]; otherwise process every visit in the positions dict
80 """
81 visits = sorted(position.keys())
83 visits0 = visits[:] # needed to keep frame numbers aligned
84 if not force:
85 visits = [v for v in visits if -v not in mos] # don't process ones we've already seen
86 if onlyVisits:
87 visits = [v for v in visits if v in onlyVisits]
89 width, height = mos[visits[0]].getDimensions()
90 X, Y = np.meshgrid(np.arange(width), np.arange(height))
92 for v in visits:
93 im = mos[v].clone()
94 mos[-v] = im
95 im[2121:2230, 590:830] = np.nan # QE for this chip is bad
97 ima = im.getArray()
98 im[:] -= np.percentile(ima, 25)
100 xc, yc = position[v]
101 xc -= im.getX0()
102 yc -= im.getY0()
104 ima[np.hypot(X - xc, Y - yc) < R] = np.nan
105 #
106 # multiprocessing
107 #
108 worker = MedianFilterImageWorker(mos, medianN)
109 if nJob > 1:
110 pool = multiprocessing.Pool(max([len(visits), nJob]))
112 # We use map_async(...).get(9999) instead of map(...) to workaround
113 # a python but in handling ^C in subprocesses
114 # (http://bugs.python.org/issue8296)
115 for v, im in pool.map_async(worker, visits).get(9999):
116 mos[-v] = im
118 pool.close()
119 pool.join()
120 else:
121 for v in visits:
122 mos[-v] = worker(v)
123 #
124 # Display
125 #
126 frame = frame0 - 1
127 for v in visits0:
128 frame += 1
130 if v not in visits:
131 continue
133 im = mos[-v]
135 disp = afwDisplay.Display(frame=frame)
136 if True:
137 disp.mtv(im, title=v)
138 else:
139 disp.erase()
141 xc, yc = position[v]
142 xc -= im.getX0()
143 yc -= im.getY0()
145 disp.dot("o", xc, yc, size=R, ctype=afwDisplay.GREEN if yc < im.getHeight() else afwDisplay.RED)
148def OLDprepareFrames(mos, frame0=1, R=23, medianN=23, onlyVisits=[], force=False):
149 """Prepare frames to have their radial profiles measured.
151 #. Subtract the first quartile
152 #. Set a radius R circle about the centre of Arcturus to NaN
153 #. Set a chip with a bad QE value to NaN
154 #. Median filter with a medianN x medianN filter
156 The result is set as mos[-visit]
158 If onlyVisits is specified, only process those chips [n.b. frame0 is still
159 obeyed]
160 """
162 visits = sorted(position.keys())
164 width, height = mos[visits[0]].getDimensions()
166 X, Y = np.meshgrid(np.arange(width), np.arange(height))
168 frame = frame0 - 1
169 for v in visits:
170 frame += 1
171 if onlyVisits and v not in onlyVisits:
172 continue
174 print("Processing %d" % v)
176 im = mos[v].clone()
177 im[2121:2230, 590:830] = np.nan # QE for this chip is bad
179 ima = im.getArray()
180 im[:] -= np.percentile(ima, 25)
182 xc, yc = position[v]
183 xc -= im.getX0()
184 yc -= im.getY0()
186 ima[np.hypot(X - xc, Y - yc) < R] = np.nan
188 if force or -v not in mos:
189 im = utils.medianFilterImage(im, medianN)
190 mos[-v] = im
191 else:
192 im = mos[-v]
194 disp = afwDisplay.Display(frame=frame)
195 if True:
196 disp.mtv(im, title=v)
197 else:
198 disp.erase()
199 disp.dot("o", xc, yc, size=R, ctype=afwDisplay.GREEN if yc < im.getHeight() else afwDisplay.RED)
202def annularAverage(im, nBin=100, rmin=None, rmax=None, median=False):
203 """Return image im's radial profile (more accurately the tuple
204 (rbar, prof), binned into nBin bins.
206 r is measured from (0, 0) in the image, accounting properly for XY0
208 If rmax is provided it's the maximum value of r to consider
209 """
211 width, height = im.getDimensions()
213 if not rmax:
214 rmax = 0.5*min(width, height)
216 bins = np.linspace(0, rmax, nBin)
217 binWidth = bins[1] - bins[0]
219 rbar = np.empty_like(bins)
220 prof = np.empty_like(bins)
222 X, Y = np.meshgrid(np.arange(width), np.arange(height))
223 R = np.hypot(X + im.getX0(), Y + im.getY0()).flatten()
224 imProf = im.getArray().flatten()
226 if rmin:
227 imProf = imProf[R > rmin]
228 R = R[R > rmin]
230 for i in range(len(bins)):
231 inBin = np.abs(R - bins[i]) < 0.5*binWidth
232 rbar[i] = np.mean(R[inBin])
233 if np.sum(inBin):
234 ivals = imProf[inBin]
235 if median:
236 val = np.median(ivals)
237 else:
238 val = np.mean(ivals[np.isfinite(ivals)])
239 else:
240 val = 0
242 prof[i] = val
244 return rbar, prof
247def plotAnnularAverages(mos, visits, bin=16, useSmoothed=True, weightByR=True, showSum=True, **kwargs):
248 pyplot.clf()
249 #
250 # Find the spacing between exposures of Arcturus
251 #
252 allVisits = sorted(position.keys())
253 spacing = np.hypot(*position[allVisits[1]]) - np.hypot(*position[allVisits[0]])
255 sumProf = None
256 for v in visits:
257 rbar, prof = annularAverage(mos[-v if useSmoothed else v], **kwargs)
258 rbar *= bin
259 prof *= spacing*bin # scale up as if we had exposures of Arcturus with 1 pixel steps
260 #
261 # Allow for the extra phase space further away from the boresight
262 if weightByR:
263 r = bin*(np.hypot(*position[v]) + 0.3*spacing) # really a geometric mean?
264 prof *= 2*np.pi*r
266 print("%d %07.2g" % (v, np.sum(prof)))
268 if showSum:
269 if sumProf is None:
270 sumProf = np.zeros_like(prof)
272 sumProf += prof
274 pyplot.plot(rbar, prof, label=str(v))
276 if sumProf is not None:
277 pyplot.plot(rbar, sumProf, label="sum")
279 legend = pyplot.legend(loc="best", ncol=2,
280 borderpad=0.1, labelspacing=0, handletextpad=0, handlelength=2, borderaxespad=0)
282 if legend:
283 legend.draggable(True)
285 pyplot.show()