• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

desihub / desispec / 19150128419

06 Nov 2025 09:17PM UTC coverage: 37.704% (+0.7%) from 37.002%
19150128419

Pull #2521

github

web-flow
Merge 2934f9b9b into 6a90a0547
Pull Request #2521: Add redshift QA scripts

12985 of 34439 relevant lines covered (37.7%)

0.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.45
/py/desispec/scripts/stdstars.py
1
"""
2
desispec.scripts.stdstars
3
=========================
4

5
Get the normalized best template to do flux calibration.
6
"""
7

8
#- TODO: refactor algorithmic code into a separate module/function
9

10
import argparse
1✔
11
import sys
1✔
12

13
import numpy as np
1✔
14
from astropy.io import fits
1✔
15
from astropy import units
1✔
16
from astropy.table import Table
1✔
17
import astropy.coordinates as acoo
1✔
18

19
import desispec.fluxcalibration
1✔
20
from desispec import io
1✔
21
from desispec.fluxcalibration import match_templates,normalize_templates,isStdStar
1✔
22
from desispec.interpolation import resample_flux
1✔
23
from desiutil.log import get_logger
1✔
24
from desispec.parallel import default_nproc
1✔
25
from desispec.io.filters import load_legacy_survey_filter, load_gaia_filter
1✔
26
from desiutil.dust import dust_transmission,extinction_total_to_selective_ratio, SFDMap, gaia_extinction
1✔
27
from desispec.fiberbitmasking import get_fiberbitmasked_frame
1✔
28

29
from desispec.fiberflat import apply_fiberflat
1✔
30
from desispec.sky import subtract_sky
1✔
31

32
def parse(options=None):
1✔
33
    parser = argparse.ArgumentParser(description="Fit of standard star spectra in frames.")
1✔
34
    parser.add_argument('--frames', type = str, default = None, required=True, nargs='*',
1✔
35
                        help = 'list of path to DESI frame fits files (needs to be same exposure, spectro)')
36
    parser.add_argument('--skymodels', type = str, default = None, required=True, nargs='*',
1✔
37
                        help = 'list of path to DESI sky model fits files (needs to be same exposure, spectro)')
38
    parser.add_argument('--fiberflats', type = str, default = None, required=True, nargs='*',
1✔
39
                        help = 'list of path to DESI fiberflats fits files (needs to be same exposure, spectro)')
40
    parser.add_argument('--starmodels', type = str, help = 'path of spectro-photometric stellar spectra fits')
1✔
41
    parser.add_argument('-o','--outfile', type = str, help = 'output file for normalized stdstar model flux')
1✔
42
    parser.add_argument('--ncpu', type = int, default = default_nproc, required = False, help = 'use ncpu for multiprocessing')
1✔
43
    parser.add_argument('--delta-color', type = float, default = 0.2, required = False, help = 'max delta-color for the selection of standard stars (on top of meas. errors)')
1✔
44
    parser.add_argument('--min-blue-snr', type = float, default = 4.0, required = False,
1✔
45
            help = 'Minimum required S/N in blue CCD to be used')
46
    parser.add_argument('--color', type = str, default = None, choices=['G-R', 'R-Z', 'GAIA-BP-RP','GAIA-G-RP'], required = False, help = 'color for selection of standard stars')
1✔
47
    parser.add_argument('--z-max', type = float, default = 0.008, required = False, help = 'max peculiar velocity (blue/red)shift range')
1✔
48
    parser.add_argument('--z-res', type = float, default = 0.00002, required = False, help = 'dz grid resolution')
1✔
49
    parser.add_argument('--template-error', type = float, default = 0.1, required = False, help = 'fractional template error used in chi2 computation (about 0.1 for BOSS b1)')
1✔
50
    parser.add_argument('--maxstdstars', type=int, default=30, \
1✔
51
            help='Maximum number of stdstars to include')
52
    parser.add_argument('--std-targetids', type=int, default=None,
1✔
53
                         nargs='*',
54
                         help='List of TARGETIDs of standards overriding the targeting info')
55
    parser.add_argument('--mpi', action='store_true', help='Use MPI')
1✔
56
    parser.add_argument('--apply-sky-throughput-correction', action='store_true',
1✔
57
                        help =('Apply a throughput correction when subtraction the sky '
58
                               '(default: do not apply!)'))
59
    log = get_logger()
1✔
60

61
    args = parser.parse_args(options)
1✔
62

63
    if options is None:
1✔
64
        cmd = ' '.join(sys.argv)
×
65
    else:
66
        cmd = 'desi_fit_stdstars ' + ' '.join(options)
1✔
67

68
    log.info('RUNNING {}'.format(cmd))
1✔
69

70
    return args
1✔
71

72
def safe_read_key(header,key) :
1✔
73
    value = None
1✔
74
    try :
1✔
75
        value=header[key]
1✔
76
    except KeyError :
×
77
        value = None
×
78
        pass
×
79
    if value is None : # second try
1✔
80
        value=header[key.ljust(8).upper()]
×
81
    return value
1✔
82

