Iris Lab Notes

SRT on GitHub ·

Updated 2026-08-07 10:42 EDT

What shape is a star?

2026-08-07 · POINT SPREAD FUNCTION · GAUSSIAN vs MOFFAT · 11.5% APART

Abstract

A star is a point source, yet it lands on the sensor as a blob several pixels across. The shape of that blob — the point spread function — is what every measurement of “seeing” actually measures, and the number depends on which function you fit to it. This derives the FWHM from first principles for both the Gaussian and the Moffat, shows where the constant 2.3548 in the code comes from, and explains why the two disagree by a systematic 11.5% on this telescope: 1.76″ against 1.56″ on the same 118 stars. The Moffat wins because atmospheric wings follow a power law that a Gaussian cannot represent, so the Gaussian widens its core to compensate. It is also why PixInsight has always reported lower seeing than this pipeline did.

The question

A star is, for any telescope ever built, a perfect point — unresolvably far away. Yet it arrives on the sensor as a smear several pixels wide, and how wide it is happens to be the most-quoted number in amateur astronomy. That smear is the point spread function: what the atmosphere and the optics do to a point of light on its way to a pixel.

Reporting its width means choosing a function to fit, and the choice is not cosmetic. Fit two different functions to the same star and you get two different answers, both defensible, differing here by 11.5%. That is larger than most of the effects an observatory is trying to detect.

The Gaussian, and where 2.3548 comes from

The obvious first choice is a Gaussian, on the reasoning that many independent random perturbations tend toward one:

I(r) = A exp( −r² / 2σ² ) A = peak intensity  ·  r = distance from centre  ·  σ = width parameter

σ means nothing to an observer at the eyepiece, so width is quoted as the full width at half maximum: how wide the profile is at half its peak brightness. Deriving it takes three lines. Set the intensity to half the peak and solve for r:

A/2 = A exp( −r² / 2σ² )
½ = exp( −r² / 2σ² )
ln(½) = −r² / 2σ²   →   r² = 2σ² ln 2

That r is the half width, so the full width is twice it:

FWHM = 2σ √(2 ln 2) = 2.3548 σ

There is the magic constant that appears unexplained in almost every piece of astronomy software, this pipeline included. It is not a fudge factor or a calibration — it is 2√(2 ln 2), and it follows from the definition of half maximum and nothing else. For an elliptical star the two axes combine as a geometric mean, FWHM = 2.3548 √(σxσy), which is the width of the circular Gaussian covering the same area.

The Moffat

The Gaussian has a problem that only appears far from the centre. Moffat’s 1969 profile replaces the exponential with a power law:

I(r) = A [ 1 + (r/γ)² ]−α γ sets the core width  ·  α controls how fast the wings fall away

The FWHM follows by the same three steps:

½ = [ 1 + (r/γ)² ]−α
21/α = 1 + (r/γ)²   →   r = γ √(21/α − 1)
FWHM = 2γ √(21/α − 1)

Two functions, two derivations, one definition of width. And the Gaussian is not a rival so much as a limiting case: as α → ∞ with γ = σ√(2α), the Moffat becomes exactly the Gaussian, because (1 + x/α)−α → e−x. The Gaussian is the Moffat with infinitely steep wings — which is precisely the assumption that fails.

Why they disagree: the wings

Far from the centre the two behave completely differently. The Gaussian falls as e−r², faster than any power law and effectively zero within a few σ. The Moffat falls as r−2α — a power law, which keeps a little flux at large radii indefinitely.

Atmospheric turbulence produces power-law wings, so a real star has more light far from its centre than a Gaussian permits — visible in the right-hand panel below, where the Gaussian hits zero while the star is still clearly shining. The fitting routine has to reconcile that somehow, and it does so by widening the core: a broader Gaussian captures more of the wing flux at the cost of misrepresenting the middle. The reported FWHM comes out too large, systematically, for every star, on every frame.

A Moffat has no such conflict. Its α absorbs the wings while γ describes the core, so the two are fitted independently instead of traded against one another. For Kolmogorov turbulence α ≈ 4.8; smaller values mean heavier wings.

Three panels: a real star with both fitted FWHM circles drawn on it; the radial profile on a linear scale where both fits look identical; and the same profile on a log scale where the Gaussian falls away sharply while the measured points and the Moffat continue outward
One real star from an SH2-92 sub-exposure, at pixel (1409, 6240), with both profiles fitted to the same pixels. Left: the star, with each fit’s FWHM drawn as a circle — the Gaussian in orange is visibly the wider of the two. Centre: the measured radial profile on a linear scale, where the two fits are almost impossible to tell apart, which is exactly why this discrepancy went unnoticed for so long. Right: the same data on a log scale, where the disagreement is obvious. The Gaussian collapses to nothing beyond about 10 pixels while the star keeps emitting light out to 20 and beyond. Note that the measured points sit above even the Moffat in the far wings: the real profile is heavier still, so the Moffat is a better description rather than a perfect one. Click for full size.

What it measures here

Both functions fitted to the same 118 bright stars in one SH2-92 sub-exposure, on identical pixels:

ProfileFWHM (px)FWHM (arcsec)
Gaussian6.771.76″
Moffat6.001.56″

The Moffat reads 11.5% smaller, in the direction the theory predicts. This also settles a long-standing niggle: PixInsight has consistently reported better seeing than this pipeline, and the reason is simply that PixInsight fits a Moffat. Neither was wrong. They were measuring the widths of two different functions and both calling the answer FWHM.

What changed, and what deliberately did not

The pipeline now fits both profiles to every star. Width comes from the Moffat; shape — eccentricity and major-axis angle — still comes from the Gaussian. That is not indecision. The standard Moffat implementation is circularly symmetric: γ and α and nothing else, so it cannot express elongation at all. Swapping wholesale would have silently deleted the very numbers the optics-trend metrics use to detect tilt, coma and collimation drift. Two fits, each asked only what it can answer.

Historical measurements were tagged, not converted. It is tempting to scale the old Gaussian numbers by 11.5% and keep one continuous series, but that figure is not a constant — it depends on α, which varies with the seeing on the night. Scaling would manufacture a precision that was never measured. Records now carry the model that produced them, the trend code refuses to mix the two families, and the single night of existing baseline was recomputed from its original frames rather than adjusted. The frames are the archive; every derived number is only a cache.

A search that finds nothing looks exactly like a search that works

2026-08-05 · INJECTION TESTING · 3 BUGS IN THE PIPELINE · 4 IN THE TEST

Abstract

A supernova search that had reported nothing for weeks turned out to be incapable of detecting a supernova — and no amount of reading its output would have revealed it, because a working detector and a blind one both print “no candidates”. Injecting fake sources of known brightness into real frames exposed three pipeline bugs in a single run: a shape cut that rejected 85% of genuine point sources, a mask that deleted the brightest transients by construction, and catalogue identification that had been dead for 67 commits. It also exposed four bugs in the test itself. The closing argument is why the standing assertion is monotonicity rather than a pinned number: a conventional regression test would have recorded the broken behaviour as correct and failed the repair.

The question

The supernova search had been running for weeks and had found nothing. That is the correct result — one amateur telescope revisiting a couple of dozen galaxies should discover approximately none — and the note describing it said so with some confidence.

But it also carried a caveat that turned out to be the only interesting sentence in it: a non-detection is worth exactly what its sensitivity is. "Nothing found" is a statement about the sky only if you know how bright a real event would have had to be to survive the filtering. Otherwise it is a statement about the software. And the two are indistinguishable from the output, because a detector that is working perfectly and a detector that is completely blind both print "no candidates".

That is an uncomfortable position for every cut in the pipeline. Each one was added because it removed false positives, and each one demonstrably did. Nothing in a normal run reveals whether it also removed the real thing.

The method: stop waiting for a supernova

A supernova cannot be summoned on demand, so the answer is to inject fake ones. Take the real frames from a real night, paint in synthetic point sources of known position and known brightness, run the entire unmodified pipeline, and count how many come back. The result is a completeness curve: recovery fraction against brightness. It is standard practice in professional surveys for exactly this reason, and the script to do it had been sitting in the repository, unrun.

Two words are doing a lot of work here, and neither means quite what it sounds like. The template is a deep image of the same patch of sky, built by stacking every previous night. The science image is the most recent night's stack. Both are ordinary exposures of the target and differ only in when — neither has anything to do with calibration frames, the bias, dark and flat exposures that remove the instrument's own signature. Subtract "before" from "now", and what remains is what changed.

Two details then decide whether the exercise means anything. The injections go into the real registered science frames, so they carry the night's actual noise, sky and galaxy underneath — only the source is synthetic. And they are injected into the science image only, never the template, so they look precisely like something that appeared since the last visit.

The first run answered the question immediately

It produced this, on NGC 5907:

Injected flux (ADU)Recovered
1000 / 3
2000 / 3
4002 / 3
8000 / 3
16000 / 3
32000 / 3
64000 / 3

This is not a detection limit. It is impossible. A source sixteen times brighter cannot be harder to find than one at 400 ADU, and no amount of noise produces that shape. The curve is not a measurement of the sky; it is a bug report about the detector, and it took one run to get it.

Three bugs, and none of them were visible in normal output

