Declaring Instrument Clocks

Every spectrometer carries a small set of reference clocks — the synthesizer that drives the up- and down-conversion chain, the arbitrary-waveform generator, the digitizer sample clock. Those clocks leave deterministic fingerprints in the spectrum: narrow, non-decaying tones at frequencies set by clock arithmetic (harmonics, mixing products, sample-rate images). They are spurs, not molecular lines, and the Stage 5 spur gate already removes the ones it can recognize. Declaring the instrument’s clocks grounds that recognition in the known clock arithmetic instead of an empirical guess.

A clock declaration adds two pieces of prior knowledge:

  • A sharper spur prior. The declared clock fundamentals generate a lattice of frequencies where spurs are expected. The spur gate uses the lattice both to know where to look and to lower its burden of proof on a tone that lands exactly where the clocks predict — so it catches instrumental tones an unprimed gate leaves behind. Any fitted line that lands on the lattice is flagged for review.

  • A self-calibrated frequency axis. A digitizer that is not locked to the laboratory frequency standard runs at a slightly wrong rate, which stretches the frequency axis by a constant fractional factor. That scale error is measurable — prior-free — from the Rb-locked clock spurs themselves, because their true frequencies are known exactly. The timebase object measures it and Stage 6 applies it; the estimator and its precision are the subject of the timebase self-calibration methods note.

Both are additive prior knowledge. With no declaration the pipeline behaves exactly as it does without one (the spur gate falls back to a coarse integer-MHz anchor, and the frequency axis is reported as measured); the results are byte-identical. Declaring the clocks only ever adds information.

The instrument clock tree

The home instrument the example data come from is a useful worked example. Its clocks are:

  • an up-conversion synthesizer at 5760 MHz (doubled to the 11520 MHz LO, multiplied to the excitation band after a lower-sideband mix);

  • a down-conversion synthesizer at 5120 MHz (multiplied to the 40960 MHz probe LO);

  • an arbitrary-waveform generator clocked at 16000 MHz;

  • a digitizer sampling at 50 GSa/s (eight interleaved 6.25 GSa/s ADCs).

The first three are referenced to a 10 MHz rubidium standard; the digitizer is free-running. Every cross-fixture recurring spur in the example data is a direct identity of one of these clocks — in one of two frames:

  • the baseband frame, the frequency a tone enters the digitizer at, where the molecular frequency maps as \(f_\text{bb} = |f_\text{mol} - f_\text{probe}|\) (a synthesizer reference at 5120 MHz, a mixing product at 2×5120, the AWG half-rate at 8000 MHz, an ADC image at 2×6250);

  • the molecular (RF) frame, harmonics radiating directly into the receiver (5×5760, 6×5760).

All of the Rb-locked tones fall on a single comb — the intermodulation lattice of the locked fundamentals, whose spacing is their greatest common divisor. The free-running digitizer is the one source that drifts: its tones wander relative to the Rb-locked comb from one acquisition to the next.

This is the structure a declaration makes available to the pipeline.

Declaring the clocks

A declaration is a short list of clock sources, each a frequency in MHz, a locked flag (referenced to the laboratory standard or not), and an optional label. The clocks object manages it:

# Show the current declaration
$ ftmwpipeline clocks show exp.ftmw

# Replace it (token: freq[:locked|free[:label]])
$ ftmwpipeline clocks set exp.ftmw 5760:locked:upconv 5120:locked:downconv \
                                   16000:locked:awg 6250:free:digitizer

# Append, remove by frequency, or clear
$ ftmwpipeline clocks add exp.ftmw 8000:locked:awg-half
$ ftmwpipeline clocks remove exp.ftmw 6250
$ ftmwpipeline clocks clear exp.ftmw

The same operations are available on the functional API and the Pipeline object:

import ftmwpipeline.api as ftmw

ftmw.set_clock_sources(
    "exp.ftmw",
    [
        {"freq_mhz": 5760, "locked": True, "label": "upconv"},
        {"freq_mhz": 5120, "locked": True, "label": "downconv"},
        {"freq_mhz": 16000, "locked": True, "label": "awg"},
        {"freq_mhz": 6250, "locked": False, "label": "digitizer"},
    ],
)
ftmw.get_clock_sources("exp.ftmw")

Two conventions matter:

  • Declare fundamentals, not products. A harmonic or mixing product is generated from the fundamentals, so the lattice produces it for free. Declare the 5760 MHz synthesizer, not the 11520 MHz LO it is doubled to. Frequency division is the exception — a half-rate tone (here the AWG’s 8000 MHz) is not a generated harmonic, so if the instrument produces one it is declared as its own fundamental.

  • Integer-MHz fundamentals. Rb-locked synthesizers are set to exact frequencies; the lattice is built from the integer greatest common divisor of the declared locked clocks, so declare them as integers.