83
def get_gaia_ab_correction():
1✔
84
    """
85
Get the dictionary with corrections from AB magnitudes to
86
Vega magnitudes (as the official gaia catalog is in vega)
87
"""
88
    vega_zpt = dict(G=25.6914396869,
1✔
89
                    BP=25.3488107670,
90
                    RP=24.7626744847)
91
    ab_zpt=dict(G=25.7915509947,
1✔
92
                BP=25.3861560855,
93
                RP=25.1161664528)
94
    # revised dr2 zpts from https://www.cosmos.esa.int/web/gaia/iow_20180316
95
    ret = {}
1✔
96
    for k in vega_zpt.keys():
1✔
97
        ret['GAIA-'+k] = vega_zpt[k] - ab_zpt[k]
1✔
98
    # these corrections need to be added to convert
99
    # the simulated ab into vega
100
    return ret
1✔
101

102
def get_magnitude(stdwave, model, model_filters, cur_filt):
1✔
103
    """ Obtain magnitude for a filter taking into
104
account the ab/vega correction if needed.
105
Wwe assume the flux is in units of 1e-17 erg/s/cm^2/A
106
    """
107
    fluxunits = 1e-17 * units.erg / units.s / units.cm**2 / units.Angstrom
1✔
108

109
    # AB/Vega correction
110
    if cur_filt[:5] == 'GAIA-':
1✔
111
        corr = get_gaia_ab_correction()[cur_filt]
1✔
112
    else:
113
        corr = 0
1✔
114
    if not(cur_filt in model_filters):
1✔
115
        raise Exception(('Filter {} is not present in models').format(cur_filt))
×
116
    # see https://github.com/desihub/speclite/issues/34
117
    # to explain copy()
118
    retmag = model_filters[cur_filt].get_ab_magnitude(model * fluxunits, stdwave.copy())+ corr
1✔
119
    return retmag
1✔
120

121
def main(args=None, comm=None) :
1✔
122
    """ finds the best models of all standard stars in the frame
123
    and normlize the model flux. Output is written to a file and will be called for calibration.
124
    """
125

126
    if not isinstance(args, argparse.Namespace):
1✔
127
        args = parse(args)
×
128

129
    log = get_logger()
1✔
130

131
    log.info("mag delta %s = %f (for the pre-selection of stellar models)"%(args.color,args.delta_color))
1✔
132

133
    if args.mpi or comm is not None:
1✔
134
        from mpi4py import MPI
×
135
        if comm is None:
×
136
            comm = MPI.COMM_WORLD
×
137
        size = comm.Get_size()
×
138
        rank = comm.Get_rank()
×
139
        if rank == 0:
×
140
            log.info('mpi parallelizing with {} ranks'.format(size))
×
141
    else:
142
        comm = None
1✔
143
        rank = 0
1✔
144
        size = 1
1✔
145

146
    # disable multiprocess by forcing ncpu = 1 when using MPI
147
    if comm is not None:
1✔
148
        ncpu = 1
×
149
        if rank == 0:
×
150
            log.info('disabling multiprocess (forcing ncpu = 1)')
×
151
    else:
152
        ncpu = args.ncpu
1✔
153

154
    if ncpu > 1:
1✔
155
        if rank == 0:
1✔
156
            log.info('multiprocess parallelizing with {} processes'.format(ncpu))
1✔
157

158
    # READ DATA
159
    ############################################
160
    # First loop through and group by exposure and spectrograph
161
    frames_by_expid = {}
1✔
162
    rows = list()
1✔
163
    for filename in args.frames :
1✔
164
        log.info("reading %s"%filename)
1✔
165
        frame=io.read_frame(filename)
1✔
166
        night = safe_read_key(frame.meta,"NIGHT")
1✔
167
        expid = safe_read_key(frame.meta,"EXPID")
1✔
168
        camera = safe_read_key(frame.meta,"CAMERA").strip().lower()
1✔
169
        rows.append( (night, expid, camera) )
1✔
170
        spec = camera[1]
1✔
171
        uniq_key = (expid,spec)
1✔
172

173
        # To save memory, trim to just stdstars as each frame is read;
174
        # more quality cuts will be applied later
175
        if args.std_targetids is None:
1✔
176
            keep = isStdStar(frame.fibermap)
1✔
177
        else:
178
            keep = np.isin(frame.fibermap['TARGETID'], args.std_targetids)
1✔
179

180
        frame = frame[keep]
1✔
181

182
        if uniq_key in frames_by_expid.keys():
1✔
183
            frames_by_expid[uniq_key][camera] = frame
×
184
        else:
185
            frames_by_expid[uniq_key] = {camera: frame}
1✔
186

187
    input_frames_table = Table(rows=rows, names=('NIGHT', 'EXPID', 'CAMERA'))
1✔
188

189
    frames={}
1✔
190
    flats={}
1✔
191
    skies={}
1✔
192

193
    spectrograph=None
1✔
194
    starfibers=None
1✔
195
    fibermap=None
1✔
196
    # For each unique expid,spec pair, get the logical OR of the FIBERSTATUS for all
197
    # cameras and then proceed with extracting the frame information
198
    # once we modify the fibermap FIBERSTATUS
199
    for (expid,spec),camdict in frames_by_expid.items():