The shape window was tuned backwards. A supernova is a point source, so the search kept only compact round residuals — semi-major axis between 0.6 and 1.3 pixels, said to match "measured stellar a ≈ 0.7". That figure does not describe this telescope. Measured on the stack: real stars have median a = 2.51 (FWHM 5.6 px), and residuals in the difference image 2.83. The window was admitting 15% of genuine point sources and rejecting the rest as "extended" — it was selecting for the sub-pixel artifacts it existed to exclude. Where the 0.7 figure came from is still unknown. A hot-pixel origin was proposed here and then withdrawn: the frame-statistics code was the obvious suspect, but tested against normal frames it measures stars correctly, so it did not produce that number.

Widening it to the measured stellar spread raised recovery from 2/21 to 8/21 and lowered false positives from 5 to 3. That is the detail worth pausing on: this was never a trade between sensitivity and purity. The cut was simply worse on both axes, and had been for as long as it existed.

Bright transients masked themselves out. Saturated stars subtract badly, so the pipeline masked the top 0.1% of pixels and rejected anything landing inside. It built that mask from the science image — and a bright supernova is, by definition, among the brightest pixels of the science image. Every injection at or above 3200 ADU was being discarded as a saturated star. The brighter the event, the more certain its rejection. Saturated stars are in the template too, so building the mask from the template alone loses nothing: false positives stayed at 3 while recovery went from 8/21 to 14/21.

Identification had been dead for 67 commits. A refactor on 17 July changed a shared function to take a solved astrometric position instead of a file path. The transient search was never updated, so every call raised a type error — into a try/except that logged it with the same bland message used for a failed network lookup. For three weeks the search produced no coordinates, no catalogue cross-match and no object names. And because the alert rule is "notify if bright and not a known star", the clause that suppresses alerts for catalogued variables could never be true: every bright variable was paging a phone as a possible discovery.

After all three, on the same field and filter:

Injected flux (ADU)BeforeAfter
1000 / 30 / 3
2000 / 30 / 3
4002 / 33 / 3
8000 / 33 / 3
16000 / 33 / 3
32000 / 33 / 3
64000 / 33 / 3

Monotonic, against one false positive on the un-injected difference. The honest summary of the previous weeks is that the search could not have found a supernova: anything with a normal stellar profile was cut as extended, and anything bright was masked as a saturated star. The narrow band where those two failures did not overlap was roughly 400–800 ADU, and even there recovery was about 2 in 21.

The discovery underneath the bugs

Fixing the shape window exposed something more awkward than the bug. The injected sources all have an identical profile, so their measured size should be identical too. It is not:

Injected flux400800160032006400
Measured semi-major axis1.061.902.292.472.66

Same physical width, two and a half times the measured size. The detector's size estimate is a second moment over pixels above the detection threshold, and a brighter source lifts more of its wings above that threshold. So size is not independent of brightness, and any fixed size window is a brightness cut in disguise. The window in place now is bounded by measurement rather than by a mis-remembered number, which is an improvement, but the right answer is a shape test that compares each candidate against the frame's own stars at matched brightness. That is a redesign, not a tuning change, and it has not been done.

Turning the same question on the exoplanet search

If a detector can be structurally blind while looking healthy, the transit search deserved the same treatment — more so, because it carries the more prominent claim. HAT-P-32b was found and ranked first, which reads as evidence the search works.

The transit version injects the opposite sign: a box-shaped dip of known depth, multiplied into a real star's raw flux before every normalisation step, so a synthetic transit faces everything a real one does. It has not yet produced a trustworthy depth floor, for reasons given below, but it produced one number immediately that needs no calibration at all: of 1055 stars detected in the field, 490 are searched. The edge margin, validity threshold and saturation cut discard 54% of the field before any transit scoring happens. A planet transiting a star in that other half is invisible at any depth. Whether those cuts are right is a separate question; that they halve the survey was not written down anywhere.

Four bugs in the test itself

The instrument measuring the instrument needs the same scepticism, and it earned it. Every one of these produced a confident, wrong result before being caught:

MistakeSymptom
Injections placed on stars the search never examines0% recovery at every depth, including 4%
All injections given one shared epochcorrelated dip, correctly removed by the detrending it was supposed to survive
Epoch rounded to five decimals when recordeddepth silently under-delivered by 10%
Targets chosen by brightness, not photometric qualitysame 4% dip at SNR 12.4 on one star and 1.8 on another

The last one is why no transit depth floor is quoted here. Brightness is not precision, and with targets spanning that range the curve describes the noise of arbitrarily chosen stars rather than the sensitivity of the search. Selecting targets by lowest scatter is the fix and is not yet done.

The assertion that caught all of it

Every failure above — three in the pipeline, four in the test — announced itself the same way: recovery that did not increase with signal strength. That is now asserted in both tests, and it is worth being precise about what is asserted, because the obvious choice is wrong.

A conventional regression test pins current behaviour and fails when it changes. One written against the supernova search the day before would have recorded "recovery = 2/21" as correct, and then failed the moment the shape window was fixed — reporting the repair as a regression and defending the bug. Completeness is supposed to improve, so pinning it is worse than not testing at all.

What cannot legitimately change is the shape. A stronger signal must not be recovered less often than a weaker one. That is physics rather than tuning, so it survives any amount of genuine improvement while still catching every failure listed here. The tests now enforce it and exit non-zero when it breaks, which makes them a standing check rather than a thing that was run once.

Writing the check produced two more bugs, in the check. A real failure — the deepest transit recovered zero times while a shallower one was found — passed, because the drop sat exactly on the tolerance. And a legitimate one-sample wobble failed, because 2/3 < 1.0 - 1/3 is true in floating-point arithmetic. Both are fixed: comparisons are integer counts, and total failure at the strongest signal is checked with no tolerance at all, since the easiest case failing outright is never noise.

What this still does not answer

No limiting magnitude yet, and the reason is worse than "not done". Two identical runs of the fixed supernova search put the 50%-recovery floor at 400 ADU and then at 800 ADU, and produced 662 and 892 raw residuals from the same input frames. A factor of two in the sensitivity, run to run, with nothing changed. Until that is understood there is no stable number to convert into a magnitude, and quoting one would be inventing precision. The monotonicity invariant held in both runs, which is the argument for asserting shape rather than a remembered value.

The result that survives all of this is not a detection limit. It is that the pipeline spent weeks reporting a clean, plausible, entirely meaningless negative, and that no amount of reading its output would have revealed it. The only thing that did was giving it a question whose answer was already known.

Looking for something that was not there last night

2026-08-05 · DIFFERENCE IMAGING · RUNS UNATTENDED ON THE GPU BOX · SUPERNOVAE FOUND: 0

Abstract

How the transient command hunts supernovae by subtracting a deep template built from every previous night from the most recent night’s stack. Most of what survives a subtraction is not astronomy, so the bulk of this is about telling a real point source from misregistration dipoles, hot pixels and galaxy structure — and then from the far likelier explanation, a catalogued variable star. Runs unattended on a separate GPU machine after the night’s frames are copied across, and raises a Pushover alert above a threshold. No supernovae found, which is the expected result.

The question

A galaxy photographed twice looks the same both times. That is the whole difficulty. Somewhere in a spiral arm a star may have exploded since the last visit, and it will be one dot among several thousand dots, sitting on top of a bright, structured, wildly uneven background. Blinking two images by eye is how this was done for decades and it does not scale past a handful of fields.

The transient command — diff is the same thing — asks the question mechanically instead. It gathers every sub ever taken of a target in one filter, splits them by night, builds a deep template from every night except the most recent, treats the most recent night as the science image, and subtracts one from the other. What survives the subtraction is, in principle, what changed.

Why subtracting two images is harder than it sounds

The two images are never the same image. The telescope was pointed slightly differently, the seeing was better on one night than the other, and the sky was brighter or darker. Subtract them naively and every star in the field leaves a residual, because a star that is 1.9″ wide on one night and 2.4″ on another does not cancel. So before subtracting, both are registered onto a common pixel grid, the sharper one is deliberately blurred to match the softer one, and the background and scale are matched robustly. Only then is the difference meaningful.

Even done properly, most of what survives is not astronomy. This is the part worth being honest about, because the first real run said so loudly: on NGC 5907 the search returned five candidates at signal-to-noise between 1200 and 7800, numbers that would be extraordinary if any of them were real. All five were artifacts.

The thousand-sigma figures were themselves the clue. Significance was being measured against the noise of the whole frame, and NGC 5907 is an edge-on galaxy with a bright core — a large, genuinely bright structure that dragged the global noise estimate down and made every residual look impossibly significant against it. Noise is now measured in an annulus around each candidate, so a residual is judged against its own neighbourhood rather than against an average dominated by somewhere else in the picture.

What a supernova has to look like

The rest of the tuning is a set of statements about what a real transient is, each one aimed at a specific thing that was being mistaken for one:

TestCutWhat it rejects
Local significance≥ 5σ vs annulusnoise, and the bright-core inflation above
Source sizesemi-major 0.6–1.3 pxhot pixels below, galaxy knots above
Roundnesselongation ≤ 1.4trails, edges, spiral-arm structure
Negative lobe nearby≥ 4σ within a scaled radiussubtraction dipoles from misregistration
Bright-source maskinside the masksaturated-star residuals
Newnesstemplate flux ≤ 50% of sciencestars that were always there
Gaia cross-matchwithin 2″catalogued stars — down-ranked, not deleted

The size window is the one that carries the most weight, and it is measured rather than chosen. This telescope undersamples: real stars on these frames come out with a semi-major axis of about 0.7 pixels and an elongation near 1.0. A supernova is a point source, so it must look exactly like a star and nothing else. Anything meaningfully larger is galaxy structure; anything smaller is a single hot pixel or a cosmic ray. Two cuts, one measurement.

