fixedbugs

Masking fixed-pattern residuals in APERO spectra · SPIRou & NIRPS

Masquage des résidus fixes dans les spectres APERO · SPIRou et NIRPS

APERO t.fits · fixed-pattern residuals

t.fits APERO · résidus à motif fixe

fixedbugs

Some pixels of a reduced spectrum are wrong in every single exposure, always at the same place on the detector. This tool finds them by letting the Earth's orbit do the work, and writes clean copies of your APERO t.fits files with those pixels set to NaN.

Certains pixels d'un spectre réduit sont problématiques dans chaque pose, toujours au même endroit sur le détecteur. Cet outil les repère en laissant l'orbite terrestre faire le travail, puis réécrit vos fichiers t.fits APERO avec ces pixels mis à NaN.

The bugs that never moveLes bogues qui ne bougent jamais

APERO, the reduction pipeline of SPIRou and NIRPS, writes one t.fits file per exposure. Its science flux extension holds the extracted, blaze-shaped, telluric corrected 2D spectrum: one row per diffraction order, one column per detector pixel.

That spectrum is never perfect. A handful of pixels in every order carry a residual that no amount of averaging removes, because the residual sits at the same detector pixel in every exposure. The usual culprits:

  • imperfectly corrected telluric absorption, in the deep water and methane bands;
  • residuals of the OH airglow emission subtraction;
  • persistence left over from the daytime calibrations, above all by the saturated lines of the Uranium-Neon wavelength-calibration lamp, which stay imprinted on the detector well into the night;
  • other detector features: bad pixel clusters, ghosts, order edges.

All of these are fixed in the observer's rest frame. The star is not. That single difference is enough to tell them apart, and it is why we call these residuals fixed bugs: they are bugs, and they never move.

APERO, le pipeline de réduction de SPIRou et NIRPS, produit un fichier t.fits par pose. Son extension de flux scientifique contient le spectre 2D extrait, encore modulé par le blaze et corrigé des tellurics : une rangée par ordre de diffraction, une colonne par pixel du détecteur.

Ce spectre n'est jamais parfait. Dans chaque ordre, quelques pixels portent un résidu qu'aucun moyennage n'élimine, parce qu'il se trouve au même pixel du détecteur dans toutes les poses. Les coupables habituels :

  • une correction tellurique imparfaite, dans les bandes profondes d'eau et de méthane ;
  • les résidus de la soustraction des raies d'émission OH de l'atmosphère ;
  • la rémanence laissée par les calibrations de jour, surtout par les raies saturées de l'Uranium-Néon, la lampe de calibration en longueur d'onde, qui restent imprimées sur le détecteur jusque tard dans la nuit ;
  • les autres défauts du détecteur : amas de mauvais pixels, fantômes, bords d'ordre.

Tout cela est fixe dans le référentiel de l'observateur. L'étoile, elle, ne l'est pas. Cette seule différence suffit à les séparer, et c'est pourquoi on appelle ces résidus des bogues fixes : ce sont des bogues, et ils ne bougent jamais.

On a real data setSur un vrai jeu de données

Exposures
Poses
321
Of measurable pixels
Des pixels mesurables
17.5 %
BERV span
Amplitude BERV
37.9 km/s
Runtime
Temps de calcul
50 s

321 SPIRou exposures of TOI-2120, 49 orders × 4088 pixels, on a laptop. A quarter of that detector (25.4%) is already NaN before fixedbugs touches it, so the masked fraction is quoted against the 149 466 pixels it can actually judge, not against the whole array.

321 poses SPIRou de TOI-2120, 49 ordres × 4088 pixels, sur un portable. Un quart de ce détecteur (25,4 %) est déjà à NaN avant que fixedbugs n'y touche : la fraction masquée est donc rapportée aux 149 466 pixels qu'il peut réellement juger, pas au tableau entier.

See it directly

Voir directement