1✔
200

201
        fiberstatus = None
1✔
202
        for frame in camdict.values():
1✔
203
            if fiberstatus is None:
1✔
204
                fiberstatus = frame.fibermap['FIBERSTATUS'].data.copy()
1✔
205
            else:
206
                fiberstatus |= frame.fibermap['FIBERSTATUS']
×
207

208
        for camera,frame in camdict.items():
1✔
209
            frame.fibermap['FIBERSTATUS'] |= fiberstatus
1✔
210
            # Set fibermask flagged spectra to have 0 flux and variance
211
            frame = get_fiberbitmasked_frame(frame,bitmask='stdstars',ivar_framemask=True)
1✔
212
            frame_fibermap = frame.fibermap
1✔
213

214
            #- Confirm that all fluxes have entries but trust targeting bits
215
            #- to get basic magnitude range correct
216
            keep_legacy = np.ones(len(frame_fibermap), dtype=bool)
1✔
217
            for colname in ['FLUX_G', 'FLUX_R', 'FLUX_Z']:  #- and W1 and W2?
1✔
218
                keep_legacy &= frame_fibermap[colname] > 10**((22.5-30)/2.5)
1✔
219
                keep_legacy &= frame_fibermap[colname] < 10**((22.5-0)/2.5)
1✔
220

221
            keep_gaia = np.ones(len(frame_fibermap), dtype=bool)
1✔
222
            for colname in ['G', 'BP', 'RP']:
1✔
223
                keep_gaia &= frame_fibermap['GAIA_PHOT_'+colname+'_MEAN_MAG'] > 10
1✔
224
                keep_gaia &= frame_fibermap['GAIA_PHOT_'+colname+'_MEAN_MAG'] < 20
1✔
225

226
            n_legacy_std = keep_legacy.sum()
1✔
227
            n_gaia_std = keep_gaia.sum()
1✔
228

229
            if spectrograph is None :
1✔
230
                spectrograph = frame.spectrograph
1✔
231
                fibermap = frame_fibermap
1✔
232
                starfibers=np.asarray(fibermap["FIBER"])
1✔
233

234
            elif spectrograph != frame.spectrograph :
×
235
                log.error("incompatible spectrographs {} != {}".format(spectrograph,frame.spectrograph))
×
236
                raise ValueError("incompatible spectrographs {} != {}".format(spectrograph,frame.spectrograph))
×
237
            elif len(fibermap) != len(frame_fibermap) or np.any(fibermap['FIBER'] != frame_fibermap['FIBER']):
×
238
                log.error("incompatible fibermap")
×
239
                raise ValueError("incompatible fibermap")
×
240

241
            if not camera in frames :
1✔
242
                frames[camera]=[]
1✔
243

244
            frames[camera].append(frame)
1✔
245

246
    # possibly cleanup memory
247
    del frames_by_expid
1✔
248

249
    # wait for all ranks to finish reading and trimming before reading more
250
    if comm is not None:
1✔
251
        comm.barrier()
×
252

253
    #- Read sky models and fiberflats, also trimming to starfibers kept for frames
254
    for filename in args.skymodels :
1✔
255
        log.info("reading %s"%filename)
1✔
256
        sky=io.read_sky(filename)
1✔
257
        camera=safe_read_key(sky.header,"CAMERA").strip().lower()
1✔
258
        if not camera in skies :
1✔
259
            skies[camera]=[]
1✔
260
        skies[camera].append(sky[starfibers%500])
1✔
261

262
    for filename in args.fiberflats :
1✔
263
        log.info("reading %s"%filename)
1✔
264
        flat=io.read_fiberflat(filename)
1✔
265
        camera=safe_read_key(flat.header,"CAMERA").strip().lower()
1✔
266

267
        # NEED TO ADD MORE CHECKS
268
        if camera in flats:
1✔
269
            log.warning("cannot handle several flats of same camera (%s), will use only the first one"%camera)
×
270
            #raise ValueError("cannot handle several flats of same camera (%s)"%camera)
271
        else :
272
            flats[camera]=flat[starfibers%500]
1✔
273

274
    # if color is not specified we decide on the fly
275
    color = args.color
1✔
276
    if color is not None:
1✔
277
        if color[:4] == 'GAIA':
1✔
278
            legacy_color  = False
1✔
279
            gaia_color = True
1✔
280
        else:
281
            legacy_color = True
1✔
282
            gaia_color = False
1✔
283
        if n_legacy_std == 0 and legacy_color:
1✔
284
            raise Exception('Specified Legacy survey color, but no legacy standards')
×
285
        if n_gaia_std == 0 and gaia_color:
1✔
286
            raise Exception('Specified gaia color, but no gaia stds')
×
287

288
    if starfibers.size == 0:
1✔
289
        log.error("no STD star found in fibermap")
×
290
        raise ValueError("no STD star found in fibermap")
×
291
    log.info("found %d STD stars" % starfibers.size)
1✔
292

293
    if n_legacy_std == 0:
1✔
294
        gaia_std = True
1✔
295
        if color is None:
1✔
296
            color = 'GAIA-BP-RP'
1✔
297
    else:
