Skip to content

Commit 05f4212

Browse files
authored
Merge pull request #2444 from desihub/traceshift2025b
Also scan for large trace shifts in continuum/LED exposures
2 parents e033a8c + d1ad42e commit 05f4212

File tree

4 files changed

+87
-16
lines changed

4 files changed

+87
-16
lines changed

bin/desi_ccd_xy

+19-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import numpy as np
44
import astropy.io.fits as pyfits
5+
from astropy.table import Table
56
import specter.psf
67
import sys
78
import argparse
@@ -18,16 +19,29 @@ def readpsf(filename) :
1819
return specter.psf.SpotGridPSF(filename)
1920

2021
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
21-
parser.add_argument('--psf', type = str, default = None, required = True,
22-
help = 'path of psf file')
22+
parser.add_argument('--psf', type = str, default = None, required = True, nargs = "*",
23+
help = 'path of psf files')
2324
parser.add_argument('--fiber', type = int, default = None, required = True,
2425
help = 'fiber for psf1')
2526
parser.add_argument('--wavelength', type = float, default = 6000., required = False,
2627
help = 'wavelength')
28+
parser.add_argument('-o','--outtable', type = str, default = None, required = False,
29+
help = 'output table')
2730

2831
args = parser.parse_args()
2932

33+
xx=[]
34+
yy=[]
35+
for filename in args.psf :
36+
psf=readpsf(filename)
37+
x,y=psf.xy(args.fiber%500,args.wavelength)
38+
print("x,y",x,y)
39+
xx.append(x)
40+
yy.append(y)
3041

31-
psf=readpsf(args.psf)
32-
xy=psf.xy(args.fiber%500,args.wavelength)
33-
print("xy=",xy)
42+
if args.outtable is not None :
43+
t=Table()
44+
t["X"]=np.array(xx)
45+
t["Y"]=np.array(yy)
46+
print(t)
47+
t.write(args.outtable,overwrite=True)

py/desispec/scripts/proc.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,7 @@ def main(args=None, comm=None):
558558
cmd += " --psf {}".format(inpsf)
559559
cmd += " --degxx 2 --degxy 0"
560560
if args.obstype in ['FLAT', 'TESTFLAT', 'TWILIGHT'] :
561-
cmd += " --continuum"
561+
cmd += " --continuum --no-large-shift-scan"
562562
else :
563563
cmd += " --degyx 2 --degyy 0"
564564
if args.obstype in ['SCIENCE', 'SKY']:

py/desispec/scripts/trace_shifts.py

+28-10
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from desispec.io import shorten_filename
2323
from desiutil.log import get_logger
2424
from desispec.util import parse_int_args
25-
from desispec.trace_shifts import write_traces_in_psf,compute_dx_from_cross_dispersion_profiles,compute_dy_from_spectral_cross_correlation,monomials,polynomial_fit,compute_dy_using_boxcar_extraction,compute_dx_dy_using_psf,shift_ycoef_using_external_spectrum,recompute_legendre_coefficients,recompute_legendre_coefficients_for_x,recompute_legendre_coefficients_for_y,list_of_expected_spot_positions
25+
from desispec.trace_shifts import write_traces_in_psf,compute_dx_from_cross_dispersion_profiles,compute_dy_from_spectral_cross_correlation,monomials,polynomial_fit,compute_dy_using_boxcar_extraction,compute_dx_dy_using_psf,shift_ycoef_using_external_spectrum,recompute_legendre_coefficients,recompute_legendre_coefficients_for_x,recompute_legendre_coefficients_for_y,list_of_expected_spot_positions,compute_x_offset_from_central_band_cross_dispersion_profile
2626
from desispec.large_trace_shifts import detect_spots_in_image,match_same_system
2727

2828
def parse(options=None):
@@ -60,7 +60,7 @@ def parse(options=None):
6060
parser.add_argument('--degyy', type = int, default = 0, required=False,
6161
help = 'polynomial degree for y shifts along y')
6262
parser.add_argument('--continuum', action='store_true',
63-
help = 'only fit shifts along x for continuum input image')
63+
help = 'only fit shifts along x for continuum or LED input image')
6464
parser.add_argument('--auto', action='store_true',
6565
help = 'choose best method (sky,continuum or just internal calib) from the FLAVOR keyword in the input image header')
6666

@@ -72,7 +72,8 @@ def parse(options=None):
7272
help = "width of cross-dispersion profile")
7373
parser.add_argument('--ccd-rows-rebin', type = int, default = 4 , required=False,
7474
help = "rebinning of CCD rows to run faster")
75-
75+
parser.add_argument('--no-large-shift-scan', action="store_true",
76+
help = "do not perform a large shift scan for arc lamp or continuum exposures")
7677
args = parser.parse_args(options)
7778

7879
return args
@@ -459,15 +460,21 @@ def fit_trace_shifts(image, args):
459460
args.sky = True
460461
log.info("wavelength calib, internal={}, sky={} , arc_lamps={}".format(internal_wavelength_calib,args.sky,args.arc_lamps))
461462