The dipole test deserves a mention because it catches the failure that looks most like a discovery. If registration is off by a fraction of a pixel, subtracting a star leaves a bright crescent beside a dark one. The bright half alone is a compact, round, significant, apparently new source. Looking for the matching dark lobe nearby is what tells them apart, and the search radius scales with the source, because the wide dipoles thrown off by an edge-on galaxy's structure put their negative half further out than a fixed radius would ever reach.

Then the harder question: what is it?

Finding a new point of light is the easy half. The overwhelmingly likely explanation for a star-like object that brightened is a variable star in our own galaxy, not a supernova in a distant one. So surviving candidates are cross-matched against Gaia, and anything within 2″ of a catalogued point source is heavily down-ranked — down-ranked rather than deleted, since a supernova can sit close to a foreground star by chance and silently discarding it would be the one unrecoverable mistake. A SIMBAD lookup then puts a human-readable name and object type on whatever matched, which usually ends the question immediately.

On NGC 5907 the tuned search returned one marginal candidate instead of five spectacular ones. Identification resolved it to a magnitude-17 Gaia star 0.24″ away. Not a supernova — but the pipeline said so itself, which is the outcome that matters.

Where it runs

None of this happens while the telescope is working. Once the night's lights and calibration frames are complete they are copied across to a separate NVIDIA machine, and the search runs there unattended on what arrived. The observatory PC stays a real-time controller with a roof and a mount to worry about; the GPU box does the arithmetic in the morning, when being slow costs nothing.

If a candidate clears signal-to-noise 8 and is not already a catalogued star, a Pushover notification goes out with the position and the triptych image — template, science, difference, side by side, which is the view that lets a human dismiss most things in about two seconds. Everything else is written to a JSON file to be looked at whenever. An alert that fires for every marginal blob would be switched off within a week, so the bar to interrupt somebody is deliberately higher than the bar to record a candidate.

Result

No supernovae. This is the correct and expected result: a single amateur telescope revisiting a couple of dozen galaxies should discover approximately none, and a pipeline that had found one in its first weeks would be more likely broken than lucky. What the work bought is a search that produces one honest marginal candidate instead of five confident wrong ones.

A non-detection is only worth what its sensitivity is. "Nothing found" means nothing at all without knowing how bright a real transient would have had to be to survive those cuts — a set of filters tuned until the false positives went away can always be tuned until the true positives go too. There is an acceptance test that answers this by injecting fake sources of known brightness into the science frames and measuring what fraction come back, which is the only way to get the number without waiting for a real supernova. It has since been run, and it found that this search could not have detected a supernova at all. What happened next is its own note: a search that finds nothing looks exactly like a search that works. The cuts described above are the corrected ones; the note explains what they were, and why the difference was invisible.

Learning to denoise without ever seeing a clean image

2026-08-04 · NOISE2NOISE · U-NET, ~7M PARAMS · STATUS: TRAINING, NOT IN THE PIPELINE

Abstract

Every sub-exposure is mostly noise, and the obvious fix — train a network to map noisy images to clean ones — is unavailable, because no clean image of a galaxy exists anywhere to train against. Noise2Noise removes that requirement: map one noisy frame to another noisy frame of the same scene and you converge on the same model, because the noise carries no learnable information. An observatory turns out to be close to the ideal case for it. Covers what was built, four ways it goes wrong — dithering teaches the network that stars are noise — and why nothing consumes the output yet.

The question

Every sub-exposure this observatory takes is mostly noise. A 300-second frame of a faint nebula carries read noise from the sensor and shot noise from the photons themselves, and the target is often fainter than either. The classical answer is to take a lot of frames and average them, which is what the stacker does, and it works: noise falls as the square root of the frame count. The question is whether a neural network can do better than the square root — whether it can recognise what sensor noise looks like on this camera and remove it, rather than merely averaging it away.

The obvious way to train such a network is supervised learning: show it a noisy image, show it the clean truth, and penalise the difference. That approach is unavailable here, and not for want of effort. There is no clean image of a galaxy. Nobody has one. No exposure is long enough, no sensor cold enough; the ground truth does not exist anywhere in the universe to be collected. Every supervised denoiser needs a target that astronomy fundamentally cannot supply.

The trick, which is the interesting part

Noise2Noise removes the requirement. The insight is that you can train the network to map one noisy frame to another noisy frame of the same scene, and get the same model you would have got from clean targets.

The reason is a property of the loss function rather than of the network. Training minimises the expected error against the target, and the predictor that minimises squared error against a random target is that target's mean. If the noise is zero-mean and drawn independently for each frame, then the mean of the noisy target is exactly the clean signal. So the network is being asked to predict something whose expected value is the truth. It cannot predict the specific noise in any particular target frame — that noise is independent of its input, and therefore carries no learnable information — so the best it can do is output the underlying signal. The noise averages out inside the loss instead of inside the stack.

Put another way: the network learns whatever the two frames have in common and discards whatever differs between them. Point it at two views of the same sky and what they have in common is the sky.

An observatory turns out to be close to the ideal case for this. The method needs many independent noisy observations of an unchanging scene, which is an awkward thing to arrange for photographs of the physical world — and it is precisely what a telescope produces by accident. Three hundred frames of the same nebula on the same filter, each with its own independent draw of read and shot noise, are the by-product of a single ordinary night. The training pairs cost nothing; they had already been collected and stacked and archived before the idea came up.

What is built

A four-level U-Net of about seven million parameters, single channel in and out, trained per filter and per exposure time — the noise character of a 90-second red frame is not that of a 300-second H-alpha frame, so they get separate models. Training draws random 256-pixel patch pairs from two different frames, 2000 pairs an epoch for 130 epochs, with the same random flip and rotation applied to both members of a pair so they stay aligned. Inference runs in 512-pixel tiles with 64 pixels of overlap, because the QHY600 sensor is 9576×6388 and will not fit in video memory whole. 272aa6e

Four ways to get it wrong

Every one of these was found the hard way, and each is a variation on the same theme: the method erases whatever differs between the two frames, so any difference you failed to think about is a thing it will destroy.

Dithering deletes the stars. The telescope deliberately shifts the pointing a few pixels between exposures, so that sensor defects do not land in the same place every frame. This is good practice for stacking and fatal for Noise2Noise. If the scene has moved between input and target, then the stars are among the things that differ between the two frames — and the network duly concludes that stars are noise and learns to suppress them. Frames now get registered to a common reference by matching star centroids before any training happens. Registration is not a refinement here; it is a correctness requirement, and the failure it prevents looks like a beautifully smooth image with the astronomy removed. 8c1afd2, 67a1c9a

Pairs must be the same scene. Nothing in the loss function knows what a scene is. Pair a frame of M13 with a frame of NGC 7331 and the maths still runs — it just now describes a network learning to predict the average of all sky, since that is what those two frames have in common. Pairs are restricted to within one target, which then made the definition of "one target" load-bearing: an early version grouped frames by directory depth and grouped by night instead, silently splitting one object across sessions. 737e79e

Squared error is the wrong loss for a starfield. The argument above is cleanest for squared error, whose optimum is the mean. But a starfield contains saturated pixels, and squared error is dominated by its largest residuals, so a handful of blown-out star cores pull the model around. The loss is now L1, which optimises toward the median instead and barely notices outliers. The trade is real and worth stating: the median of a skewed distribution is not its mean, and photon noise at low count rates is skewed, so L1 buys robustness at the cost of a small bias the clean theory does not have. That bias has not been measured here. e5c149e

The validation split was measuring nothing. The original code used a standard random split of the dataset, which was a no-op: the dataset ignores the sample index it is handed and draws a fresh random pair on every access, so "train" and "val" were both sampling the same pool of frames. The validation curve was real-looking and meaningless. It now holds out whole targets — at least two, extended until a fifth of the frames are held back — so the number measures generalisation to a sky the model has never seen. That is both the honest test and the actual inference condition, and it is the one that would expose a model that had memorised its training scenes rather than learned this camera's noise. 98a8ff7, 2ba083d

Where it runs

Training happens on an NVIDIA box rather than the observatory machine, which has a telescope to run and no business spending its evening on gradient descent. This is the first piece of a wider move: the observatory PC stays a real-time controller, and the heavy image work — denoising now, stacking and the blind transit search later — migrates to the GPU as an offline stage that reads the night's frames after they have been copied across. Denoising is a good first tenant because it is embarrassingly parallel, needs no interaction, and does not matter if it is late.

Result

There isn't one yet, and that is the honest status. Models train, frames come out denoised, and the comparison images look convincingly better — which is exactly the evidence that should be trusted least. Nothing in the nightly pipeline consumes any of it.

What "looks better" is not. A denoiser that makes an image prettier and a denoiser that preserves photometry are different things, and this one has not been shown to be the second. If the network subtly reshapes stellar profiles it will change measured brightnesses, and the science on this site — the cluster colour–magnitude diagram, the 2% transit — lives entirely on measured brightnesses. A smoother picture that shifts a star by half a percent would be worse than useless: it would be wrong in a way that still looks right.

So the test to run before this is allowed anywhere near the pipeline is not a visual comparison. It is the convergence curve already used elsewhere on this site — stack error against frame count — measured with and without denoising, to see whether the network genuinely buys frames or merely launders noise into smoothness. Alongside it, aperture photometry on the same stars before and after, which is the measurement that would catch the failure the pretty picture hides.

The papers

