feat: tProc v2 backend — multiplexed readout, AveragerProgramV2, premium seam + name-scrub guard#5
feat: tProc v2 backend — multiplexed readout, AveragerProgramV2, premium seam + name-scrub guard#5aarontrowbridge wants to merge 6 commits into
Conversation
Add additive `readout_routing` field to QickProgram mirroring QickChannelMap.readout_chs, populated in pulse_to_envelopes. The board (PyQickSoc/expt_service) assembles the AveragerProgramV2 + multiplexed readout from this plain representation; the Julia side stays board-agnostic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nown-kind error (Spec B §2.2,§5.7) Add reduce_readout(kind, raw, discriminator, indices): :iq reduces Julia-side via the discriminator (iq_to_measurements); :wigner and :tomography_1q are reduced BOARD-side (expt_service / qick program) and tagged + passed through (no Wigner/tomography math in the Julia package, C7); any other kind is a typed error, never a crash. Migrate iq_to_measurements to the multiplexed channel-major raw[ch][k] shape (C3), concatenating per-channel discriminator outputs into one Measurement per knot index. Route the QickExperiment closure through reduce_readout with a kind (default :iq); readout(b) stays raw (C2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MockQickSoc.acquire returns the multiplexed channel-major shape raw[ch][k] (one per-knot blob list per requested readout channel, length(raw)==length(ro_chs)) via the existing synthetic state->IQ forward model; accepts a kind kwarg for interface parity (mock is kind-agnostic; real Wigner/tomography physics is board-side). Thread kind through readout(b)/acquire (readout return stays raw). Update the mock + backend round-trip testitems and the AbstractQickSoc acquire docstring/fallback to the channel-major shape. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-validated) (Spec B §2.1) play_program! assembles a concrete tProc v2 AveragerProgramV2 from the device-agnostic QickProgram (declare_gen + add_envelope + add_pulse at carrier freq, multiplexed declare_readout from readout_routing, trigger); acquire runs it and returns channel-major raw[ch][k], dispatching board-side reduction on kind (:iq/:wigner/:tomography_1q). Both use the lazy qick.asm_v2 import with actionable errors and are hardware-only, NOT exercised in CI (the existing py_soc discipline). Type-only testitem asserts the v2 methods exist without constructing a board. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…c B §2.3,§5.5) Add a name-scrub @testitem (src/name_scrub.jl) that walks src/*.jl and fails if any private-package identifier appears (the private calibration package name, its strategy type, the bare private acronym). The pattern is assembled from fragments so the guard file never trips itself; extend never shrink. Scrub the pre-existing occurrences from public comments, docstrings, and the README (replaced with 'closed-loop calibration' / 'Piccolo x QICK'); grep -riE over src is now empty. Entitlement (Step 1): the public package exposes only the un-gated chassis + standard readouts; the premium calibration strategy ships in the private package. The entitlement predicate (private package resolvable in the Julia env) is consumed by Spec A — no public code change needed. Closes-the-loop test (Step 3) requires the private package -> lives in the private repo/demo, NOT public CI (deferred). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ETHOD Aaron 2026-07-07: the product name 'Intonatissimo' + 'closed-loop calibration' capability are an intentional wink/nod (advertise the premium package at point of need). name_scrub guard now forbids only the strategy type + method acronym, NOT the product name. README gains a 'Closed-loop calibration on hardware' funnel section pointing at Intonatissimo (partner-licensed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| prog = asm.AveragerProgramV2(soc.qicksoc; reps = 1, final_delay = 1.0) | ||
|
|
||
| # Generators: declare + add each channel's staged envelope + a pulse at its | ||
| # carrier frequency (Hz → MHz for the v2 API). | ||
| for (gen_ch, cf) in program.carrier_freqs | ||
| idata, qdata = get(soc._envelopes, gen_ch, program.envelopes[gen_ch]) | ||
| prog.declare_gen(ch = gen_ch, nqz = 1) | ||
| prog.add_envelope(ch = gen_ch, name = "env$(gen_ch)", | ||
| idata = pylist(idata), qdata = pylist(qdata)) | ||
| prog.add_pulse(ch = gen_ch, name = "pulse$(gen_ch)", | ||
| freq = cf / 1e6, envelope = "env$(gen_ch)") | ||
| end | ||
|
|
||
| # Multiplexed readout: one declared readout per routed channel. | ||
| for ro_ch in program.readout_routing | ||
| prog.declare_readout(ch = ro_ch, length = length(program.times)) | ||
| end | ||
|
|
||
| # Play the generators + trigger the readout(s). The concrete timing/trigger | ||
| # sequence (delays, phase resets) is finalized on hardware. | ||
| for gen_ch in keys(program.carrier_freqs) | ||
| prog.pulse(ch = gen_ch, name = "pulse$(gen_ch)", t = 0.0) | ||
| end | ||
| prog.trigger(ros = pylist(program.readout_routing), t = 0.0) |
There was a problem hiding this comment.
As I mentioned - this doesn't quite work. The idiom being used here is that you instantiate the QICK program object, then append the declarations and instructions to it. That works for the base QickProgramV2 class, but not the AveragerProgramV2 which is what is usually used. So (all examples from the notebook https://github.com/meeg/qick_demos_sho/blob/main/tprocv2/00_intro.ipynb):
- keep this idiom, using QickProgramV2 instead of AveragerProgramV2 (like in the "using macro methods" example late in my notebook). Instead of using
repsandfinal_delayand having AveragerProgramV2 write the program loop for you, you will need to write that loop yourself. I don't like this, because this basically means duplicating AveragerProgramV2 on the Julia side. - use AveragerProgramV2 correctly (like in most of my notebook). Need to make your own program class that subclasses of AveragerProgramV2, with your declarations and instructions in the overriden _initialize() and _body() methods. I have no idea if this is doable within Julia.
- expect the user to provide their own program class in a Python file, where anything we want to change from the Julia side is parametrized in the config dictionary. In other words - we import MultiPulseProgram, we make a config dictionary (setting gen_ch, ro_ch, freq, trig_time, ro_len), instantiate the program. We lose the ability to write the pulse sequence in Julia, but is that something we wanted?
- add some new program class on the QICK side that is more suited for the Julia interface - e.g. take two AsmV2 objects in place of the initialize() and body()
prefer option 3, but option 4 might make sense.
| end | ||
| qicksoc = bitfile === nothing ? qick.QickSoc() : qick.QickSoc(bitfile) | ||
| return PyQickSoc(qicksoc, Float64(dac_rate), Float64(adc_rate)) | ||
| return PyQickSoc(qicksoc, Float64(dac_rate), Float64(adc_rate), |
There was a problem hiding this comment.
I dunno what the dac_rate and adc_rate are supposed to be here. The Python QickSoc class has dac_sample_rates and adc_sample_rates parameteres but those are not normally used.
| # mock path never touch Python. The interface methods below assemble + run a | ||
| # concrete tProc **v2** `AveragerProgramV2` from a device-agnostic `QickProgram`. | ||
| # They call the qick v2 proxy API but are validated only on a board, with the | ||
| # QICK collaboration; they are intentionally NOT exercised in CI (constructing a | ||
| # PyQickSoc requires a board / the qick package). The v2 specifics |
There was a problem hiding this comment.
FYI, you don't need hardware to instantiate an AveragerProgramV2 - all you need is a JSON dump of the firmware configuration (which you can get by running QickSoc.dump_cfg() on a board: https://docs.qick.dev/latest/_autosummary/qick.qick_asm.html#qick.qick_asm.QickConfig.dump_cfg). Then you can instantiate a QickConfig object from JSON, and pass that to your AveragerProgramV2.
| # Generators: declare + add each channel's staged envelope + a pulse at its | ||
| # carrier frequency (Hz → MHz for the v2 API). | ||
| for (gen_ch, cf) in program.carrier_freqs | ||
| idata, qdata = get(soc._envelopes, gen_ch, program.envelopes[gen_ch]) | ||
| prog.declare_gen(ch = gen_ch, nqz = 1) | ||
| prog.add_envelope(ch = gen_ch, name = "env$(gen_ch)", | ||
| idata = pylist(idata), qdata = pylist(qdata)) | ||
| prog.add_pulse(ch = gen_ch, name = "pulse$(gen_ch)", | ||
| freq = cf / 1e6, envelope = "env$(gen_ch)") | ||
| end |
There was a problem hiding this comment.
This (and the prog.pulse() below) are setting up and playing just one pulse per generator, and all pulses are arbitrary-envelope. In most cases you will have multiple drive pulses (you could concatenate everything, but then the gaps between pulses are 0's, and you waste memory), and some pulses (esp. readout pulses) will be constant-envelope or flat-top (which again you could encode as arb-envelope, but that's a waste).
| # Play the generators + trigger the readout(s). The concrete timing/trigger | ||
| # sequence (delays, phase resets) is finalized on hardware. |
There was a problem hiding this comment.
what does "finalized on hardware" mean?
| # One averaged sweep by default; sweeps/dynamic readout add v2 loops here. | ||
| prog = asm.AveragerProgramV2(soc.qicksoc; reps = 1, final_delay = 1.0) |
There was a problem hiding this comment.
not sure what "one averaged sweep" means, but reps=1 means we are just running a single shot and averaging or sweeping is meaningless. It is true that if you want to do a sweep, you need to add a loop.
|
Thanks @meeg — this is exactly the ground truth we needed, and it reshaped our approach. Point-by-point, and then the bigger decision your first comment forces: On the program model (your top comment — options 1–4). You're right that appending to a bare program object is a No board needed via Multiple pulses + const/flat-top (line 93). Agreed — one giant arb per generator wastes memory. qicklab's IR has a typed envelope union (
"finalized on hardware" (line 101). Fair callout — it was vague. It meant only the trigger/timing sequence needed board validation; given the Net: this PR's inline |
|
Quick follow-up @meeg: we renamed the package qicklab → |
What
The QICK tProc v2 port (Spec B,
spec-20260706-221349), rebased onto main (post Intonato 0.3 compat) and pushed at Aaron's direction. Six commits:QickProgramcarries multiplexed-readout routing (§2.1) — the v2 program model.iq/wigner/tomography_1q, with a typed error for unknown kinds (never a silent misread).PyQickSocassemblesAveragerProgramV2(§2.1) — structural assembly for the v2 tProc; on-board execution paths remain the explicit collaboration-finalized stubs.Verification
main(8fd80c4, Intonato0.2, 0.3compat).Notes for review
🤖 Generated with Claude Code