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
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
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.
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_vtemplate, 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_vd'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
Choosing the thresholdChoisir le seuil
nsig_mask.
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.
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.5 | 49.4 % |
| 1.0 | 30.6 % |
| 2.0 | 17.5 % |
| 3.0 | 11.6 % |
| 4.0 | 7.9 % |
| 6.0 | 3.9 % |
At dilation: 2.
À dilation: 2.
| dilation | Pixels maskedPixels masqués |
|---|---|
| 0 | 8.8 % |
| 1 | 13.5 % |
| 2 | 17.5 % |
| 5 | 27.3 % |
| 10 | 40.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.