The original is Lehtinen, Munkberg, Hasselgren, Laine, Karras, Aittala and Aila, Noise2Noise: Learning Image Restoration without Clean Data (ICML 2018) — the paper that established you can drop the clean target, demonstrated on photographic noise, Monte Carlo rendering and MRI reconstruction. It is unusually readable for the strength of what it claims.

Two successors push the same idea further, and are worth knowing about because they remove the one requirement this observatory happens to satisfy for free. Noise2Noise still needs two noisy views of a scene. Krull, Buchholz and Jug, Noise2Void — Learning Denoising from Single Noisy Images (CVPR 2019), get there with one image by predicting each pixel from its neighbours and never letting the network see the pixel it is predicting. Batson and Royer, Noise2Self: Blind Denoising by Self-Supervision (ICML 2019), generalise that into a framework needing no prior on the signal, no noise estimate and no clean data at all. For a microscopist with one irreplaceable image these are the important papers. For a telescope that produces three hundred views of the same sky every clear night, the original is the right tool, and its stronger assumption costs nothing.

How many hours is it actually worth? Reading the night-planning chart

2026-08-04 · NIGHT PLANNING · 9 NIGHTS · 330 FRAMES · 7 SERIES, 2 GATES

Abstract

The chart behind the question “how many hours is this target actually worth tonight?”. The headline is that the naive answer, time above altitude zero, is wrong by roughly a factor of two here, because trees put the local horizon at 46–65 degrees along the target’s track. Then every plotted series and which of them can actually stop the night: only two of seven gate anything. Most of the space goes to the seeing forecast, where the standard amateur proxy — the jet stream — measurably does not work at this site, and a much lower layer does.

The question the chart answers

Before the roof opens, something has to decide whether tonight is worth the trouble and which target gets it. That reduces to a number — how many usable hours this object has — and a verdict on whether the weather permits any of them. This is the chart behind both.

Night planning chart for SH2-92 showing target altitude, local horizon, moon, cloud, humidity, wind, precipitation, smoke and seeing wind against time
SH2-92 on a real night. Start 22:04, finish 01:57, elapsed 3h53m, air mass 1.03 at best. Seven series on one pair of axes, all rescaled onto a 0–90 range so they can share the altitude axis. Click for full size.

The heavy purple curve is the target's altitude, and the grey bands are darkness. The naive answer to "how long is it up?" is where purple sits above zero, which here would be most of the night. That answer is wrong by a factor of two, and the reason is the thick green line.

The horizon is not at the horizon

That green trace is the local horizon, and it is nowhere near zero — it wanders between about 46° and 65° over the course of the night. It is not a weather series at all: it is a survey of the trees and buildings around this observatory, 33 measured altitude readings at fixed compass bearings. As the target moves across the sky its azimuth changes, so the obstruction beneath it changes, and the green line is the profile sampled along the path the target actually takes.

Polar sky chart with the local horizon in red and the blocked region shaded, showing only a small usable region near the zenith and toward the south
The same horizon in plan view. Centre is the zenith, the edge is the true horizon, and everything pink is blocked. The white region is the entire sky this observatory can actually see; blue dots are the target's track across the night.

It is a humbling picture. Due north the trees reach 82.8° — less than eight degrees short of straight up — so the northern sky, Polaris and the circumpolar objects with it, simply does not exist here. The best direction is due south at 25.1°. This is a site that observes through a ragged hole, and any planning that assumed a flat horizon would schedule hours of imaging into a tree.

So the usable window is the intersection of two conditions: the target above the local horizon, and the sky astronomically dark. For this night that is 22:04 to 01:57 — 3 hours 53 minutes, against a target that peaks at 76°. The other number in the title, air mass 1.03, is the thickness of atmosphere being looked through, relative to straight up, and it is simply 1/sin(altitude): 1.0 at the zenith, 2.0 at 30°, and rising sharply after that. It ranks the quality of those hours where the elapsed time only counts them.

The seven series, and which of them can actually stop the night

This is the part that surprises people, including the person who wrote it. Seven quantities are plotted. Two of them can veto a night.

SeriesPlotted asGateEffect
Cloud coverred, % of 90> 80%BLOCKS
Precipitation probabilitypink, % of 90> 20%BLOCKS
Surface windblack, % of 40 km/hnonedisplayed only
Humiditythin green, % of 90nonedisplayed only
Moon altitude & phasebluenonedisplayed only
Smoke (PM2.5 AQI)brown, AQI/150advisory at 60warns, never blocks
Seeing wind (850 hPa)orange, km/h ÷ 60label at 20 / 30describes, never blocks

Cloud above 80% and rain above 20% are the whole gate. Everything else is information for a human, and the restraint is deliberate in each case. Surface wind is drawn but unused — the function that decides even accepts a wind argument and never reads it, which is honest if untidy: a roll-off roof observatory has a real wind limit, but it has never been measured here, and a threshold invented at the keyboard would either block good nights or fail to block dangerous ones.

Smoke earns a mention because it used to gate and no longer does. It was removed on 27 July 2026 after blocking a night on a composite air-quality index of 102 that turned out to be almost entirely ground-level ozone — a gas that does not scatter or absorb visible light and has no effect whatsoever on transparency. The reading that mattered, the PM2.5 sub-index, was 61. The report now uses the particulate sub-index only, always states it, warns above 60, and blocks nothing, on the grounds that the threshold was never validated against a labelled night.

The seeing wind, which is the interesting one

Astronomical seeing — the atmospheric blurring that sets how small a star can be — is the quantity an imager most wants forecast and the one hardest to get. The standard amateur proxy is the jet stream, the 250 hPa wind about nine kilometres up, and there are websites devoted to overlaying it on a map. That is what this system used.

It was the wrong layer for this site, and nine nights of its own data say so. Every sub-exposure already carries a measured star width, so 330 frames of SH2-92 across nine nights could be checked against the forecast wind at every available altitude. Correlating median FWHM against wind speed level by level gives this:

Pressure levelHeightSpearman ρ vs FWHMp
Surfaceground+0.730.031
850 hPa~1.5 km+0.870.005
700 hPa~3 km+0.730.031
500 hPa~5.5 km+0.650.067
300 hPa~9 km+0.370.336
250 hPa — the jet stream~10 km+0.330.385
200 hPa~12 km−0.180.644

The jet stream — the thing everyone forecasts seeing from — has essentially no relationship with how sharp the stars are here. The 850 hPa wind, a kilometre and a half up, has a strong one.

What makes that believable is not the top value but the shape of the column. The correlation decays monotonically with height, from +0.87 at the bottom to nothing at the top, and noise does not sort itself by altitude. There is also a physical reason to expect exactly this. Jet-stream seeing forecasts were developed for professional observatories on mountains, which sit above the turbulent boundary layer, so for them the only turbulence left to worry about is high aloft. A backyard observatory near sea level is inside that boundary layer, where the turbulence that bloats a star is local and low. The site determines which layer matters, and the received wisdom was written for the other kind of site.

How the thresholds were set

Having found the right variable, the labels on it were measured rather than borrowed. Here is the per-night table — the one that shows what each wind level actually did to the stars.

NightFramesMedian FWHM850 hPaJet 250RHCloud
2026-07-23461.75″4.447.783%0.0%
2026-07-31451.73″8.863.493%16.6%
2026-07-12551.97″4.335.782%12.0%
2026-07-11351.98″9.586.081%2.8%
2026-07-25482.00″10.240.081%25.5%
2026-07-19432.46″9.360.359%0.6%
2026-08-03362.74″17.191.378%0.8%
2026-07-1492.81″39.954.071%1.2%
2026-07-13112.98″22.567.169%15.8%

Sorted by star width, the 850 hPa column sorts with it and the jet column does not. The three worst nights are the three windiest at 1.5 km, and they separate cleanly: every night under about 22 km/h landed between 1.73″ and 2.46″, every night above it between 2.74″ and 2.98″, with no overlap at all. Note the 14 July row — a jet of 54 km/h, which is unremarkable, and an 850 hPa wind of 40 km/h, the highest in the set, on the second-worst night. The jet-stream forecast would have called that a good night.

The published thresholds are then deliberately not placed at the 22 km/h split: "good" ends at 20 and "poor" begins at 30, with everything between reported as "fair". That gap is exactly where this site has no data, and labelling it "fair" says so, rather than pretending a nine-night sample can locate a boundary to the kilometre per hour.

What the same analysis says not to trust

Two results in that run are worth keeping visible because they are cautionary. Relative humidity correlates with FWHM at −0.88 — humid nights are sharper, and it is the single strongest weather relationship in the set. It would be very easy to add a humidity gate pointing the wrong way. The likely explanation is that calm, humid, hazy air is thermally settled while the dry nights here arrive on wind, which is the thing that actually hurts — humidity is riding along with wind rather than causing anything. Detected star count correlates at −0.95, which is near-perfect and completely circular: blurrier stars means fewer of them clear the detection threshold. It is a sanity check that the pipeline is measuring what it thinks, not a finding.

And the honest statistical caveat, which the analysis script prints itself: about sixteen variables were tested against nine nights. Correcting for that many comparisons puts the bar at p < 0.0031, and the 850 hPa result at p = 0.0045 does not clear it. The monotonic decay with altitude and the boundary-layer argument are doing more work here than the p-value is. Nine nights is a thin calibration; the labels are a steer, not a promise, and the script that produced all of this is kept in the repository specifically so the whole thing can be re-run and overturned once there are more nights.

Closing the loop: autonomy has to include the data reduction

2026-08-04 · SH2-92 · 330 SUBS · 9 NIGHTS · 27.5 HOURS