298
        gaia_std = False
1✔
299
        if color is None:
1✔
300
            color='G-R'
1✔
301
        if n_gaia_std > 0:
1✔
302
            log.info('Gaia standards found but not used')
1✔
303

304
    if gaia_std:
1✔
305
        # The name of the reference filter to which we normalize the flux
306
        ref_mag_name = 'GAIA-G'
1✔
307
        color_band1, color_band2  = ['GAIA-'+ _ for _ in color[5:].split('-')]
1✔
308
        log.info("Using Gaia standards with color {} and normalizing to {}".format(color, ref_mag_name))
1✔
309
        # select appropriate subset of standards
310
        keep_stds = keep_gaia
1✔
311
    else:
312
        ref_mag_name = 'R'
1✔
313
        color_band1, color_band2  = color.split('-')
1✔
314
        log.info("Using Legacy standards with color {} and normalizing to {}".format(color, ref_mag_name))
1✔
315
        # select appropriate subset of standards
316
        keep_stds = keep_legacy
1✔
317

318
    if np.sum(keep_stds) != len(keep_stds):
1✔
319
        log.warning('sp{} has {}/{} standards with good photometry'.format(
×
320
            spectrograph, np.sum(keep_stds), len(keep_stds)))
321

322
    starfibers = starfibers[keep_stds]
1✔
323
    fibermap   = fibermap[keep_stds]
1✔
324
    assert(len(fibermap)==len(starfibers)) # check same size
1✔
325
    assert(len(fibermap)==np.sum(keep_stds)) # test for funny astropy Table issue
1✔
326
    assert(np.all(fibermap['FIBER']==starfibers)) # same order
1✔
327

328
    log.info(f'sp{spectrograph} stdstar fibers {starfibers}')
1✔
329

330
    # excessive check but just in case
331
    if not color in ['G-R', 'R-Z', 'GAIA-BP-RP', 'GAIA-G-RP']:
1✔
332
        raise ValueError('Unknown color {}'.format(color))
×
333

334
    # log.warning("Not using flux errors for Standard Star fits!")
335

336
    # DIVIDE FLAT AND SUBTRACT SKY , TRIM DATA
337
    ############################################
338
    # since poping dict, we need to copy keys to iterate over to avoid
339
    # RuntimeError due to changing dict
340
    frame_cams = list(frames.keys())
1✔
341
    for cam in frame_cams:
1✔
342

343
        if not cam in skies:
1✔
344
            log.warning("Missing sky for %s"%cam)
×
345
            frames.pop(cam)
×
346
            continue
×
347
        if not cam in flats:
1✔
348
            log.warning("Missing flat for %s"%cam)
×
349
            frames.pop(cam)
×
350
            continue
×
351

352
        flat = flats[cam][keep_stds]
1✔
353

354
        for i in range(len(frames[cam])):
1✔
355
            frame = frames[cam][i][keep_stds]
1✔
356
            sky = skies[cam][i][keep_stds]
1✔
357

358
            #- don't use masked or ivar=0 data
359
            frame.ivar *= (frame.mask == 0)
1✔
360
            frame.ivar *= (sky.ivar != 0)
1✔
361
            frame.ivar *= (sky.mask == 0)
1✔
362
            frame.ivar *= (flat.ivar != 0)
1✔
363
            frame.ivar *= (flat.mask == 0)
1✔
364
            frame.flux *= (frame.ivar > 0) # just for clean plots
1✔
365

366
            apply_fiberflat(frame, flat)
1✔
367
            subtract_sky(frame, sky, apply_throughput_correction = args.apply_sky_throughput_correction)
1✔
368

369
            #- keep newly flat-fielded sky-subtracted frame
370
            frames[cam][i] = frame
1✔
371

372
        nframes=len(frames[cam])
1✔
373
        if nframes>1 :
1✔
374
            # optimal weights for the coaddition = ivar*throughput, not directly ivar,
375
            # we estimate the relative throughput with median fluxes at this stage
376
            medflux=np.zeros(nframes)
×
377
            for i,frame in enumerate(frames[cam]) :
×
378
                if np.sum(frame.ivar>0) == 0 :
×
379
                    log.error("ivar=0 for all std star spectra in frame {}-{:08d}".format(cam,frame.meta["EXPID"]))
×
380
                else :
381
                    medflux[i] = np.median(frame.flux[frame.ivar>0])
×
382
            log.debug("medflux = {}".format(medflux))
×
383
            medflux *= (medflux>0)
×
384
            if np.sum(medflux>0)==0 :
×
385
               log.error("mean median flux = 0, for all stars in fibers {}".format(list(frames[cam][0].fibermap["FIBER"])))
×
386
               sys.exit(12)
×
387
            mmedflux = np.mean(medflux[medflux>0])
×
388
            weights=medflux/mmedflux
×
389
            log.info("coadding {} exposures in cam {}, w={}".format(nframes,cam,weights))
×
390

391
            sw=np.zeros(frames[cam][0].flux.shape)
×
392
            swf=np.zeros(frames[cam][0].flux.shape)
×
393
            swr=np.zeros(frames[cam][0].resolution_data.shape)