Every exposure, stackedToutes les poses, empilées

Residuals of 321 SPIRou exposures for one order, with vertical stripes
One order, 321 exposures, zoomed on the middle 1000 pixels. Each row is one exposure; blue and red are negative and positive residuals with respect to a shifted stellar template. The stellar lines move from row to row with the barycentric velocity and average away. What survives are the vertical stripes: pixels that are wrong in every exposure, at exactly the same place. The black bar underneath is the mask derived from them.
Un ordre, 321 poses, centré sur les 1000 pixels du milieu. Chaque rangée est une pose ; le bleu et le rouge sont les résidus négatifs et positifs par rapport à un template stellaire décalé. Les raies de l'étoile se déplacent d'une rangée à l'autre avec la vitesse barycentrique et se moyennent. Ce qui subsiste, ce sont les bandes verticales : des pixels problématiques dans toutes les poses, exactement au même endroit. La barre noire en dessous est le masque qu'on en déduit.

Why it works: the Earth does the workPourquoi ça marche : la Terre fait le travail

Over a year, the Earth's motion around the Sun changes the observed radial velocity of any star by up to ±30 km/s. APERO records that shift in each header as the barycentric Earth radial velocity (BERV, in km/s). At SPIRou's dispersion of 2.28 km/s per pixel, it slides the entire stellar spectrum by a couple of dozen pixels across the detector, back and forth, on a perfectly predictable schedule.

The instrumental and atmospheric residuals do not follow. So if you shift every exposure back into the stellar frame and compare it to a template, the star lines up and the bugs smear; compare in the detector frame instead, as this tool does, and the bugs line up while the star smears. That is the entire principle.

Au cours d'une année, le mouvement de la Terre autour du Soleil fait varier la vitesse radiale observée de n'importe quelle étoile de ±30 km/s au maximum. APERO consigne ce décalage dans chaque entête sous le nom de vitesse radiale barycentrique de la Terre (BERV, en km/s). À la dispersion de SPIRou, 2,28 km/s par pixel, cela fait glisser tout le spectre stellaire d'une vingtaine de pixels sur le détecteur, dans un sens puis dans l'autre, selon un calendrier parfaitement prévisible.

Les résidus instrumentaux et atmosphériques, eux, ne suivent pas. Si on ramène chaque pose dans le référentiel de l'étoile pour la comparer à un template, l'étoile se superpose et les bogues s'étalent ; si on compare plutôt dans le référentiel du détecteur, comme le fait cet outil, ce sont les bogues qui se superposent et l'étoile qui s'étale. C'est tout le principe.

BERV of 321 SPIRou exposures of TOI-2120 against time
The barycentric velocity of the 321 TOI-2120 exposures over about 450 days. The right-hand axis converts it into the shift of the stellar spectrum on the detector. A 37.9 km/s span is 17 pixels of leverage: plenty to separate what moves from what does not.
La vitesse barycentrique des 321 poses de TOI-2120 sur environ 450 jours. L'axe de droite la convertit en décalage du spectre stellaire sur le détecteur. Une amplitude de 37,9 km/s représente un levier de 17 pixels : largement de quoi séparer ce qui bouge de ce qui ne bouge pas.

Pass 1 of 2

Passe 1 sur 2

Measuring where the bugs areMesurer où sont les bogues

For every exposure and every order:

  • Shift a high signal-to-noise 1D template of the same star (an APERO s1d_v template, which lives in the barycentric frame) into the observer frame with that exposure's BERV, and multiply it by the blaze. That is the model of what the exposure should look like.
  • Divide the observed flux by that model and normalise by the median. A perfect reduction would give a flat line at 1.
  • Subtract a running median, to throw away the large-scale mismatch we do not care about: template colour, imperfect blaze, continuum.
  • Divide by a robust sigma from the 16th–84th percentile spread, so every exposure contributes on the same scale whatever its signal-to-noise.