Abstract

Autonomy that stops at the shutter is not autonomy. This follows one target — 330 subs of SH2-92 over nine nights, 27.5 hours — through the half of the job that usually stays manual: knowing what the night actually delivered, and knowing when more frames stop being worth taking. Covers the per-frame statistics the observatory records about its own output, and the convergence measurement that answers when to stop.

The gap

Iris was already unattended in every part of the job that involves moving hardware. It picks the night's target from the queue by what is actually observable, waits for the sun, checks that it is safe to open, opens the roof, slews, images, watches for trouble, and parks and closes at the end. None of that needs a human. What it produced, though, was a directory of raw FITS files — and a directory of raw FITS files is not a result. The last step was still manual, which meant the observatory could run itself all night and then wait days for someone to sit down with it.

There are really two things missing in that gap, and only one of them is the picture. The obvious one is reduction: calibrating, registering, and stacking hundreds of subs into an image, then stretching it so the faint structure is visible without blowing out the stars. The less obvious one is the decision. An autonomous scheduler has to answer “is this target finished, or does it want another night?” every single evening, and until it can measure that, it is just guessing — either quitting while more exposure would still have helped, or grinding away on a target that stopped improving a week ago while the queue backs up behind it. SH2-92 is the first target taken all the way through both.

The target

SH2-92 is entry 92 in Stewart Sharpless's 1959 catalogue of H II regions — clouds of interstellar hydrogen made to glow by nearby hot stars. It sits in Vulpecula at 19h47m +28°10′, right down in the plane of the Milky Way, and it is enormous: over 200 light years across, which is one reason it is so diffuse and so rarely photographed. Distance estimates disagree, putting it somewhere between about 12,000 and 14,300 light years, plausibly out at the far edge of the Orion Arm. The star doing the ionising is thought to be WR 127 — a Wolf-Rayet star, one of the short-lived, furiously hot objects a massive star becomes as it sheds its outer layers — and it falls inside this frame, eleven arcminutes from the catalogue centre.

An object like this is a good test of the whole pipeline precisely because it is unspectacular. There is no bright core to anchor on. The signal is a faint wash spread over the entire field, so it only separates from the sky background with a great deal of integration, and any error in the calibration or the stretch shows up immediately as false structure in what should be smooth gas.

SH2-92, a faint H II region in Vulpecula, imaged in hydrogen-alpha and oxygen-III
SH2-92. 330 five-minute subs — 137 Hα and 193 O III — over nine nights from 2026-07-11 to 2026-08-03, 27.5 hours of integration, on a 17-inch CDK at f/6.8 with a mono CMOS camera. Rendered in the HOO palette: hydrogen-alpha drives the red channel, doubly-ionised oxygen drives green and blue, so red is where hydrogen is recombining and teal is where the gas is hot enough to have stripped oxygen of two electrons. Click for full size.

Knowing what the night actually gave you

Before any of those frames can be stacked, something has to judge them. Every sub is measured as it lands and the numbers are cached, which turns a nine-night run into four time series — and those series are more honest about observing conditions than memory is.

Four-panel plot of FWHM, eccentricity, sky brightness and star count across 328 frames of SH2-92
Every frame of the campaign, in order. Vertical shading separates calendar nights; red points are Hα, orange are O III. Click for full size.

FWHM — the full width at half maximum of a star's profile, in arcseconds — is the seeing measurement, and it is the one that decides whether a frame is worth keeping. A star is a point source; anything more than a point is atmosphere. The median here is 1.99″, but the panel makes the spread obvious: some nights sit flat around 1.7″ and one runs 2.5″ and rising. Those soft frames are not merely less good, they are actively harmful in a stack, because averaging a sharp frame with a bloated one gives you a bloated result.

Eccentricity measures how far from circular the star images are, and it separates atmosphere from mechanics. Seeing blurs stars symmetrically; a mount that is tracking imperfectly, flexing, or fighting wind smears them into ellipses. A night where FWHM is fine but eccentricity climbs is a hardware complaint, not a weather one. The median of 0.395 with excursions past 0.55 is the honest signature of a real backyard mount.

Sky brightness in ADU per second is the background the signal has to compete with — moonlight, twilight, light pollution, high cloud. The sawtooth pattern is the most legible thing in the whole figure: each night begins bright and decays as astronomical twilight finishes draining out of the sky, then resets at the next sunset. The nights riding three to four times the median are the moonlit ones, and they cost real depth.

Star count is the blunt instrument that catches everything the other three miss. Detected stars fall when clouds roll through, when the focus drifts, when dew forms on the corrector. The frames near zero in that bottom panel are not marginal — they are frames where the sky closed, and no amount of processing recovers them. Taken together the four panels are what lets the stacker discard 64 of the 330 subs — nearly one in five — without a human ever looking at a single frame.

Knowing when to stop

That leaves the harder question. Stacking N frames beats stacking one, everybody knows that — random noise averages down as the square root of the number of frames, so four frames halve it and a hundred frames cut it by ten. But square-root improvement is brutally diminishing. Going from 4 frames to 16 is a big visible win; going from 100 to 112, one more clear night, changes almost nothing you can see. Somewhere in there is the point where the telescope should move on, and eyeballing the stack is a terrible way to find it.

So the pipeline measures it directly. It stacks every good frame into a reference — the “golden” stack, the best this data can do — and then asks how close it could have got with fewer. For each of a series of frame counts (1, 2, 3, 5, 8, 13, 21, 34…) it draws twenty random subsets of that size, stacks each one the same way, and measures the RMS difference from the golden, expressed as a percentage of the sky level. Plot that against frame count and you get a curve that starts high and falls toward zero, and the shape of it answers the question.

Stack convergence curve for the hydrogen-alpha frames of SH2-92
Hα: 118 frames survived the quality cut out of 137.
Stack convergence curve for the O-III frames of SH2-92
O III: 148 of 193. Note where the blue curve sits relative to the dotted line compared with Hα above.

Three things are drawn on each plot. The blue line is the measured residual, with the shaded band showing the spread across the twenty random draws — that band is the luck of which frames you happened to get, and it is wide at small counts and narrow at large ones, which is itself a good argument for more frames. The dashed red line is a straight fit to the tail, quoted in percent per frame: that single number is the decision. It is what one more sub is worth right now. When it flattens past a threshold — here 0.4% per frame — the target is done and the scheduler can retire it.

The dotted yellow curve is the interesting one. It is where the measured curve would sit if every frame's noise were completely independent of every other frame's — pure square-root averaging, anchored to this data's own single-frame noise. It is not an absolute floor; a bad night lifts both curves together. It is a statement about behaviour. Riding that line means the frames are averaging down exactly as they should. Sitting above it means some component of the error is common to all the frames, and a correlated error does not average away — you can shoot all year and it will still be there.

What came out

Both filters passed. Hα finished at a tail slope of 0.23% per frame and a residual of 6.4% of sky; O III at 0.20% per frame and 5.2%. Both are inside the thresholds, so SH2-92 reads as complete and the scheduler is free to move on — a judgement made from measurements rather than from somebody deciding the picture looked finished.

The two filters did not behave the same way, though, and that is the actual finding. The Hα curve rides the independent-noise line the whole way down: those frames are averaging as well as frames can. The O III curve runs 2.81× above it. Nearly three times more residual than independent noise explains means something systematic is riding along in the O III data, and the extra frames were not removing it. O III is the filter most exposed to gradients — moonlight and skyglow are far stronger there than at Hα — so a sky gradient that shifts between nights is the first suspect, with imperfect flat correction the second. Chasing that down is the next piece of work, and it is a good example of why the convergence curve earns its keep: the stacked O III image looks fine. The curve is what says it should have been better.

Teaching the observatory to hear its own roof failing

2026-08-03 · ROOF SENTRY · 67 GOOD SPECTROGRAMS · 38 GOOD CURRENT TRACES

Abstract

The roof is the one part of this observatory that can fail expensively and unattended, so it is watched by two independent detectors that share no failure mode: the motor’s electrical current signature, and the sound the move makes. Built from 67 labelled spectrograms and 38 current traces. Includes what the current watchdog has actually caught, why two detectors rather than one better one, and a correction to an earlier claim about an alert that turned out never to have fired on a real stall.

The thing that can actually break

Every other experiment on this site is about extracting more from the data. This one is about the observatory not destroying itself. The roof is a motorised panel that slides off the building, and it is the only part of the system with enough mass and torque to wreck the telescope. Two rules are absolute: never move the scope unless the roof is confirmed open, and never move the roof unless the scope is confirmed parked. A roof that closes onto a telescope pointed at the zenith is the end of the telescope.

Those rules protect against the software being wrong about state. They do nothing about the mechanism itself degrading — a drive gear that stops engaging, a wheel going out of true, ice, a bearing starting to bind. Failures of that kind announce themselves for weeks before they strand the roof half-open over an exposed telescope, but only to somebody standing there listening. For an observatory whose entire point is that nobody is standing there, the interesting question is whether the roof can be made to report its own health. It turns out to have two voices: what it sounds like, and what it draws from the wall.

Voice one: the current signature

The roof motor is powered through a relay with an energy monitor, so a move can be sampled at 20 Hz across its travel window and stored as a trace of real power against time. The move is command-triggered, which makes this much easier than it sounds — there is no need to detect that the roof is moving, because the software is the thing that started it. The trace of a healthy move is remarkably repeatable.