×
394

395
            for i,frame in enumerate(frames[cam]) :
×
396
                sw  += weights[i]*frame.ivar
×
397
                swf += weights[i]*frame.ivar*frame.flux
×
398
                swr += weights[i]*frame.ivar[:,None,:]*frame.resolution_data
×
399
            coadded_frame = frames[cam][0]
×
400
            coadded_frame.ivar = sw
×
401
            coadded_frame.flux = swf/(sw+(sw==0))
×
402
            coadded_frame.resolution_data = swr/((sw+(sw==0))[:,None,:])
×
403
            frames[cam] = [ coadded_frame ]
×
404

405
    # We're done with skies and flats dict; remove them to possibly save memory
406
    del skies
1✔
407
    del flats
1✔
408

409
    # Double check indexing
410
    assert np.all(fibermap['FIBER'] == starfibers)
1✔
411
    for cam in frames:
1✔
412
        for frame in frames[cam]:
1✔
413
            assert np.all(frame.fibermap['FIBER'] == starfibers)
1✔
414

415
    # CHECK S/N
416
    ############################################
417
    # for each band in 'brz', record quadratic sum of median S/N across wavelength
418
    snr=dict()
1✔
419
    for band in ['b','r','z'] :
1✔
420
        snr[band]=np.zeros(starfibers.size)
1✔
421
    for cam in frames :
1✔
422
        band=cam[0].lower()
1✔
423
        for frame in frames[cam] :
1✔
424
            msnr = np.median( frame.flux * np.sqrt( frame.ivar ) / np.sqrt(np.gradient(frame.wave)) , axis=1 ) # median SNR per sqrt(A.)
1✔
425
            msnr *= (msnr>0)
1✔
426
            snr[band] = np.sqrt( snr[band]**2 + msnr**2 )
1✔
427
    log.info("SNR(B) = {}".format(snr['b']))
1✔
428

429
    # Sort and trim by blue S/N
430
    indices=np.argsort(snr['b'])[::-1][:args.maxstdstars]
1✔
431
    validstars = np.where(snr['b'][indices]>args.min_blue_snr)[0]
1✔
432

433
    #- TODO: later we filter on models based upon color, thus throwing
434
    #- away very blue stars for which we don't have good models.
435

436
    log.info("Number of stars with median stacked blue S/N > {} /sqrt(A) = {}".format(args.min_blue_snr,validstars.size))
1✔
437
    if validstars.size == 0 :
1✔
438
        log.error(f"No valid star for sp{spectrograph}")
×
439
        sys.exit(12)
×
440

441
    validstars = indices[validstars]
1✔
442

443
    for band in ['b','r','z'] :
1✔
444
        snr[band]=snr[band][validstars]
1✔
445

446
    log.info("BLUE SNR of selected stars={}".format(snr['b']))
1✔
447

448
    for cam in frames :
1✔
449
        for i in range(len(frames[cam])) :
1✔
450
            frames[cam][i] = frames[cam][i][validstars]
1✔
451

452
    starfibers  = starfibers[validstars]
1✔
453
    fibermap = Table(fibermap[validstars])
1✔
454
    nstars = starfibers.size
1✔
455

456
    # Check indexing yet again
457
    assert np.all(fibermap['FIBER'] == starfibers)
1✔
458
    for cam in frames:
1✔
459
        for frame in frames[cam]:
1✔
460
            assert np.all(frame.fibermap['FIBER'] == starfibers)
1✔
461

462
    # MASK OUT THROUGHPUT DIP REGION
463
    ############################################
464
    mask_throughput_dip_region = True
1✔
465
    if mask_throughput_dip_region :
1✔
466
        wmin=4300.
1✔
467
        wmax=4500.
1✔
468
        log.warning("Masking out the wavelength region [{},{}]A in the standard star fit".format(wmin,wmax))
1✔
469
    for cam in frames :
1✔
470
        for frame in frames[cam] :
1✔
471
            ii=np.where( (frame.wave>=wmin)&(frame.wave<=wmax) )[0]
1✔
472
            if ii.size>0 :
1✔
473
                frame.ivar[:,ii] = 0
×
474

475
    # READ MODELS
476
    ############################################
477

478
    log.info("reading star models in %s"%args.starmodels)
1✔
479
    stdwave,stdflux,templateid,teff,logg,feh=io.read_stdstar_templates(args.starmodels)
1✔
480

481
    # COMPUTE MAGS OF MODELS FOR EACH STD STAR MAG
482
    ############################################
483

484
    #- Support older fibermaps
485
    if 'PHOTSYS' not in fibermap.colnames:
1✔
486
        log.warning('Old fibermap format; using defaults for missing columns')
×
487
        log.warning("    PHOTSYS = 'S'")
×
488
        log.warning("    EBV = 0.0")
×
489
        fibermap['PHOTSYS'] = 'S'
×
490
        fibermap['EBV'] = 0.0
×
491

492
    if not np.isin(np.unique(fibermap['PHOTSYS']),['','N','S','G']).all():
1✔
493
        log.error('Unknown PHOTSYS found')
×
494
        raise Exception('Unknown PHOTSYS found')