Where the declaration lives

The clocks verbs write the recommended settings layer — the same layer an importer populates from instrument metadata. In the resolution order (Settings and presets), an explicit fit-time clock argument and a persisted Stage 5 spur declaration both outrank it, so the recommended declaration is the “declare before you fit” baseline rather than an override. If a Stage 5 fit already exists when you change the declaration, the change does not retroactively alter that fit; rerun the fit to apply it. The CLI warns when this is the case.

Automatic population from Blackchirp

A Blackchirp experiment records its synthesis chain directly. The loader reads the clock chain and the digitizer sample rate at import and writes the recommended declaration, so for the example data the lattice fundamentals are already declared before you touch the clocks verbs. The one fact the metadata does not carry is whether the digitizer is locked, which defaults per the instrument preset.

The spur lattice prior

From the declared locked clocks the pipeline computes the lattice spacing \(g\) (their greatest common divisor) and predicts spurs wherever \(f_\text{mol}\) or \(f_\text{bb}\) is a multiple of \(g\), in both frames. For the home instrument’s three locked synthesizers (\(\gcd(5120, 5760, 16000) = 640\) MHz) this is a few dozen in-band points — a far tighter prior than “any integer MHz,” which spans tens of thousands of candidates. Because the locked tones are exact, the match tolerance is the measurement tolerance (about one analysis bin), not a clock tolerance.

Declaring a divided clock can tighten the lattice further: the AWG’s half-rate (8000 MHz) is itself a strong tone and, declared as its own fundamental, brings the spacing down to \(\gcd(5120, 5760, 8000) = 320\) MHz.

Each unlocked clock contributes a drifting sub-lattice: multiples of its fundamental, matched within a wider window (the measured drift scale). Membership marks a candidate as expected to drift; it never sharpens a frequency test.

The gate uses the lattice in four ways:

  • Where to look. With a declaration the gate’s frequency sweep runs over the lattice points (together with the legacy integer-MHz anchors as a union, so off-lattice instrumental tones — confirmed by the decay probe — keep gating).

  • Burden of proof. On a locked-lattice point the prior odds of a spur are far higher, so the gate relaxes its evidence bar: an on-lattice tone is gated unless the decay probe shows it clearly decays like a molecular line.

  • The drifting family. A wandering digitizer tone defeats a fixed-frequency decay probe, so an unlocked-lattice match is adjudicated by a drift-tolerant statistic (a late-versus-early band-power ratio) instead. This is what gates the drifting digitizer spur the example data carry.

  • Line annotation. Any fitted line that lands on the lattice is stamped with the matched identity (for example 640x8 (bb) — the eighth multiple of a 640 MHz lattice, in the baseband frame). The annotation is the conservative half of the feature: it never removes a line automatically. It appears as a marker in fit show, as a flagged column in the Stage 6 report line tables, and makes an on-lattice line a one-command review removal.

An annotated line is a candidate instrumental artifact, not a verdict — a real molecular line can coincide with a lattice point. Treat the flag as a prompt to look, especially when the same identity recurs across experiments.

Timebase self-calibration

A free-running digitizer samples at a rate that is slightly off its nominal value, which stretches the baseband frequency axis by a constant fractional scale error \(\varepsilon\). That error is measurable, prior-free, from the Rb-locked clock spurs themselves: their true frequencies are known exactly, so their measured offsets recover \(\varepsilon\) directly. The timebase object runs the measurement after import and persists the result; Stage 6 applies the correction and folds its uncertainty into the reported per-line frequency budget.

# Measure epsilon from the Rb-locked spur lattice and persist it
$ ftmwpipeline timebase run exp.ftmw

# Print the summary and the per-tone diagnostic table
$ ftmwpipeline timebase show exp.ftmw
import ftmwpipeline.api as ftmw

result = ftmw.calibrate_timebase("exp.ftmw")
result.epsilon, result.sigma_epsilon      # fractional scale error +/- 1 sigma

timebase show prints the consensus \(\varepsilon\) and its uncertainty, the lattice spacing, and a per-tone table grouping the tones into those kept in the fit, those rejected for inconsistency, and the drift controls (unlocked multiples, never in the fit). The estimator — how each tone’s offset is read from its phase rather than a fitted lineshape, the precision bound that follows, and how the correction reaches the line list — is the subject of the timebase self-calibration methods note.

Interfaces

Everything on this page is read-only with respect to the fit. The two objects behave identically across the CLI, the functional API, and the Pipeline class:

  • clocksshow / set / add / remove / clear (get_clock_sources / set_clock_sources / remove_clock_sources / clear_clock_sources).

  • timebaserun / show (calibrate_timebase / load_timebase_calibration).

The clock declaration is persisted with the file and travels with it; the timebase calibration is persisted as its own record and consumed by Stage 6.