Then take, pixel by pixel, the median of the absolute value across all exposures. A pixel that misbehaves once, a cosmic ray say, is voted down by the median. A pixel that misbehaves every time survives.

That curve is not flat even when nothing is wrong: the noise itself grows towards the order edges, where the blaze drops. So instead of one global threshold, a degree-2 polynomial is fitted to the noise floor along the order, and only the excess over that floor is thresholded.

Fitting the floor rather than the middle matters more than it sounds. An ordinary least-squares fit is pulled upwards by the very pixels we are hunting, which then hide behind their own contribution. So the fit uses an asymmetric loss: a point above the curve costs asymmetry times its squared residual, a point below costs 1 - asymmetry times it. At the default 0.02 that is a 49:1 ratio, and the curve settles just under the bulk of the points.

The penalty on points above the curve is small but deliberately not zero. If it were, nothing would stop the fit sliding down forever, since going lower would cost nothing at all. Keeping a little cost per point means a curve that has sunk below everything pays for every single point at once, which pulls it straight back up to the floor. That one term is what makes the problem well posed. It is solved by iteratively reweighted least squares in 5 to 10 passes, in about thirty lines of the script, so there is no fitting library to install.

Pour chaque pose et chaque ordre :

  • Décaler un template 1D à haut rapport signal sur bruit de la même étoile (un template s1d_v d'APERO, qui vit dans le référentiel barycentrique) vers le référentiel de l'observateur avec le BERV de cette pose, puis le multiplier par le blaze. C'est le modèle de ce à quoi la pose devrait ressembler.
  • Diviser le flux observé par ce modèle et normaliser par la médiane. Une réduction parfaite donnerait une droite plate à 1.
  • Soustraire une médiane glissante, pour éliminer les désaccords à grande échelle dont on se moque : couleur du template, blaze imparfait, continuum.
  • Diviser par un sigma robuste tiré de l'écart entre les 16e et 84e centiles, pour que chaque pose contribue sur la même échelle quel que soit son rapport signal sur bruit.

On prend ensuite, pixel par pixel, la médiane de la valeur absolue sur toutes les poses. Un pixel qui déraille une seule fois, à cause d'un rayon cosmique par exemple, est écrasé par la médiane. Un pixel qui déraille à chaque fois survit.

Cette courbe n'est pas plate même quand tout va bien : le bruit lui-même augmente vers les bords de l'ordre, là où le blaze s'effondre. Plutôt qu'un seuil global unique, on ajuste donc un polynôme de degré 2 sur le plancher de bruit le long de l'ordre, et on ne seuille que l'excès par rapport à ce plancher.

Ajuster le plancher plutôt que le milieu compte plus qu'il n'y paraît. Un ajustement des moindres carrés ordinaire est tiré vers le haut par les pixels mêmes que l'on cherche, qui se cachent alors derrière leur propre contribution. L'ajustement utilise donc une pénalité asymétrique : un point au-dessus de la courbe coûte asymmetry fois son résidu au carré, un point en dessous coûte 1 - asymmetry fois ce même résidu. À la valeur par défaut de 0,02, c'est un rapport de 49 contre 1, et la courbe se pose juste sous le gros du nuage de points.

La pénalité sur les points au-dessus est faible mais volontairement non nulle. Si elle l'était, rien n'empêcherait l'ajustement de descendre indéfiniment, puisque descendre ne coûterait rien. En gardant un petit coût par point, une courbe qui est passée sous tous les points les paie tous d'un coup, ce qui la ramène aussitôt au plancher. C'est ce seul terme qui rend le problème bien posé. On le résout par moindres carrés repondérés en 5 à 10 itérations, en une trentaine de lignes du script : il n'y a aucune bibliothèque d'ajustement à installer.

The mask, order by orderLe masque, ordre par ordre

The full mask, 49 orders by 4088 pixels
The whole mask: 49 orders by 4088 pixels, bright where a pixel is rejected. The structure is real, not noise. The dense band at the top is the K-band orders, where telluric absorption is worst, and the clean diagonal running across the bottom half is one atmospheric feature walking from order to order as the wavelength coverage shifts.
Le masque complet : 49 ordres sur 4088 pixels, en clair là où un pixel est rejeté. La structure est réelle, ce n'est pas du bruit. La bande dense du haut correspond aux ordres de la bande K, où l'absorption tellurique est la pire, et la diagonale nette qui traverse la moitié inférieure est une même structure atmosphérique qui se déplace d'ordre en ordre à mesure que la couverture en longueur d'onde se décale.

Choosing the thresholdChoisir le seuil

Median absolute residual along one order with the fitted expectation
One order. The grey curve is the median absolute residual over all 321 exposures, in units of the robust noise. The red curve is the fitted noise floor: it hugs the bottom of the point cloud, and follows the natural rise towards the red end of the order instead of fighting it. Pink bands are the pixels flagged as being in excess of that floor. This is the plot to look at when tuning nsig_mask.
Un ordre. La courbe grise est le résidu absolu médian sur les 321 poses, en unités de bruit robuste. La courbe rouge est le plancher de bruit ajusté : il épouse le bas du nuage de points et suit la montée naturelle vers l'extrémité rouge de l'ordre au lieu de la combattre. Les bandes roses sont les pixels signalés comme en excès par rapport à ce plancher. C'est la figure à regarder pour régler nsig_mask.

Pass 2 of 2

Passe 2 sur 2

Applying it: adding zero and NaNL'appliquer : additionner des zéros et des NaN

The mask is stored as an array of 0.0 (keep) and NaN (reject), and it is simply added to the flux. Adding zero changes nothing; adding NaN gives NaN. One line, no indexing, no branching.

Every other extension and the whole header are copied verbatim, so the output is still a valid APERO product that any downstream tool can read. Tools that already skip NaNs — LBL, CCF codes, template builders — then ignore those pixels for free. Provenance goes into the header (FBVERS, FBINSTR, FBFIBER, FBNSIG, FBFRAC…), so a file found on disk a year from now can be traced back to the settings that produced it.

Le masque est stocké comme un tableau de 0.0 (garder) et de NaN (rejeter), et il est simplement additionné au flux. Ajouter zéro ne change rien ; ajouter NaN donne NaN. Une ligne, sans indexation ni branchement.

Toutes les autres extensions et l'entête complète sont recopiées telles quelles : la sortie reste un produit APERO valide, lisible par n'importe quel outil en aval. Ceux qui ignorent déjà les NaN — LBL, les codes de CCF, les constructeurs de templates — sautent alors ces pixels gratuitement. La traçabilité part dans l'entête (FBVERS, FBINSTR, FBFIBER, FBNSIG, FBFRAC…), pour qu'un fichier retrouvé sur disque dans un an puisse être relié aux réglages qui l'ont produit.

One order of one exposure, before and after masking
One real order of one real exposure, as delivered by APERO on top and after fixedbugs below. The pink bands are the masked pixels; in the lower panel they are simply gone. This is order 25, on the edge of the 1.4 micron water band, where half of what could be measured turns out to be untrustworthy. The gaps already present in the upper panel are pixels APERO itself had given up on.
Un vrai ordre d'une vraie pose, tel que livré par APERO en haut et après fixedbugs en bas. Les bandes roses sont les pixels masqués ; dans le panneau du bas, ils ont simplement disparu. C'est l'ordre 25, en bordure de la bande d'eau à 1,4 micron, où la moitié de ce qui était mesurable s'avère non fiable. Les trous déjà présents dans le panneau du haut sont des pixels qu'APERO avait lui-même abandonnés.

SPIRou and NIRPS, without changing anythingSPIRou et NIRPS, sans rien changer

The two instruments do not name their science fibre the same way. SPIRou combines its two polarimetric fibres A and B into a single science channel; NIRPS keeps A as the science fibre and points B at the sky.

Les deux instruments ne nomment pas leur fibre scientifique de la même façon. SPIRou combine ses deux fibres polarimétriques A et B en un seul canal scientifique ; NIRPS garde A comme fibre scientifique et pointe B vers le ciel.

InstrumentInstrument Science fibreFibre scientifique ExtensionsExtensions Typical shapeDimensions typiques
SPIRou AB FluxAB, WaveAB, BlazeAB 49 × 4088
NIRPS A FluxA, WaveA, BlazeA 75 × 4088

fixedbugs reads the INSTRUME keyword of the first input file and picks the right one on its own. Array dimensions are always measured from the data, never assumed, and the speed of light comes from astropy.constants rather than a retyped literal. You can still force a fibre if you want to clean a calibration or sky channel.

fixedbugs lit le mot-clé INSTRUME du premier fichier et choisit tout seul. Les dimensions des tableaux sont toujours mesurées sur les données, jamais supposées, et la vitesse de la lumière vient d'astropy.constants plutôt que d'une constante recopiée à la main. Vous pouvez toujours forcer une fibre pour nettoyer un canal de calibration ou de ciel.

Install and runInstaller et lancer

A single script. Clone it, install its dependencies, point it at your data:

Un seul script. Clonez-le, installez ses dépendances, pointez-le vers vos données :

git clone https://github.com/eartigau/fixedbugs.git
cd fixedbugs
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt

cp config.yaml my_star.yaml
# edit my_star.yaml
python fixedbugs.py my_star.yaml

The virtual environment keeps these dependencies off your system Python. Three entries in the YAML are required: io.input_glob, io.output_dir and template.file. Paths may be relative to the YAML file itself, so a configuration keeps working from any directory.

L'environnement virtuel garde ces dépendances hors de votre Python système. Trois entrées du YAML sont obligatoires : io.input_glob, io.output_dir et template.file. Les chemins peuvent être relatifs au fichier YAML lui-même, de sorte qu'une configuration continue de fonctionner depuis n'importe quel répertoire.

Two knobs that matterDeux réglages qui comptent

nsig_mask is a relative excess over the fitted noise floor, so 2.0 means "more than three times the local noise floor". dilation is how far each flagged region is grown, in pixels, because a bad pixel bleeds into its neighbours through the extraction profile. Both measured on the 321-exposure SPIRou set, as a share of the pixels fixedbugs can actually judge:

nsig_mask est un excès relatif par rapport au plancher de bruit ajusté : 2.0 signifie donc « plus de trois fois le plancher local ». dilation indique de combien de pixels chaque région signalée est élargie, parce qu'un mauvais pixel déborde sur ses voisins par le profil d'extraction. Les deux mesurés sur le jeu SPIRou de 321 poses, en proportion des pixels que fixedbugs peut réellement juger :

nsig_mask Pixels maskedPixels masqués
0.549.4 %
1.030.6 %
2.017.5 %
3.011.6 %
4.07.9 %
6.03.9 %

At dilation: 2.

À dilation: 2.

dilation Pixels maskedPixels masqués
08.8 %
113.5 %
217.5 %
527.3 %
1040.0 %

At nsig_mask: 2.0.

À nsig_mask: 2.0.

The highlighted row of each table is the default. The dilation costs more than people expect, because most flagged regions are only a pixel or two wide to begin with: going from 0 to 5 triples what gets removed. Setting it to 0 leaves the flagged pixels exactly as they are.

La ligne surlignée de chaque tableau est la valeur par défaut. La dilatation coûte plus cher qu'on ne le croit, parce que la plupart des régions signalées ne font qu'un ou deux pixels de large au départ : passer de 0 à 5 triple ce qui est retiré. La mettre à 0 laisse les pixels signalés exactement tels quels.

When it cannot tellQuand il ne peut pas trancher

If an order cannot be measured at all, fixedbugs leaves it alone and says so in the log, rather than rejecting it. Two very different things land in that bucket: a saturated telluric band, where the data really is unusable, and a template that does not reach that far in wavelength, where the data may be perfectly fine and we simply cannot check it. Since the residuals alone cannot tell them apart, the default is to only ever remove what there is evidence against.

Si un ordre ne peut pas être mesuré du tout, fixedbugs le laisse intact et le signale dans le journal, plutôt que de le rejeter. Deux situations très différentes aboutissent là : une bande tellurique saturée, où les données sont réellement inutilisables, et un template qui ne va pas si loin en longueur d'onde, où les données sont peut-être parfaites et où l'on ne peut simplement pas vérifier. Comme les résidus seuls ne permettent pas de les distinguer, le comportement par défaut est de ne jamais retirer que ce contre quoi on a des preuves.

For students

Pour les étudiants

Four ideas worth stealingQuatre idées à voler

Use the thing that moves to find the thing that does not. Nothing in this script knows what a telluric line or a bad pixel looks like. The separation comes entirely from the fact that one signal is fixed in the detector frame and the other is not, and the Earth's orbit does the sweeping for free. Look for that structure in your own problem before you start writing a model of what you are trying to remove.

Take the median of absolute values, not the mean. Averaging residuals lets one bad exposure dominate. The median over exposures asks a different and much more robust question: is this pixel wrong most of the time? A cosmic ray cannot answer yes.

Fit the floor, not the middle. The residual level rises towards the order edges where the blaze drops, so a single flat cut would flag whole edges while missing genuinely bad pixels in the bright middle. Worse, an ordinary fit of that level is itself pulled upwards by the very pixels we are hunting, which then hide behind their own contribution. An asymmetric loss that fits the lower envelope breaks that circularity in a handful of lines.

Quote the honest denominator. A quarter of these files is already NaN before we start, so "13.1% of the detector masked" quietly credits the tool with pixels it never touched. What a reader actually wants to know is what fraction of the usable data was removed, which is 17.5%. Pick the denominator that answers the question, and say which one you picked.

Servez-vous de ce qui bouge pour trouver ce qui ne bouge pas. Rien dans ce script ne sait à quoi ressemble une raie tellurique ou un mauvais pixel. La séparation vient entièrement du fait qu'un signal est fixe dans le référentiel du détecteur et l'autre non, et que l'orbite terrestre fait le balayage gratuitement. Cherchez cette structure dans votre propre problème avant de vous mettre à modéliser ce que vous voulez retirer.

Prenez la médiane des valeurs absolues, pas la moyenne. Moyenner les résidus laisse une seule mauvaise pose dominer. La médiane sur les poses pose une question différente et bien plus robuste : ce pixel est-il problématique la plupart du temps ? Un rayon cosmique ne peut pas répondre oui.

Ajustez le plancher, pas le milieu. Le niveau de résidu monte vers les bords de l'ordre, là où le blaze s'effondre : une coupure plate unique signalerait des bords entiers tout en ratant de vrais mauvais pixels au milieu, là où c'est brillant. Pire, un ajustement ordinaire de ce niveau est lui-même tiré vers le haut par les pixels mêmes que l'on cherche, qui se cachent alors derrière leur propre contribution. Une pénalité asymétrique qui ajuste l'enveloppe inférieure brise cette circularité en quelques lignes.

Citez le bon dénominateur. Un quart de ces fichiers est déjà à NaN avant qu'on commence : dire « 13,1 % du détecteur masqué » crédite discrètement l'outil de pixels auxquels il n'a jamais touché. Ce qu'un lecteur veut vraiment savoir, c'est quelle fraction des données utilisables a été retirée, soit 17,5 %. Choisissez le dénominateur qui répond à la question, et dites lequel vous avez choisi.