×
495
    # Fetching Filter curves
496
    model_filters = dict()
1✔
497
    for band in ["G","R","Z"] :
1✔
498
        for photsys in np.unique(fibermap['PHOTSYS']) :
1✔
499
            if photsys in ['N','S']:
1✔
500
                model_filters[band+photsys] = load_legacy_survey_filter(band=band,photsys=photsys)
1✔
501
    if len(model_filters) == 0:
1✔
502
        log.info('No Legacy survey photometry identified in fibermap')
1✔
503

504
    # I will always load gaia data even if we are fitting LS standards only
505
    for band in ["G", "BP", "RP"] :
1✔
506
        model_filters["GAIA-" + band] = load_gaia_filter(band=band, dr=2)
1✔
507

508
    # Compute model mags on rank 0 and bcast result to other ranks
509
    # This sidesteps an OOM event on Cori Haswell with "-c 2"
510
    model_mags = None
1✔
511
    if rank == 0:
1✔
512
        log.info("computing model mags for %s"%sorted(model_filters.keys()))
1✔
513
        model_mags = dict()
1✔
514
        for filter_name in model_filters.keys():
1✔
515
            model_mags[filter_name] = get_magnitude(stdwave, stdflux, model_filters, filter_name)
1✔
516
        log.info("done computing model mags")
1✔
517

518
    if comm is not None:
1✔
519
        model_mags = comm.bcast(model_mags, root=0)
×
520

521
    # LOOP ON STARS TO FIND BEST MODEL
522
    ############################################
523
    star_mags = dict()
1✔
524
    star_unextincted_mags = dict()
1✔
525

526
    if gaia_std and (fibermap['EBV']==0).all():
1✔
527
        log.info("Using E(B-V) from SFD rather than FIBERMAP")
×
528
        # when doing gaia standards, on old tiles the
529
        # EBV is not set so we fetch from SFD (in original SFD scaling)
530
        ebv = SFDMap(scaling=1).ebv(acoo.SkyCoord(
×
531
            ra = fibermap['TARGET_RA'] * units.deg,
532
            dec = fibermap['TARGET_DEC'] * units.deg))
533
    else:
534
        ebv = fibermap['EBV']
1✔
535

536
    photometric_systems = np.unique(fibermap['PHOTSYS'])
1✔
537
    if not gaia_std:
1✔
538
        for band in ['G', 'R', 'Z']:
1✔
539
            star_mags[band] = 22.5 - 2.5 * np.log10(fibermap['FLUX_'+band])
1✔
540
            star_unextincted_mags[band] = np.zeros(star_mags[band].shape)
1✔
541
            for photsys in  photometric_systems :
1✔
542
                r_band = extinction_total_to_selective_ratio(band , photsys) # dimensionless
1✔
543
                # r_band = a_band / E(B-V)
544
                # E(B-V) is a difference of magnitudes (dimensionless)
545
                # a_band = -2.5*log10(effective dust transmission) , dimensionless
546
                # effective dust transmission =
547
                #                  integral( SED(lambda) * filter_transmission(lambda,band) * dust_transmission(lambda,E(B-V)) dlamdba)
548
                #                / integral( SED(lambda) * filter_transmission(lambda,band) dlamdba)
549
                selection = (fibermap['PHOTSYS'] == photsys)
1✔
550
                a_band = r_band * ebv[selection]  # dimensionless
1✔
551
                star_unextincted_mags[band][selection] = 22.5 - 2.5 * np.log10(fibermap['FLUX_'+band][selection]) - a_band
1✔
552

553
    for band in ['G','BP','RP']:
1✔
554
        star_mags['GAIA-'+band] = fibermap['GAIA_PHOT_'+band+'_MEAN_MAG']
1✔
555

556
    for band, extval in gaia_extinction(star_mags['GAIA-G'],
1✔
557
                                        star_mags['GAIA-BP'],
558
                                        star_mags['GAIA-RP'], ebv).items():
559
        star_unextincted_mags['GAIA-'+band] = star_mags['GAIA-'+band] - extval
1✔
560

561

562
    star_colors = dict()
1✔
563
    star_unextincted_colors = dict()
1✔
564

565
    # compute the colors and define the unextincted colors
566
    # the unextincted colors are filled later
567
    if not gaia_std:
1✔
568
        for c1,c2 in ['GR', 'RZ']:
1✔
569
            star_colors[c1 + '-' + c2] = star_mags[c1] - star_mags[c2]
1✔
570
            star_unextincted_colors[c1 + '-' + c2] = (
1✔
571
                star_unextincted_mags[c1] - star_unextincted_mags[c2])
572
    for c1,c2 in [('BP','RP'), ('G','RP')]:
1✔
573
        star_colors['GAIA-' + c1 + '-' + c2] = (
1✔
574
            star_mags['GAIA-' + c1] - star_mags['GAIA-' + c2])
575
        star_unextincted_colors['GAIA-' + c1 + '-' + c2] = (
1✔
576
            star_unextincted_mags['GAIA-' + c1] -
577
            star_unextincted_mags['GAIA-' + c2])
578

579
    local_comm, head_comm = None, None