Roof motor power against time for an open and a close move, both about 12 seconds
A healthy open and close. Idle draw is about 2.7 W; the motor pulls roughly 300–350 W for some eleven seconds and returns to baseline. Note the open trace's brief spike above 400 W at the start — that is breakaway, the extra torque needed to get a stationary roof moving.

Each trace is reduced to five scalars — running power, running current, move duration, total energy, and peak power — and each new move is checked against the mean and standard deviation of the good library for that direction. Anything more than three sigma out gets flagged. The features are chosen to be physically interpretable rather than merely discriminative, which matters when the alert wakes you at two in the morning: a roof binding on ice draws more running power, a roof that stops early has a short duration, and a drive gear that never engages spins the motor without doing work.

That last one is dangerous enough to get its own live watchdog rather than after-the-fact analysis. A normal move runs the motor for about eleven seconds, so if it is still drawing more than 20 W thirty seconds after the relay fired, the roof is not travelling. The system cuts motor power at the smart plug, sends a priority-2 push notification, banks the current trace and the audio as evidence, and refuses to proceed — the roof state is now unknown, and unknown must not be followed by a scope move. Two consecutive over-threshold readings are required so one bad sample cannot cut power spuriously, and a failed read never triggers, so losing the monitor degrades to a plain timed wait rather than a false emergency.

What it has actually caught

A flagged roof open overlaid on three good moves, showing an elevated startup surge but a normal plateau
A flagged open (red) against three good moves from the same day. The running plateau is entirely normal; the divergence is confined to the startup surge.

This is the failure mode the envelope keeps finding, and it recurs: peak power 721 W against a good mean of 398 on 19 July, 759 W against 393 on 30 July, 741 W against 398 on 3 August — each time with running power and duration inside tolerance. A move that needs twice the normal force to break away but then runs normally is the signature of something binding at rest and freeing once moving: stiction, thermal contraction, debris on the rail. Every capture also records observatory temperature and humidity, specifically so that hypothesis can eventually be tested against the weather rather than argued about.

Power deviation from the plateau median during a close, showing much larger ripple than the good library
A different fault entirely: a bent wheel. Plotted as deviation from each move's own plateau median, so the comparison is about ripple rather than level. The good closes stay within roughly ±25 W; this one swings from −43 W to +52 W as the damaged wheel comes round.

That one is worth dwelling on, because it is the case the scalar envelope is blind to. Its running power, duration and energy are all perfectly normal — the average work done is unchanged. What gives it away is the structure within the plateau, a periodic surge as the out-of-true wheel rotates. Reducing a trace to five numbers throws exactly that away. Curve-shape comparison is the acknowledged next step and is not built yet.

Voice two: the audio

Which is where the microphone comes in. The current monitor sees the motor's gross mechanical load at 20 Hz; it is comprehensively deaf to grinding, chattering, scraping and knocking — the sounds that a human would identify instantly as a mechanism in trouble, and which barely move the power draw at all. So a roof move also records audio for its duration, and the recording is converted to a mel spectrogram: a picture of which frequencies were present when, on a scale spaced the way human hearing is. Turning sound into an image is the useful move here, because it converts “does this sound wrong” into a question about pictures.

Mel spectrogram of a healthy roof open: one clean burst then silence
Healthy open: one burst of broadband noise while the motor runs, then quiet.
Mel spectrogram of a bad roof close: a long smear plus extra bursts after the move should have ended
A close labelled bad: the burst is longer and louder, and there are further bursts well after the move should have finished.

The classifier is deliberately unsophisticated: compare the new spectrogram against every known-good one for that direction by pixel-wise similarity and keep the best match. The interesting part is the threshold, which is not a hand-tuned constant. It is derived from the library itself — take the worst similarity between any two known-good moves, and require the new move to be at least 90% as similar as that. In other words: a move is acceptable if it looks about as normal as good moves look to each other. The threshold retunes itself as the library grows, and there is no magic number to maintain.

The library is self-extending too. A move judged good is filed into the reference set automatically, on a rolling cap of 40 per direction — the cap exists because the comparison is pairwise over the whole library, so an unbounded set makes every roof move slower forever. Only captures that come back bad or unknown wait for a human to label them with a chat command. That inverts the usual labelling burden: the system handles the common case and asks for help exactly when it is confused. The library currently holds 44 good opens and 24 good closes.

Why two detectors instead of one

Because they fail in opposite directions, and neither is close to sufficient alone. Sampling power at 20 Hz gives an excellent account of sustained mechanical load and no account whatever of transient acoustic texture; a gear chattering for half a second is invisible in the power trace. The microphone is exquisitely sensitive to that chatter and would happily flag a passing aircraft, a rainstorm, or a neighbour's dog. Requiring the roof to look normal in both a physical-load sense and an acoustic sense is much harder to satisfy by accident than either test alone, and the two disagreeing is itself informative — normal power with abnormal sound is a mechanical texture problem, abnormal power with normal sound is load.

What does not work yet

The honest list is longer than the successes. Pixel-wise image comparison of spectrograms is a crude similarity measure that treats a small time shift as a large difference, and it will plateau; the intended path is comparison in a feature or embedding space rather than raw pixels. A scratch-trained convolutional network was considered and rejected as the wrong tool — this is a one-class problem with a few dozen examples, which is where small networks memorise rather than generalise. The current-signature envelope cannot see plateau structure, as the bent wheel demonstrates. And the startup-surge anomaly has now recurred four times without a diagnosis; a detector that keeps flagging the same unexplained thing is doing half its job.

The most instructive failure is not in the classifiers at all. Rehearsing the stall watchdog against a stub notifier produced the log line roof stall: pushover emergency failed — the safety action completing correctly and the alert not going out. The immediate cause was trivial and confined to the test harness, but it prompted a look at the real notifier, which turned out to have the more serious version of the same problem: it posted the alert and discarded the reply.

That matters because Pushover answers a bad token, a malformed emergency request, or an exceeded quota with a perfectly healthy HTTP response whose body says status: 0. Never reading the body made an undelivered alert indistinguishable from a delivered one, and transport errors were swallowed into a print, so the caller logged success either way. An unattended observatory that protects itself correctly and cannot tell anyone has solved the easier half of the problem — and it would have had no way of knowing. Delivery is now verified, emergencies are retried, and the stall path logs loudly when the roof motor has been cut but nobody could be reached (aef2169).

Which is the argument for rehearsing safety paths rather than waiting for them. The watchdog has never fired on a real stall — the one genuine gear-not-engaging event predates it. A drill on a path that is supposed to never run is the only way to find out that the part after the emergency was broken.

The camera that has to be right about the roof

2026-08-03 · VISION SAFETY · 14 EXPOSURE LADDERS · 3 MARKERS

Abstract

Both hardware safety rules turn on the word confirmed, and software that believes the roof is open because it sent an open command has confirmed nothing. Ground truth comes from a webcam and three markers. The core argument is why a limit switch cannot do this job: a switch does not know when it is broken, and a dead one keeps reporting open or closed in the same tone of voice it used when working. A camera can say “I cannot tell”, and that third answer is the whole point. Also: why match confidence turned out to be useless for separating the states, and a bug that is deliberately left unfixed.

Why a camera and not a switch

The roof sentry watches for the mechanism degrading. This is the other half of the same problem: knowing what state the observatory is actually in. Two rules govern every hardware movement — never move the scope unless the roof is confirmed open, never move the roof unless the scope is confirmed parked — and the word doing the work in both is confirmed. Software that believes the roof is open because it sent an open command has not confirmed anything. It has remembered its own intent, which is exactly the belief that gets a telescope crushed.

The obvious fix is a limit switch. Put one at each end of the roof travel, another on the mount, and read them. They are cheap, they are what every garage door in the world uses, and they answer the question directly. The reason there is a camera instead comes down to a single property: a switch does not know when it is broken.

A switch has two outputs and no vocabulary for anything else. Corrode a contact, shear a wire, knock the actuator out of alignment, let a spider build a nest in the gap — and it does not fall silent or report an error. It goes on faithfully returning open or closed, with exactly the same confidence it had when it was working, and nothing downstream can tell the difference. A disconnected wire is not a null reading; on most wiring schemes it is one of the two valid states. The failure mode of a switch is not silence. It is a lie told in the same tone of voice as the truth.

And the lie is discovered at the worst possible moment, because the only thing that reveals it is the mechanism doing something unexpected — which here means the mount slewing under a roof that a broken sensor cheerfully certified as open. The interlock designed to prevent that collision is the thing that caused it.

A camera does not have that property, and not because it is more accurate. It is because a camera produces evidence, and evidence is allowed to be inconclusive. That buys a third answer the switch can never give: open, closed, and I cannot tell. If some exposures in the ladder read open and others read closed, the verdict is not a majority vote — it is unknown. If the snapshot comes back unreadable, unknown. If the scope is not confirmed parked, the roof is not read at all, because the marker geometry only means anything in the parked position.

That third answer is the entire point. The two rules are written in terms of confirmed, and "I cannot tell" is simply not confirmed, so the system refuses to move and says why. Refusing is a safe outcome: the worst case is a clear night wasted, and a clear night is cheaper than a telescope. A broken switch has no way to reach that outcome, because it never produces the input that would trigger it.

None of which makes the camera infallible — it can be confidently wrong too, and later sections here are mostly about the ways it was. The difference is narrower than "better sensor" and more useful than it sounds: the camera's failures are the kind a system can be built to notice, and the switch's are not. For an interlock, a sensor that fails loudly beats one that is right more often.