462-
if args.arc_lamps :
463+
cfinder = CalibFinder([image.meta])
464+
fibers=np.arange(tset.nspec)
465+
if cfinder.haskey("BROKENFIBERS") :
466+
brokenfibers=parse_int_args(cfinder.value("BROKENFIBERS"))%500
467+
log.debug(f"brokenfibers={brokenfibers}")
468+
fibers=fibers[np.isin(fibers, brokenfibers, invert=True)]
469+
470+
# for arc lamp images, we can predict where we expect to see spots on the CCD image
471+
# given an input traceset (variable tset), and use the comparison between the prediction
472+
# and actual spots locations to derive a first correction to the coordinates saved in the traceset
473+
# this is disabled by the option --no-large-shift-scan
474+
if args.arc_lamps and (not args.no_large_shift_scan) :
463475

464476
log.info("for arc lamps, find a first solution by comparing expected spots positions with detections over the whole CCD")
465-
cfinder = CalibFinder([image.meta])
466-
fibers=np.arange(tset.nspec)
467-
if cfinder.haskey("BROKENFIBERS") :
468-
brokenfibers=parse_int_args(cfinder.value("BROKENFIBERS"))%500
469-
log.debug(f"brokenfibers={brokenfibers}")
470-
fibers=fibers[np.isin(fibers, brokenfibers, invert=True)]
477+
471478
xref,yref = list_of_expected_spot_positions(tset,fibers)
472479
xin,yin = detect_spots_in_image(image)
473480

@@ -510,6 +517,16 @@ def fit_trace_shifts(image, args):
510517
xcoef[:,0] += delta_xref
511518
ycoef[:,0] += delta_yref
512519

520+
# for continuum images, we can predict where we expect to see spectral traces on the CCD image
521+
# given an input traceset (variable tset), and use the comparison between the prediction
522+
# and actual spectral traces to derive a first correction to the X coordinates saved in the traceset
523+
# this is disabled by the option --no-large-shift-scan
524+
if args.continuum and (not args.no_large_shift_scan) :
525+
log.info("for continuum or LED exposures, find a first solution on delta_X by comparing a wide cross-dispersion profile with expectations")
526+
delta_xref = compute_x_offset_from_central_band_cross_dispersion_profile(tset, image, fibers=fibers)
527+
log.info(f"apply best shift delta x = {delta_xref} to traceset")
528+
xcoef[:,0] += delta_xref
529+
513530
spectrum_filename = args.spectrum
514531
continuum_subtract = False
515532
if args.sky :
@@ -557,6 +574,7 @@ def fit_trace_shifts(image, args):
557574

558575
else :
559576

577+
560578
# internal calibration method that does not use the psf
561579
# nor a prior set of lines. this method is much faster
562580

py/desispec/trace_shifts.py

+39
Original file line numberDiff line numberDiff line change
@@ -1507,3 +1507,42 @@ def list_of_expected_spot_positions(traceset,fibers=None,max_number_of_lines=50)
15071507
xspot=np.hstack(xspot)
15081508
yspot=np.hstack(yspot)
15091509
return xspot,yspot
1510+
1511+
1512+
def compute_x_offset_from_central_band_cross_dispersion_profile(tset, image, fibers=None,halfwidth=50) :
1513+
log=get_logger()
1514+
bands=7 # measure delta_x in 7 horizontal bands of 100 pixel wide each and return the median offset
1515+
bandwidth=100
1516+
hb=bands//2
1517+
n0=image.pix.shape[0]
1518+
n1=image.pix.shape[1]
1519+
ivar=image.ivar*(image.mask==0)
1520+
if fibers is None :
1521+
fibers = np.arange(tset.nspec)
1522+
delta_x_array=[]
1523+
for b in range(-hb,hb+1) :
1524+
begin=int(n0//2+(b-0.5)*bandwidth)
1525+
end=begin+bandwidth
1526+
1527+
sw=np.sum(ivar[begin:end,:],axis=0)
1528+
swf=np.sum(image.pix[begin:end,:]*ivar[begin:end,:])
1529+
prof=(swf/(sw+(sw==0)))
1530+
y=int(n0//2+b*bandwidth)
1531+
xc=np.array([tset.x_vs_y(fiber,y) for fiber in fibers])
1532+
oversample=4
1533+
ox=np.arange(n1*oversample)/oversample
1534+
xx=np.arange(n1)
1535+
sigma=1.5 # pixel, approximate
1536+
mprof=np.exp(-(ox[:,None]-xc[None,:])**2/2./sigma**2).sum(axis=1)
1537+
norm = 1./np.sqrt(np.sum(prof**2)*(np.sum(mprof**2)/oversample))
1538+
ddx=np.linspace(-halfwidth,halfwidth,4*halfwidth+1)
1539+
xcorr=np.zeros(ddx.size)
1540+
for i,dx in enumerate(ddx) :
1541+
xcorr[i]=np.sum(prof*np.interp(xx,ox+dx,mprof))
1542+
xcorr *= norm
1543+
imax=np.argmax(xcorr)
1544+
log.info("horizontal band {}:{} delta x = {:.1f} pixels".format(begin,end,ddx[imax]))
1545+
delta_x_array.append(ddx[imax])
1546+
delta_x = np.median(delta_x_array)
1547+
log.info("median delta x = {:.1f} pixels".format(delta_x))
1548+
return delta_x

0 commit comments

Comments
 (0)