1✔
580
    if comm is not None:
1✔
581
        # All ranks in local_comm work on the same stars
582
        local_comm = comm.Split(rank % nstars, rank)
×
583
        # The color 1 in head_comm contains all ranks that are have rank 0 in local_comm
584
        head_comm = comm.Split(rank < nstars, rank)
×
585

586
    #- Allocate arrays only needed by local_comm.rank == 0 ranks
587
    if local_comm is None or local_comm.rank == 0:
1✔
588
        linear_coefficients = np.zeros((nstars,stdflux.shape[0]))
1✔
589
        chi2dof = np.zeros((nstars))
1✔
590
        redshift = np.zeros((nstars))
1✔
591
        normflux = np.zeros((nstars, stdwave.size))
1✔
592
        fitted_model_colors = np.zeros(nstars)
1✔
593
        model = np.zeros(stdwave.size)
1✔
594
    else:
595
        linear_coefficients = None
×
596
        chi2dof = None
×
597
        redshift = None
×
598
        normflux = None
×
599
        fitted_model_colors = None
×
600
        model = None
×
601

602
    for star in range(rank % nstars, nstars, size):
1✔
603

604
        log.info("rank %d: finding best model for observed star #%d"%(rank, star))
1✔
605

606
        # np.array of wave,flux,ivar,resol
607
        wave = {}
1✔
608
        flux = {}
1✔
609
        ivar = {}
1✔
610
        resolution_data = {}
1✔
611
        for camera in frames :
1✔
612
            for i,frame in enumerate(frames[camera]) :
1✔
613
                identifier="%s-%d"%(camera,i)
1✔
614
                wave[identifier]=frame.wave
1✔
615
                flux[identifier]=frame.flux[star]
1✔
616
                ivar[identifier]=frame.ivar[star]
1✔
617
                resolution_data[identifier]=frame.resolution_data[star]
1✔
618

619
        # preselect models based on magnitudes
620
        photsys=fibermap['PHOTSYS'][star]
1✔
621

622
        if gaia_std:
1✔
623
            model_colors = model_mags[color_band1] - model_mags[color_band2]
1✔
624
        else:
625
            model_colors = model_mags[color_band1 + photsys] - model_mags[color_band2 + photsys]
1✔
626

627
        color_diff = model_colors - star_unextincted_colors[color][star]
1✔
628
        selection = np.abs(color_diff) < args.delta_color
1✔
629
        if np.sum(selection) == 0 :
1✔
630
            log.warning("no model in the selected color range for this star")
×
631
            continue
×
632

633

634
        # smallest cube in parameter space including this selection (needed for interpolation)
635
        new_selection = (teff>=np.min(teff[selection]))&(teff<=np.max(teff[selection]))
1✔
636
        new_selection &= (logg>=np.min(logg[selection]))&(logg<=np.max(logg[selection]))
1✔
637
        new_selection &= (feh>=np.min(feh[selection]))&(feh<=np.max(feh[selection]))
1✔
638
        selection = np.where(new_selection)[0]
1✔
639

640
        log.info("star#%d fiber #%d, %s = %f, number of pre-selected models = %d/%d"%(
1✔
641
            star, starfibers[star], color, star_unextincted_colors[color][star],
642
            selection.size, stdflux.shape[0]))
643

644
        # Match unextincted standard stars to data
645
        match_templates_result = match_templates(
1✔
646
            wave, flux, ivar, resolution_data,
647
            stdwave, stdflux[selection],
648
            teff[selection], logg[selection], feh[selection],
649
            ncpu=ncpu, z_max=args.z_max, z_res=args.z_res,
650
            template_error=args.template_error, comm=local_comm
651
            )
652

653
        # Only local rank 0 can perform the remaining work
654
        if local_comm is not None and local_comm.Get_rank() != 0:
1✔
655
            continue
×
656

657
        coefficients, redshift[star], chi2dof[star] = match_templates_result
1✔
658
        linear_coefficients[star,selection] = coefficients
1✔
659
        log.info('Star Fiber: {}; TEFF: {:.3f}; LOGG: {:.3f}; FEH: {:.3f}; Redshift: {:g}; Chisq/dof: {:.3f}'.format(
1✔
660
            starfibers[star],
661
            np.inner(teff,linear_coefficients[star]),
662
            np.inner(logg,linear_coefficients[star]),
663
            np.inner(feh,linear_coefficients[star]),
664
            redshift[star],
665
            chi2dof[star])
666
            )
667

668
        # Apply redshift to original spectrum at full resolution
669
        model *= 0.0   #- clear model from previous loop without re-allocating memory
1✔
670
        redshifted_stdwave = stdwave*(1+redshift[star])
1✔
671
        for i,c in enumerate(linear_coefficients[star]) :
1✔
672
            if c != 0 :
1✔
673
                model += c*np.interp(stdwave,redshifted_stdwave,stdflux[i])
1✔
674

675
        # Apply dust extinction to the model
676
        log.info("Applying MW dust extinction to star {} with EBV = {}".format(star,ebv[star]))
1✔
677
        model *= dust_transmission(stdwave, ebv[star])
1✔
678