So the ground truth comes from a webcam bolted to the top of the telescope, looking across the inside of the building. Three markers are stuck to things whose positions encode the state: one on the roof panel, one visible only when the roof is open, one on the mount that lines up when it is parked. Confirmation is then a question about pictures — find each marker, and check it is where that state requires it to be.

Annotated view from the scope-top camera showing the observatory interior with three matched marker regions boxed
What the safety camera sees, with the three matches drawn on. The telescope is the black tube at bottom; the roof rail runs across the top. Green is the open marker, red the closed marker, white the parked marker. Click for full size.
The open marker template
open
The closed marker template
closed
The parked marker template
parked

The matching itself is deliberately old-fashioned: normalised cross-correlation of each template against the frame, which returns the best-matching location and a confidence between 0 and 1. No neural network, nothing learned, nothing that can be confidently wrong in a novel way. For a safety interlock, a method whose failure modes are boring is worth more than one with a better average score.

The problem: no exposure works for the whole scene

This is where it stops being simple, and it is entirely a problem of camera sensitivity. The scene contains, simultaneously, a marker sitting in deep shade under the roof rail and — when the roof is open in daylight — a hole onto a sunlit sky. That is a dynamic range no consumer webcam sensor can capture in one frame. Expose for the sky and the marker is black. Expose for the marker and the top half of the frame is a white rectangle. There is no exposure that gets both, so "pick a good exposure" is not a strategy, it is a wish.

Worse, the obvious way to choose fails in the most dangerous direction. The original code swept a range of exposures and scored each frame the way a photographer would: mean brightness, contrast, how much of the image is clipped. That metric picks the frame a human would call correctly exposed. Look at what that means here.

A well-exposed frame of the open roof: trees visible through the opening, but the marker in shadow is unreadable
Exposure −11. Nicely exposed, nothing clipped, and a human can see instantly that the roof is open. The open marker, in shadow on the rafter at upper left, did not match: 565 px from where it belongs.
A badly overexposed frame of the same scene, but the shadowed marker is now legible
Exposure −7, same scene minutes later. 82% of the frame is clipped and it looks ruined. The marker is legible. Matched at 29 px from its expected position — roof confirmed open.

The frame that looks worse is the one that carries the answer. Judged on whole-frame quality, exposure −7 scored −2.34 — nearly the worst rung on the ladder — so the sweep discarded it and chose a clean, well-exposed frame in which the roof could not be confirmed open. The observatory then declines to move the scope, which is safe but useless: an autonomous telescope that cannot verify its own roof cannot observe.

The fix was to stop scoring the photograph and start scoring the evidence. The replacement metric ignores brightness and contrast entirely and asks only: how many of the three markers match, near where they are expected, and how strongly? Summing the confidences is the important detail — it prefers the frame where the most markers are simultaneously readable rather than the one where any single marker is sharpest. On the roof-open ladders it selects exposure −7 (parked 0.91 + open 0.66 = 1.57); on roof-closed ladders it selects −7 again (parked 0.88 + closed 0.91 = 1.79). Both verdicts correct, on a metric that would horrify a photographer.

Confidence is not the thing that separates the states

A result worth stating on its own, because it is the opposite of the obvious assumption. There is a minimum match confidence in the config, and the instinct when a check misbehaves is to tune it. The ladder data says that instinct is wrong. On a roof-open ladder the open marker matches its true position at confidence 0.66 — while on the very same ladder, at other exposures, it matches the wrong place entirely, 568 px away, at confidence 0.68 and 0.70.

The bogus matches score higher than the real one. Any threshold that accepts the true detection also accepts several false ones, and any threshold that excludes the false ones excludes the truth. Confidence cannot separate these states at all. Position does, and by an enormous margin: correct matches land 17–30 px from where they belong, and wrong matches miss by 120–700 px. There is no ambiguous middle. So the gate is geometric, and the confidence threshold has been left alone deliberately.

Voting the whole ladder

The deeper fix was to stop choosing a frame at all. The sweep captures ten exposures; every one of them is evidence. Rather than nominating a representative frame and reading the verdict off it, each rung is matched independently and the verdict is counted across all ten.

The thresholds are asymmetric, and the asymmetry is measured rather than assumed. A single rung is enough to call the roof open, which sounds reckless until you look at the ladders: the open marker is readable in a far narrower exposure band than the closed one, and in seven of eight roof-open sweeps it resolves on exactly one rung. Requiring two votes would fail nearly every roof-open check. What makes one vote safe is not the count but the requirement that the opposing state score zero — and across all fourteen ladders it does: the closed sets scored closed 3–5 and open 0; the open sets scored open 1–5 and closed 0. Never once did both states get a vote.

Parked needs three rungs, for the opposite reason. The sweep deliberately spans exposures that are useless at both ends, and in daylight the entire bright half of the ladder blows out; two real roof-open sweeps read parked on exactly five of ten rungs and nothing at all on the top five. A majority rule would have declared a parked scope unparked. Counting is the right test, because three independent exposures putting the marker within tolerance of the parked position is not a coincidence when wrong matches miss by hundreds of pixels.

The bug that must not be fixed

The function that locates a match computes its centre as (x + width) / 2 where it should be x + width / 2. That is straightforwardly wrong, and it halves the apparent displacement of anything measured with it — a marker that has really moved 300 px is reported as having moved 150. The tolerance is therefore about twice as loose as it reads.

It has not been corrected, and that is the right call for now. Every reference position in the config was measured through the same function, so the error cancels: the stored expectation and the live measurement are wrong in identical ways, and the comparison between them holds. Fixing the formula without simultaneously re-measuring all three reference positions would break a working safety check in the name of correctness. The exposure scorer carries an explicit comment forbidding an independent fix there, for exactly this reason. It is a real bug, it is logged as one, and it is a good illustration that in a calibrated system the unit of correctness is the loop, not the line.

Where it stands

Replaying the vote over every ladder captured so far gives the right answer on all fourteen, including the daylight roof-open cases that used to fail, and the roof was confirmed open in full daylight for the first time on 3 August 2026. The honest caveats: those fourteen ladders are a small sample from one camera in one building, nothing has been tested through rain, snow on the marker, or a spider deciding to nest on the lens, and the whole scheme depends on physical stickers staying stuck. The failure mode of a marker falling off is at least the safe one — nothing matches, no state is confirmed, and the observatory refuses to move.

Weighing a star cluster's age with a colour–magnitude diagram

2026-08-02 · M13 · HERCULES GLOBULAR CLUSTER · 18 BLUE + 20 RED SUBS

Abstract

Building a colour–magnitude diagram of M13 from 18 blue and 20 red subs, calibrated against Gaia, and reading a cluster age off the main-sequence turn-off. Why a globular cluster is the right target for this — every star the same age and distance, so the only variable left is mass — how the photometry was assembled, and what did not work.

M13, the Hercules globular cluster, imaged from the Iris observatory
M13 as Iris sees it. Everything below is extracted from these stars — same photons, different question.

What an H–R diagram depicts

Around 1911, Ejnar Hertzsprung and Henry Norris Russell independently did something that sounds trivial and turned out to be the single most useful plot in stellar astronomy: they graphed stars' brightness against their temperature. The result was not the shapeless cloud you would get from plotting two unrelated properties. Almost every star fell on a narrow diagonal band running from hot-and-brilliant down to cool-and-feeble — the main sequence — with a sparse population of cool but very luminous stars off to one side, which had to be enormous to be that bright while being that cold. Those are the giants.

The band exists because a star on the main sequence is doing one thing: fusing hydrogen in its core, with its own weight setting the pressure and therefore the rate. Mass fixes everything else. A star ten times the Sun's mass is hotter, bluer, thousands of times more luminous — and it burns through its fuel so extravagantly that it lives only tens of millions of years, while a red dwarf will still be going long after the galaxy stops making new stars. So position along the main sequence is really a mass axis, and the fact that a star has left the main sequence is a statement about time.

A telescope cannot measure luminosity or temperature directly. What comes out of an image is an apparent magnitude — how bright the star looks from here — and, if you shoot through two filters, a colour index: the difference between its brightness in blue light and in red light. Hot stars put out proportionally more blue, cool stars more red, so that difference is a thermometer. Apparent brightness is the harder half, because a faint star may be genuinely dim or merely distant. Plotting the two observable quantities instead of the two physical ones gives a colour–magnitude diagram, the CMD: the same picture as an H–R diagram, shifted by however far away the stars happen to be.

Why a globular cluster makes this work

That distance problem is why this experiment targets M13 rather than a patch of random sky. Its several hundred thousand stars sit roughly 25,000 light years away in a sphere about 145 light years across — which means the near side and the far side differ in distance by well under a percent. Every star in the frame is effectively at the same distance, so every apparent magnitude is offset from the true luminosity by the same constant. The shape of the diagram survives intact. Better still, the cluster's stars all formed at about the same time, some 11.65 billion years ago, out of the same material.

One age, one distance, one composition: that turns the diagram into a clock. Over 11.65 billion years, every star heavier than about the Sun has already exhausted its core hydrogen and moved off to the giant branch, so the main sequence is not a full diagonal — it is a diagonal with the top cut off. The place where it bends away, the turn-off, marks the stars just now running out, and their mass is a direct readout of how long the cluster has been running. An old cluster's turn-off sits low and red; a young one's sits high and blue. Read the corner, get the age.

How Iris built it

The observatory does this from the frames above, via the hr command (photometry/cmd_diagram.py). The blue and red subs are calibrated and stacked separately; each stack is plate-solved with ASTAP to establish exactly what sky coordinate every pixel corresponds to; stars are detected and aperture-photometered in both stacks; and the two star lists are cross-matched by position on the sky, so each star ends up with one brightness in each filter and therefore one colour.

Raw instrumental magnitudes are arbitrary — they depend on aperture, exposure, and the sensor — so the field is then cone-searched against the Gaia DR3 catalogue and the stars in common are used to pin the instrumental scale onto Gaia's: blue onto BP, red onto RP. Here that took 6,260 stars in common, which is a comfortable margin. Gaia also supplies proper motions and parallaxes, and cluster members betray themselves by moving together and sitting at a common distance, so the foreground and background field stars can be identified and stripped out rather than smeared across the diagram.

Four-panel colour-magnitude diagram of M13 built from Iris photometry and calibrated against Gaia DR3
Click for full size. M13, colour (B−R) against magnitude, on the Gaia photometric scale. Brighter is up; hotter is left; each point is coloured by its own colour index. Top left: all 6,948 stars Iris measured. Top right: the 7,767 Gaia sources whose motion and parallax mark them as cluster members. Bottom left: Iris's stars with the field population stripped out — 4,772 survive. Bottom right: those overlaid on the Gaia member sequence.

What came out

The main sequence is unmistakable — the dense near-vertical pile at colour ~1 — and it terminates rather than continuing upward, bending left and running out at around magnitude 18. Above that bend, the red giant branch sweeps up and to the right for a good six magnitudes, thinning as it climbs because a star crosses that region quickly. That sweep is the last few hundred million years in the life of stars that spent ten billion sitting quietly on the main sequence, and it is resolved here from a backyard observatory.

The comparison between the panels is the other half of the result. Stripping the field costs about 2,200 of the 6,948 measured stars, and the residual scatter in the top-left panel — the loose spray of red points to the right of the giant branch — largely disappears with them, confirming they were contaminants rather than bad photometry. Where Iris falls short of Gaia is in the crowded core: the bottom-right overlay shows the measured points thinning out against the Gaia sequence at the faint end, which is what happens when stars blend together at this plate scale.

What did not work

The pipeline is supposed to locate the horizontal branch and the main-sequence turn-off automatically and read a rough age off the magnitude gap between them. On M13 it found the giant branch and nothing else — the annotated panel carries one label instead of three, and no age was reported. The turn-off finder expects the horizontal branch to show up as a tight clump of blue stars at one magnitude, and M13's does not oblige: there is a broad blue plume running from about magnitude 14 down past 18 at colour ~0, with no single pile-up to lock onto.

That plume is worth its own investigation. It is present in the Gaia-only panel as well as in the measured one, so it is not an artefact of this telescope's photometry. The likely reading is that it is M13's genuinely extended blue horizontal branch, stretched further by the choice of plotting against RP magnitude — very hot stars emit weakly in the red, so a blue horizontal-branch star slides downward on an RP axis in a way it would not on a visual one. Confirming that, and teaching the branch-finder to cope with a cluster whose horizontal branch is a smear rather than a clump, is the open item.

Has anything changed, or was it just a bad night?

2026-08-04 · OPTICS TREND · 1,800 STARS POOLED FROM 12 FRAMES

Abstract

A command for answering “has the optical train drifted, or was that just a bad night?”, and the reason it is harder than it looks. The obvious metrics move dramatically with seeing alone — field uniformity by +88%, coma by −41%, with nothing touched between measurements — so they cannot be trended as they stand. Covers the replacement metrics designed to hold still under changing seeing, what one night looks like, and why nothing yet consumes the numbers: thresholds invented before a baseline exists are guesses.

The dull command

The optics command is the least glamorous thing in this system. It takes a frame, fits every star it can find, and reports on how the telescope is performing as an optical instrument rather than on anything in the sky. No discoveries here. It is the equivalent of checking your tyre pressures.

Optical quality summary table: CV FWHM, CV eccentricity, tilt, coma and collimation scores against good and bad thresholds
The summary: 695 stars, median FWHM 1.81″, median eccentricity 0.408. Green is fine, amber is marginal, red wants attention.

The interesting part is not the summary but the maps behind it, because a telescope's faults are spatial. A single median FWHM for the whole frame throws away the one thing that identifies what is wrong. Split the sensor into a grid and the character of the problem appears.

8x8 grid of median FWHM across the sensor, showing a gradient from 1.71 arcsec upper left to 2.16 lower right
Star sharpness by position. The best patch runs 1.71″ up and left of centre; the bottom-right corner reaches 2.16″. A smooth gradient across the field like this is the signature of tilt — sensor and focal plane not quite parallel — and not of the atmosphere, which has no reason to prefer one corner of a chip. Click for full size.
Map of star elongation direction and magnitude across the frame; stars are round at left and increasingly elongated toward the lower right
Which way each star is smeared, and how much. Round and randomly oriented on the left, increasingly elongated toward the lower right, and — the diagnostic detail — the elongations there line up with each other. Click for full size.

That distinction between how much and which way is what makes the map worth having. Optical aberrations have geometry: coma smears stars radially, away from the optical axis, so the pattern points outward like a starburst. A mount that slipped, wind shake, or a cable dragging on the pier smears every star in the same direction regardless of where it sits on the sensor. Same blur, same eccentricity number, completely different culprit — and only the angles tell them apart.

Why this cannot be trended as-is

The obvious next thought is to log these numbers nightly and watch for drift. Something changes — a mirror shifts in its cell, a focuser sags, the dog eats a cable — and the trend flags it before it quietly ruins a month of data. This is where the dull command becomes an actual problem, because the existing metrics cannot do that, and the way they fail is instructive.

Take the same target through the same filter on the best night and the worst night, with nothing whatsoever touched in between. Field uniformity moved +88%. Coma score moved −41%. Collimation score moved −29%. All three in the direction that reads as the optics got better. Nothing had been adjusted. That is entirely weather, and a change detector built on those numbers would have cheerfully reported an improvement that did not happen — or, on the reverse pair of nights, raised an alarm about a telescope that was fine.

Two mechanisms, both worth knowing about. First, the star count fell from 718 to 130 between those nights, so the statistics were not describing the same population — bad seeing leaves only the brightest stars, which are not a random sample. Second, and more subtly, seeing dilutes the very contrast the optical signature lives in: blur everything enough and elongation patterns wash toward randomness, dragging any correlation-style metric toward its null value. The metric does not measure the optics. It measures the optics multiplied by the weather.

Metrics built to hold still

So there is now a second set, designed from the start to be trendable, and every design choice in it is a defence against one of the failures above. The star sample is a fixed count, brightness-matched, so the population cannot move with the seeing. Field degradation is computed in quadrature — subtracting the sharpest patch of field from the median in the same way you would subtract noise sources — rather than as a ratio, because dividing by the mean builds an inverse dependence on seeing directly into the number.

Collimation is read from the position of the sharpest point in the field rather than from any brightness or sharpness value, and this is the neatest trick of the lot: bad seeing lifts the whole FWHM surface, but it does not move where that surface bottoms out. The location of the sweet spot is a property of the optics almost alone. And elongation is split into a radial component, which is genuinely optical, and a single-direction component, which explicitly is not — the code's own docstring notes that a high value there means "guiding, wind shake or a cable snag". The cable-eating dog has a dedicated metric.

Getting these stable enough to be worth logging took one more thing: pooling. Measured per frame and then medianed, they still scattered 86% on field excess and 123% on radial elongation across three comparable nights, because a 4×4 grid over one frame leaves only about nine stars per cell. Pooling a whole night's frames into a single measurement puts roughly ten times that in each cell and brings the edge-degradation figure down to 13% scatter. The rest land near 50%, which is honest rather than good.

One quiet trap fell out of that. The obvious way to decide whether a night has enough data is to require a total pooled star count — and it silently fails, because eleven frames of 130 stars pool to 1,489, sailing past any sensible total, while not one of those frames was individually good enough to measure. The gate has to count acceptable frames, not stars.

What one night looks like

Here is the entire trend database at the time of writing, which is to say one night: 2026-07-31, SH2-92 in Hα, 1,800 stars pooled from 12 frames out of 11,726 detected. Seeing floor 1.58″. Field excess 0.24″, edge excess 0.86″ — so the optics add very little blur across the middle of the field and a great deal at its worst corner, which is the tilt already visible in the grid above. The sweet spot sits 0.43 of the way out from centre, up and to the left, consistent with the same story.

The one number that raises an eyebrow is the single-direction elongation component, at 0.69 along an axis of about 152°. By the metric's own definition that leans toward something non-optical — guiding, wind, or the aforementioned dog. It is also exactly the sort of reading nobody should act on yet, because with a single night there is no baseline to compare it against, and a corner-heavy aberration can plausibly lift that number on its own. It is a hypothesis with a sample size of one.

Deliberately doing nothing with it

Which is the actual point, and the reason nothing consumes these numbers yet. It would be easy to wire up thresholds this week and have a change-detection command by the weekend. Those thresholds would be guesses, the guesses would be wrong in both directions, and a detector that cries wolf gets ignored precisely as fast as one that stays silent gets trusted. The metrics are being logged every night and read by nobody, on purpose, until there is enough baseline to know what normal actually looks like.

So the answer to "has anything changed?" is currently "ask again in a couple of months". The dullest note here is the one making the strongest claim: that knowing what you cannot yet measure is worth more than a number that looks like an answer. Meanwhile the cables remain unchewed, though the observatory is now, in principle, in a position to notice.

The observatory is sleeping