679
        # Compute final color of dust-extincted model
680
        photsys=fibermap['PHOTSYS'][star]
1✔
681

682
        if not gaia_std:
1✔
683
            model_mag1, model_mag2 = [get_magnitude(stdwave, model, model_filters, _ + photsys) for _ in [color_band1, color_band2]]
1✔
684
        else:
685
            model_mag1, model_mag2 = [get_magnitude(stdwave, model, model_filters, _ ) for _ in [color_band1, color_band2]]
1✔
686

687
        if color_band1 == ref_mag_name:
1✔
688
            model_magr = model_mag1
1✔
689
        elif color_band2 == ref_mag_name:
1✔
690
            model_magr = model_mag2
1✔
691
        else:
692
            # if the reference magnitude is not among colours
693
            # I'm fetching it separately. This will happen when
694
            # colour is BP-RP and ref magnitude is G
695
            if gaia_std:
1✔
696
                model_magr = get_magnitude(stdwave, model, model_filters, ref_mag_name)
1✔
697
            else:
698
                model_magr = get_magnitude(stdwave, model, model_filters, ref_mag_name + photsys)
×
699
        fitted_model_colors[star] = model_mag1 - model_mag2
1✔
700

701
        #- TODO: move this back into normalize_templates, at the cost of
702
        #- recalculating a model magnitude?
703

704
        cur_refmag = star_mags[ref_mag_name][star]
1✔
705

706
        # Normalize the best model using reported magnitude
707
        scalefac=10**((model_magr - cur_refmag)/2.5)
1✔
708

709
        log.info('scaling {} mag {:.3f} to {:.3f} using scale {}'.format(ref_mag_name, model_magr, cur_refmag, scalefac))
1✔
710
        normflux[star] = model*scalefac
1✔
711

712
    if head_comm is not None and rank < nstars: # head_comm color is 1
1✔
713
        linear_coefficients = head_comm.reduce(linear_coefficients, op=MPI.SUM, root=0)
×
714
        redshift = head_comm.reduce(redshift, op=MPI.SUM, root=0)
×
715
        chi2dof = head_comm.reduce(chi2dof, op=MPI.SUM, root=0)
×
716
        fitted_model_colors = head_comm.reduce(fitted_model_colors, op=MPI.SUM, root=0)
×
717
        normflux = head_comm.reduce(normflux, op=MPI.SUM, root=0)
×
718

719
    # Check at least one star was fit. The check is peformed on rank 0 and
720
    # the result is bcast to other ranks so that all ranks exit together if
721
    # the check fails.
722
    atleastonestarfit = False
1✔
723
    if rank == 0:
1✔
724
        fitted_stars = np.where(chi2dof != 0)[0]
1✔
725
        atleastonestarfit = fitted_stars.size > 0
1✔
726
    if comm is not None:
1✔
727
        atleastonestarfit = comm.bcast(atleastonestarfit, root=0)
×
728
    if not atleastonestarfit:
1✔
729
        log.error("No star has been fit.")
×
730
        sys.exit(12)
×
731

732
    # Now write the normalized flux for all best models to a file
733
    if rank == 0:
1✔
734

735
        # get the fibermap from any input frame for the standard stars
736
        fibermap = Table(frame.fibermap[fitted_stars])
1✔
737
        assert np.all(fibermap['FIBER'] == starfibers[fitted_stars])
1✔
738

739
        # drop fibermap columns specific to exposures instead of targets
740
        for col in ['DELTA_X', 'DELTA_Y', 'EXPTIME', 'NUM_ITER',
1✔
741
                'FIBER_RA', 'FIBER_DEC', 'FIBER_X', 'FIBER_Y']:
742
            if col in fibermap.colnames:
1✔
743
                fibermap.remove_column(col)
1✔
744

745
        data={}
1✔
746
        data['TARGETID'] = fibermap['TARGETID']
1✔
747
        data['FIBER'] = fibermap['FIBER']
1✔
748
        data['LOGG']=linear_coefficients[fitted_stars,:].dot(logg)
1✔
749
        data['TEFF']= linear_coefficients[fitted_stars,:].dot(teff)
1✔
750
        data['FEH']= linear_coefficients[fitted_stars,:].dot(feh)
1✔
751
        data['CHI2DOF']=chi2dof[fitted_stars]
1✔
752
        data['REDSHIFT']=redshift[fitted_stars]
1✔
753
        data['COEFF']=linear_coefficients[fitted_stars,:]
1✔
754
        data['DATA_%s'%color]=star_colors[color][fitted_stars]
1✔
755
        data['MODEL_%s'%color]=fitted_model_colors[fitted_stars]
1✔
756
        data['BLUE_SNR'] = snr['b'][fitted_stars]
1✔
757
        data['RED_SNR']  = snr['r'][fitted_stars]
1✔
758
        data['NIR_SNR']  = snr['z'][fitted_stars]
1✔
759
        io.write_stdstar_models(args.outfile,normflux,stdwave,
1✔
760
                starfibers[fitted_stars],data,
761
                fibermap, input_frames_table)
762

763
    if comm is not None:
1✔
764
        comm.barrier()
×
765

766
    return 0
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc