Real-time PCR (qPCR)
OpenReadout reads RDML files, which most qPCR software can export, and the native files of several instruments: Applied Biosystems .eds, Rotor-Gene .rex and LightCycler 480 .ixo. It returns the plate setup, the cycling program, the amplification and melt curves, and the vendor’s results where the file stores them. analyze qpcr computes its own Cq values from the curves (see “Analysis” below). Bio-Rad .pcrd files are encrypted and refused. The table below lists each format.
Derived from public files, the RDML schema and change logs (MIT) and the RDML R package (MIT) read as prior art. rdmlpython (MIT) is the reference reader and validator for RDML, and qslib (EUPL-1.2) is run as a black box. Provenance: docs/provenance/qpcr.md. Crate: openreadout-qpcr.
| format id | files | reads | confidence |
|---|---|---|---|
rdml |
.rdml, .rdm (zip), bare RDML .xml |
everything but digital-PCR partitions; written by export --to rdml |
high |
applied-biosystems-eds |
.eds (QuantStudio 1-7 Pro, 12K Flex, ViiA 7, StepOne(Plus), 7500) |
setup, program, per-dye multicomponent signal, Rn/ΔRn, melt, vendor results | medium |
bio-rad-pcrd |
.pcrd (CFX Maestro / CFX Manager) |
detected and refused, exit 6: the container is encrypted with a key only the vendor software has; export RDML from CFX Maestro | — |
rotor-gene-rex |
.rex (Rotor-Gene Q Series Software) |
samples, raw cycling and melt readings, profile; no results are stored | low |
roche-lightcycler-ixo |
.ixo (LightCycler 480 software 1.5) |
run, protocol, channels, samples, raw fluorescence per cycle and melt readings, the vendor’s absolute-quantification Cp and calls | medium |
LightCycler 96 experiment files (.lc96p) are RDML zips with the LightCycler 96 software’s own analysis next to the RDML document; they are read as rdml with that analysis attached (see “LightCycler 96 .lc96p” below). LightCycler 480 .lc files (older software) have no public sample and are not read.
The model
Every dialect is parsed into one model. A file holds shared definitions — samples, targets (genes/assays/detectors), dyes, thermal programs, experimenters — and one or more runs (plates; RDML files often hold one run per target or per plate). A run holds reactions (wells, by zero-based row-major position); a reaction holds one assay per target measured in it: the vendor’s result, the amplification curve and the melt curve of that target’s reporter dye. .eds reactions also keep every dye’s multicomponent signal (including the passive reference, ROX).
Names are ours; each dialect maps into them below. Tasks (the role of a well for a target) use one vocabulary: unknown, standard, ntc, nac, ntp, nrt, positive, negative, optical calibrator, blocked ipc, none.
Table 0 results (one row per well × target)
| column | dtype | meaning |
|---|---|---|
well |
uint32 | well name as a code into the column’s extra.categories (A1, P24; rotor tubes 1…72); CSV/Parquet exports write the name |
row, col |
uint16 | plate row and column, 1-based |
cq |
float64 | quantification cycle (Cq, Ct, Crt) as the vendor software or the RDML file reports it; NaN = no Cq (extra.undetermined counts the wells where the file says “undetermined”) |
target, sample, dye, task |
uint32 | codes into extra.categories |
quantity |
float64 | given quantity (standards) |
cq_mean, cq_sd |
float64 | replicate-group mean and SD (vendor) |
tm, tm2 |
float64 | melting temperature of the first and a second melt peak, °C (vendor) |
threshold |
float64 | threshold the Cq was called at: per well (.eds JSON layout ctThreshold, RDML quantFluor) or the analysis setting (.eds SDS layout Threshold; with AutoCt it is not the threshold used) |
baseline_start, baseline_end |
float64 | baseline window in cycles |
amp_status |
uint32 | amplification status (vendor), code into extra.categories |
calculated_quantity |
float64 | quantity the vendor computed from its standard curve |
efficiency |
float64 | amplification efficiency of the reaction as a fold per cycle (RDML ampEff) |
excluded |
uint8 | 1 when the well is omitted or the result excluded |
cq_confidence |
float64 | Cq confidence (vendor) |
run |
uint32 | run (plate), code into extra.categories |
cq_status |
uint8 | determined (a Cq), undetermined (no amplification: cq is NaN), no result (not analysed); code into extra.categories (CQ_STATUSES) |
cq is the first column that is not a position, so preview draws a Cq heat map of table 0 by default (preview --table 0 --column tm for Tm).
Table 0 extra: dialect (rdml, eds-sds, eds-7500, eds-json, rex, ixo), instrument {manufacturer, model, serial_number, software, software_version, firmware_version}, experiment_name, description, experiment_type, chemistry, operator, run_state, created_at, acquired_at, ended_at (ISO-8601; .eds times are Unix milliseconds, UTC), passive_reference, reference_targets (endogenous controls), calibrator_sample, experimenters [{id, first_name, last_name, email, lab}], targets [{name, kind (reference / of interest), dye, quencher, efficiency, efficiency_se, efficiency_method, melting_temperature_c, description, sequences}], samples [{name, kind, quantity, quantity_unit, per_target [{target, kind, quantity}], annotations}], dyes, programs [{name, description, lid_temperature_c, sample_volume_ul, run_mode, cycles, acquisition_temperature_c, stages [{kind, repeats, steps [{kind, temperature_c, high_temperature_c, low_temperature_c, hold_s, ramp_c_per_s, measure, goto, repeat}]}]}], runs [{name, experiment, description, instrument, software, started_at, cq_method, background_method, rows, columns, wells_used, program}], plate_rows, plate_columns, rows_by_task, rows_with_cq, undetermined, vendor_standard_curves [{target, dye, slope, intercept, r2, efficiency_percent}].
acquisition_temperature_c is the temperature of the step that reads fluorescence during cycling — the annealing/extension temperature of a two-step PCR, the extension read of a three-step one. cycles is the repeat count of the cycling stage (RDML: the loop’s repeat + 1).
Table 1 amplification and table 2 melt (long form)
amplification: well, row, col, target, dye, cycle, fluorescence, corrected, run — one row per well × target × cycle. fluorescence is what the file stores: Rn (reporter / passive reference) where the vendor analysis stored it, the reporter’s multicomponent signal for unanalysed .eds files, RDML adp/fluor, Rotor-Gene raw readings. corrected is the vendor’s baseline-corrected ΔRn (NaN when not stored). melt: well, row, col, target, dye, temperature, fluorescence, run. A table that would be empty is left out (the melt table is then table 1 or absent).
Traces
Per run and dye, one trace per kind, one channel per well × target (channel name A1 GAPDH, channel extra {well, sample, target}): amplification <dye>, amplification <dye> baseline-corrected (vendor ΔRn), melt <dye>, melt <dye> -dF/dT, multicomponent <dye> (every dye, .eds). Trace extra: kind, dye, run, quantity (Rn, fluorescence, multicomponent, ΔRn), plot: overlay (previews draw all channels in one panel), axis (cycle from 1 in steps of 1; melt traces: temperature with the mean first and last temperatures — each well has its own temperatures, which table melt keeps), and for derivative traces derivative: vendor | computed | mixed (per channel in channel extra.derivative). sample_rate_hz is 0: the abscissa is not time. With several runs the trace names start with <run>: .
The −dF/dT trace is the vendor’s derivative where the file stores it (.eds meltcuve_result.txt “Delta Rn values” on its own uniform temperature grid); otherwise we compute it by central differences on the melt fluorescence (Source::Inferred).
RDML
A zip holding rdml_data.xml (any .xml member with an <rdml> root is accepted: Bio-Rad writes BioRad_qPCR_melt.xml; LightCycler 96 adds calculated_data.xml, app_data.xml, module_data.xml, instrument_data.xml), or bare XML. Bio-Rad zips start with a PK\x07\x08 split-archive marker before the first local header. Versions 1.0-1.4 are read; elements are matched by local name (namespace-agnostic).
| RDML | ours |
|---|---|
rdml@version, dateMade |
format_version, created_at |
experimenter (firstName, lastName, email, labName) |
experimenters |
dye@id, dyeChemistry |
dyes |
sample@id, type (with or without targetId), quantity/value, quantity/unit, annotation/property+value, description |
samples: kind via the task vocabulary (unkn → unknown, std → standard, pos → positive, opt → optical calibrator); an absent type is unknown (the standard’s default) |
target@id, type (ref/toi), amplificationEfficiency, amplificationEfficiencySE, amplificationEfficiencyMethod, meltingTemperature, dyeId@id (text in RDML 1.0), sequences/*/sequence |
targets (kind reference / of interest; an efficiency above 10 is read as a percentage, StepOne writes 93.9) |
thermalCyclingConditions: lidTemperature, step/nr with temperature (temperature, duration, ramp, measure), gradient (highTemperature, lowTemperature, duration, measure), loop (goto, repeat), pause, lidOpen |
programs: one stage of kind program whose steps keep the RDML order; measure real time / meltcurve → real time / melt |
experiment@id → run@id, description, instrument, dataCollectionSoftware (name, version), backgroundDeterminationMethod, cqDetectionMethod, thermalCyclingConditions@id, runDate, pcrFormat (rows, columns, rowLabel, columnLabel; RDML 1.0: 96-well plate; A1-H12, free format, …) |
runs |
react@id (1-based row-major position; RDML 1.0 labels A1) → sample@id, data: tar@id, cq (−1 = not available), N0, ampEff, ampEffSE, meltTemp, excl, note, adp (cyc, fluor), mdp (tmp, fluor), bgFluor, bgFluorSlp, quantFluor |
reactions and assays: cq, efficiency, tm, excluded, amplification and melt curves; quantFluor → threshold, bgFluor/bgFluorSlp → the background line our Cq uses; RDML 1.0 data/quantity → calculated_quantity. A cq ≥ the cycle count (the last cycle of the reaction’s adp curve, else the program’s) is the exporting software’s “no Cq” (the StepOne export writes its NTCs as 40.0 of 40 cycles; docs/provenance/qpcr.md 2026-10-02): cq null, cq_status undetermined, cq_stored the number, flag cq_at_cycle_count (after the LightCycler 96 calls below; the corpus test counts these per file, CYCLE_COUNT_WITHHELD) |
Digital-PCR partitions are counted in a note, not read. vendor (info --view full) holds the whole RDML tree with the data points counted (adp_count, mdp_count), not copied.
GUID sample ids. When a sample’s id is a GUID (8-4-4-4-12 hexadecimal digits, as the LightCycler 96 writes them) and its description is not empty, the description is the sample name (sample in table 0 and analyze qpcr), provided every sample name stays unique; the GUID is kept as samples[].id. Otherwise nothing is renamed (a note says so).
LightCycler 96 .lc96p (2026-09-26)
A zip with rdml_data.xml (RDML 1.1, read as above) and Roche members: app_data.xml (rocheLC96AppExtension@softwareVersion; per experiment/run/react@id one factGraph per target: @id, @targetId, @isExcluded), calculated_data.xml (rocheLC96CalculatedData: per react/factGraph the software’s call; in the analysis instance relQuantDataSource (graphId, cq, epf) and relQuantStatisticalRow (graphIds/guid, cqMean, cqError), or for an absolute-quantification analysis absQuantDataSource (graphId, call, editedCall, cq, epf, slope, standard, failureType, concentration) and absQuantStatisticalRow (the same Cq mean and error, and concentrations)), module_data.xml (display settings), instrument_data.xml, manifest.xml. What we attach to the RDML reactions:
| member | ours |
|---|---|
factGraph/call Positive / Negative / other |
amp_status amplified / not amplified / the text lower-cased |
a call other than Positive |
the RDML cq of that reaction is not a Cq: the software shows none. The corpus holds 100, 1.56 and 72.55 of 50 cycles under such calls, but also plausible numbers (rdml-lc96-bactxy: 203 of 258 Negative calls between 1 and 50 cycles, e.g. A1 FAM 33.56); the software’s own absolute-quantification analysis in that file lists no Cq for 8 of 8 Negative graphs it includes (stored 29.57, 37.88, 40.33, 40.9, 42.08 and 100) and the stored Cq for 56 of 56 Positive ones, and the Negative curves rise a median 20 % over their baseline against 104 % for Positive ones. So cq is null, the number kept as cq_stored; Negative → undetermined with flag lc96_negative_call, other calls → no result with flag lc96_call_not_positive. A Cq that an analysis does list for such a graph is kept (no corpus case). rdmlpython reads the stored numbers as Cqs; the corpus test counts these per file (LC96_WITHHELD) |
isExcluded="true" |
excluded |
relQuantStatisticalRow / absQuantStatisticalRow cqMean, cqError |
cq_mean, cq_sd of every reaction of the group (only when the group’s reactions share one sample and one target); cqError is the sample standard deviation (absolute quantification: 36 of 36 groups of rdml-lc96-bactxy equal the mean and SD of their members’ Cqs) |
softwareVersion |
instrument software_version; manufacturer Roche, model LightCycler 96 |
vendor.lightcycler96 counts graphs, calls and replicate groups. A member that does not parse leaves the RDML data as it is, with a note. Checked against the depositors’ table of the LightCycler 96 software’s results for three files: every exported Cq, Cq mean and Cq error (108 wells, 36 groups), and no other reaction with a Cq.
Writing RDML (export --to rdml)
RDML 1.3 (REC), rdml_data.xml deflated in a zip. Every sample, target and dye is defined; a target without a dye refers to a dye unknown. Sample ids are the sample names; wells without one get NTC, standard <quantity> or unnamed, and a name used with conflicting types or quantities is split into name (2), … . Tasks become RDML sample types (type targetId=… when they differ by target), quantities RDML quantities (unit other unless the source is RDML). Programs are flattened into RDML steps (a cycling stage ends in a loop back to its first step, repeat = repeats − 1; melt reads become gradient steps with measure meltcurve; RDML needs a positive duration, so a missing hold is written as 1 s and noted). Runs keep their plate format; reactions their position (react@id = row × columns + column + 1). Data: tar, cq (-1 for “undetermined”), N0, ampEff, ampEffSE, meltTemp (first Tm), excl, note, adp, mdp, bgFluor; points with a repeated cycle or temperature are dropped (RDML requires them unique) and counted in the report notes. The file is written next to the output, read back with our RDML reader and compared (runs, reactions, data elements, every Cq bit for bit, point counts) before it is renamed into place; oracle/qpcr.py --validate-rdml validates it against the RDML schema with rdmlpython.
Applied Biosystems .eds
A zip in one of three layouts (dialect in table 0 extra):
eds-sds—apldbio/sds/XML (QuantStudio 3/5/6/7/12K Flex, ViiA 7; “Experiment Document Specification” 1.x inManifest.mf).eds-7500— the same withmulticomponent_data.txtinstead ofmulticomponentdata.xml(7500 / 7500 Fast / StepOne Software v2.x).eds-json—setup/,run/,primary/,extensions/JSON (Design & Analysis 2, QuantStudio 1/3/5/6 Pro/7 Pro; specification 2.0.0). Files set up in Design & Analysis 2 and run on older firmware keep JSON setup andapldbio/sds/run data side by side; both are read, JSON first.
| member | what we read |
|---|---|
Manifest.mf, apldbio/sds/Manifest.mf |
Specification-Version → format_version; Implementation-Title/Version → software; Content-Type (experiment type id) |
apldbio/sds/experiment.xml |
Name, Description, Operator, RunState, CreatedTime/RunStartTime/RunEndTime (Unix ms), Type/Name → experiment_type, ChemistryType → chemistry, InstrumentTypeId → instrument model (the vendor’s internal id: appletini, paragon, sds7500, steponeplus, …), ExperimentProperty type="RunInfo" (softwareVersion, instrumentSerialNumber, userId), Samples/Sample/Name, Detectors/Detector (Name, Reporter, Quencher) → targets. The plot-appearance properties are dropped from the vendor tree. |
summary.json, run/run_summary.json |
name, instrumentType (QS7PRO → QuantStudio 7 Pro), runStatus, createdTime; instrumentSerialNumber, firmwareVersion, operator, startTime, endTime |
apldbio/sds/plate_setup.xml |
Rows, Columns (or PlateKind/Type TYPE_16X24), PassiveReferenceDye (NULL = none), feature map sample (Index → Sample/Name), feature map detector-task (Index → DetectorTask: Task, Concentration → quantity, Detector/Name, Reporter, Quencher), Wells/Well/IsOmit → excluded |
setup/plate_setup.json |
blockType (BLOCK_384W → 16 × 24), passiveReference, samples (name, type, quantity, biogroup → annotation), targets (name, reporter, quencher), wells (index, sampleName, targetAssignments targetName, task, quantity) |
apldbio/sds/tcprotocol.xml |
SampleVolume, CoverTemperature, RunMode, TCStage (StageFlag PRE_CYCLING/CYCLING/DISSOCIATION/… → hold/cycling/melt/…, NumOfRepetitions, TCStep: first Temperature of the block zones, HoldTime s, RampRate, CollectionFlag 1 = read at the step, 2 = read during the ramp) |
setup/run_method.json |
sampleVolume, coverTemperature, runMode, stages (repeat, steps ramp{temperature, rate}, hold{duration, collectionProfile{processing}}) |
apldbio/sds/analysis_protocol.xml |
name/typed-value settings: IDetectorSettings (ObjectName = target, or the defaults) Threshold, AutoCt, BaselineStart, BaselineStop, AutoBaseline; IWellSettings (WellIndex) override them unless UseDetectorDefaults; IDDCtAnalysisSettings EndogenousControl → reference_targets, Calibrator → calibrator_sample; IDataSelectSettings StageNum / MeltStageNum choose the amplification and melt collection points |
primary/analysis_setting.json, extensions/am.rq/relative_quantification_setting.json |
defaultCtSetting (autoThreshold, autoBaseline, …); reference targets and sample |
apldbio/sds/multicomponentdata.xml |
WellCount, CycleCount, CollectionPoints [Stg:s Cyc:c Stp:p Pt:n], SampleTemperatures (well-major), DyeData@WellIndex/DyeList [FAM, ROX, VIC], SignalData@WellIndex/CycleData (one list per dye, in DyeList order) → multicomponent signals of the amplification stage (the stage with the most distinct cycles unless StageNum says otherwise); melt-stage points give the melt curve when no melt result is stored |
apldbio/sds/multicomponent_data.txt |
records WELL CYCLE DYE MSE SIGNAL (zero-based well and point) with pure-dye coefficients on continuation lines; the first cycles points are the amplification reads |
primary/multicomponent_data.json |
collectionPoints (stage, cycle, step, point), wellData (wellIndex, dyeData dyeName + fluorescences, temperatures) |
apldbio/sds/analysis_result.txt |
tab-separated; header Well, Sample Name, Detector, Task, Ct, Avg Ct, Ct SD, Delta Ct, Qty, Avg Qty, Qty SD, Amp Status, Cq Conf (columns found by name); after each result line: Rn values, Delta Rn values (one value per cycle → curve and ΔRn), DDCT Values (well, –, sample, target, task, Ct, –, ΔCt, ΔCt SD, ΔCt SE, –, RQ, RQ min, RQ max, outlier, ΔΔCt → vendor_delta_cq, vendor_rq). Ct Undetermined → no Cq, undetermined. A Ct ≥ the cycling stage’s repeat count is the vendor’s “Undetermined”: QuantStudio 3/5/6/7/12K Flex and ViiA 7 software write it as 40.0 for 40 cycles, and their Results exports show “Undetermined” for exactly those wells (five exports, docs/provenance/qpcr.md 2026-09-24). It becomes cq null, cq_status undetermined, cq_stored the number and flag ct_at_cycle_count. Without a program the number is kept. Amp Status 1/0/−1 → amplified/inconclusive/not amplified (paired with the export’s Amp / Inconclusive / No Amp). A genotyping header (Call, Marker Name, RnX, RnY, Ref, Confidence, Method) gives the table genotypes (below; the rows stay in the vendor tree as genotype_calls too). |
apldbio/sds/meltcuve_result.txt (the vendor’s spelling) |
Tm (comma-separated peaks), Sample Temperatures + Rn values → melt curve, Delta Rn Sample Temperatures + Delta Rn values → the vendor’s −dF/dT |
primary/analysis_result.json |
wellResults (wellIndex, sampleName, reactionResults targetName, task, quantity, amplificationResult {rn, deltaRn, cq (−1 = none), cqConf, ctThreshold, ctBaselineStart, ctBaselineEnd, ampStatus, flags}, meltResult, omitted), replicateGroupResults (cqMean, cqSD). The files contain bare NaN tokens (not JSON); they are read as null. |
extensions/am.sc/standard_curve_result.json |
standardCurves (targetName, dye, slope, yIntercept, r2, efficiency %) → vendor_standard_curves; reactions (wellIndex, targetName, quantity) → calculated_quantity |
Files that keep only raw optical images (apldbio/sds/images/*.tiff, quant/*.quant) have no curves; a run that was set up but never run has setup only. Both are said in info notes.
Genotyping (table genotypes). A genotyping run’s analysis_result.txt rows become the last table, genotypes: well, row, col, sample, marker, task (codes into extra.categories), call, genotype, call_code, rn_x (allele-1 reporter signal), rn_y (allele-2), reference, confidence, method (Auto/Manual); extra.markers gives each marker’s allele names from plate_setup.xml (Marker/Allele1/Name, Allele2/Name), extra.source vendor. Call codes: 1 → allele 1/allele 1, 2 → allele 1/allele 2, 3 → allele 2/allele 2, 0 → negative control, −1 → undetermined, others code <n>; genotype writes the three genotype codes in allele names (A/A, A/T, T/T). The mapping is inferred from the signal clusters and checked against 1000 Genomes genotypes of four reference samples at two markers (docs/provenance/qpcr.md, 2026-09-26). The calls are the vendor’s as stored; we do not re-call.
Automatic baseline windows (SDS/7500 layouts). analysis_protocol.xml stores one baseline setting (BaselineStart/BaselineStop); with AutoBaseline true the software picks a window per well and does not store it. baseline_start/baseline_end of such a well are the window (two or more cycles) whose least-squares line through the stored Rn equals the vendor’s baseline line Rn − ΔRn (intercept and slope within 1e-6 of the Rn scale), when exactly one window does; otherwise they are empty and a note counts the wells. They are never the setting. Checked against StepOne Software Results exports (8 StepOnePlus runs, 574 wells): 572 windows equal the exported Baseline Start/Baseline End, 2 left empty (a two-point window, a flat undetermined well). With AutoBaseline false the setting is the window used.
Bio-Rad .pcrd
Every public .pcrd is a zip (after a PK\x07\x08 marker) whose single member datafile.pcrd has general-purpose flag bit 0 (encryption) set. Reading it would need the vendor’s key; the clean-room policy (rule 4) keeps us from circumventing it. info --view format names the format; every other command exits 6 with a hint to export RDML from CFX Maestro (File > Export > RDML File), which openreadout reads. A truncated .pcrd is corrupt (exit 4).
Rotor-Gene .rex
XML: RexHeader (REX 3.15 → format_version), Operator, RunId, StartTime, FinishTime, Samples/Page/Sample (ID, Name, Type — 1 standard, 3 NTC, 5 positive, others unknown, per the RDML R package —, GivenConc, TubePosition), Samples/Groups/Group (Name, Tube → sample ID: the target), RawChannels/RawChannel (Name Cycling A.Green / Melt A.Green, StartX, StepX, one Reading per tube with space-separated values), Profile (HoldCycle, Cycle RepeatCount + NormalCyclePoint Temperature, RemainFor, AcquireTo; MeltCycle StartTemp, EndTemp, HoldFor), Record/Field Rotor Type (72-Well Rotor → 72 positions). The run is a plate of one column; wells are tube numbers. The channel colour (Green) stands in for the dye. No Cq, threshold or Tm is stored.
Roche LightCycler 480 .ixo (2026-09-26)
XML (<objectstream signature="IXOS" version="1">, CRLF) of <obj name class version>, <prop name> and <list name count> elements, then one line $ + four groups of 8 hexadecimal digits (a checksum; kept as vendor.trailer, not verified). The file is parsed up to </objectstream>.
| object (class) | ours |
|---|---|
root (HTCExperiment): name, Created, CreatedByName, SWVersion (LCS480 1.5.1.62) |
experiment_name, created_at, operator (when the run has no Technician), instrument software_version (1.5.1.62) |
run (HTCRun): StartTime, EndTime, InstrumentName, InstrumentID, Technician |
acquired_at, ended_at, instrument model, serial_number, operator |
run/Protocol/Programs (HTCRunProgramList): emlist of HTCRunProgram (name, Cycles, AnalysisMode Quantification / Melting Curves / None; Segments of HTCRunSegment: Target °C, Hold s, Slope °C/s, AcquisitionMode 0 none, 1 single, 2 continuous) |
one program LightCycler 480 protocol with one stage per LightCycler program (cycling / melt / hold, repeats = Cycles); a segment with AcquisitionMode 1 reads real time (the acquisition temperature), 2 is a gradient from the previous segment’s target with measure melt |
Programs/Container (HTCBlockType): RowCount, ColCount; Programs/SampleVolume, PlateID |
plate size; sample_volume_ul; vendor.plate_id |
Programs/DetectionFormats: the HTCDetectionFormat named by DefFormatName (or the only one), its HTCChannel items (name 465-510, ExcitationWL, EmissionWL) |
the channels, by index; a channel is the dye of its readings. Several formats and none named: refused (exit 6) |
run AcquisitionStore: base64 of a zlib stream holding HTCAcquisitionStore (SampleCount, Cycles: TCycle with Program (index into the program list), Cycle (zero-based), Acquisitions: THTCFloAcquisition Time ms, Temp ($ + 16 hex digits of a big-endian f64, °C), Channel, Valid, FloPoints = base64 of one little-endian f32 per plate position) |
amplification curves of the Quantification program (cycle = Cycle + 1; each cycle must hold exactly one reading per channel, consecutive cycles, else the program is not read, with a note); melt curves of each Melting Curves program (temperature = Temp of each reading). Valid ≠ 1 → NaN readings, noted. Readings of the wrong length, bad base64 or zlib, or a channel outside the detection format: corrupt (exit 4) |
AnlsAndSampleInfo/SampleInfoList (SamplePropInfo: ContainerPosition; SampleProperties GenSampleEditName name; per channel (ChannelIdx) TargetName name, QuantSampleTypeProperty SampleType, QuantConcProperty Concentration) |
sample; per channel target (a channel without a target name is measured under the channel’s own name, e.g. 498-640), task (qsUnknown → unknown, qsStandard → standard, qsNegative → negative, qsPositive → positive, others as written), quantity (standards) |
analyses (AnalysisList): Legacy Absolute Quantification Analysis (name, ProgramInfo/SelectedProgram, ChannelRatio NumeratorXWL/NumeratorEWL (the channel; DenominatorEWL 0 = no ratio), AnaSamples: QuantSampleB Pos, IsIncluded, Call, CrossingPoint, CalcConc, ConcStatus) |
for the analysed channel: Call 2 → cq = CrossingPoint, amp_status amplified; Call 0 → undetermined, not amplified; any other call → no Cq, cq_stored = the Cp, flag lc480_call_<n> (no such call in the corpus); IsIncluded 0 → excluded. CalcConc/ConcStatus stay in the file (not mapped). Other analysis classes, ratio analyses and a second analysis of the same channel are listed in vendor.analyses with read: false and a note |
Layout: one run (the LightCycler run’s name, Run) of wells in row-major position order (Pos k = FloPoints index k), each with one assay per channel holding the amplification curve and, when the file has exactly one melting program, its melt curve. With several melting programs (the QC runs have Melting A and Melting B) each melting program is a run of its own, named after the program, holding only melt curves. vendor: experiment, software_version, macro, instrument_id, plate_id, detection_format, channels (name, excitation_nm, emission_nm), programs (name, analysis_mode), analyses (class, name, positions, channel, program, read), calculators (class, version), trailer. The calculators’ binary result arrays (DARZ, LARZ, FORM, BAR1 properties: smoothed melt curves, the temperature log) are not decoded; −dF/dT is computed from the raw melt readings. Fluorescence is the instrument’s stored value (no color compensation applied).
Validated on four LightCycler 480 QC runs (two instruments, software 1.5.0.39 and 1.5.1.62) against a Python standard-library reading of the same files (sample names, calls, Cps, count and sum of every program × channel’s readings) and against the vendor’s own calls: every position called positive rises 1.74–1.97× in the analysed channel, every one called negative stays within 0.92–1.05×.
Analysis (openreadout analyze qpcr)
Our own computations, reported next to the vendor’s (never replacing them):
- Cq (
--cq): per curve, subtract a baseline — the file’s background line (RDMLbgFluor+bgFluorSlp·cycle), else the file’s baseline window when it records the one used (JSON-layout.edsper well; SDS/7500 layout only withAutoBaselinefalse), else a least-squares line through cycles 3-15, re-fitted with the window ending three cycles before the resulting Cq (up to three times) so early curves do not leak into their baseline — then find the first cycle where the corrected curve rises through the threshold and stays above it at the next cycle, interpolating linearly. Threshold:--threshold, else the file’s (JSON-layoutctThresholdper well, RDMLquantFluor, SDS/7500ThresholdwhenAutoCtis false), else for SDS/7500 files the vendor’s automatic threshold recovered per target as the median of the vendor’s ΔRn at the vendor’s Cq, else 10 standard deviations of the baseline residuals. With that automatic threshold a crossing counts only when the stored fluorescence rises above its baseline-window level by at least the threshold after it: decaying, photobleaching negative curves otherwise cross it once their baseline line is removed.cq_comparisonreports how our Cq agrees with the vendor’s: curves compared, both/neither/only-one with a Cq, mean and median differences, maximum, fraction within 0.1 and 0.5 cycles, Pearson r. - Undetermined results (no amplification):
cqnull,cq_statusundetermined(cq_undeterminedtrue), whatever the file writes (text, RDML-1orcq= cycle count, JSONNaN/-1, SDS/7500Ct= cycle count). They are never averaged as a number.cq_countsandtargets[]count them.targets[].cq_mean/cq_sdand ΔΔCq leave them out (the vendor’s replicate means do too), andrelative_quantities[].undeterminedcounts them per sample × target.--undetermined-as CQ(undetermined_cq) counts them at a fixed Cq instead, e.g. the cycle count. Our own Cq follows the same rule: a curve that never crosses the threshold, or crosses it only at or after the last cycle, iscomputed_cq_statusundetermined. - ΔΔCq (
--ddcq,--reference TARGET,--control SAMPLE; defaults: the file’s endogenous control / reference-type targets and calibrator): per sample and target, the mean Cq of the usable wells (a Cq, not omitted or excluded, not NTC/NAC/NTP/NRT, not standards); the reference Cq of a sample is the mean over reference targets of their mean Cq; ΔCq = target − reference; ΔΔCq = ΔCq − ΔCq(control); RQ = 2^−ΔΔCq with RQ min/max at ±1 SD of ΔCq (target and reference SDs in quadrature). When every efficiency involved is known (RDMLamplificationEfficiencyas a fold, 1-2.5), the efficiency-corrected ratio (Pfaffl; the geometric mean over reference targets) is also given. The vendor’s own ΔCt and RQ are carried next to ours when the file stores them. - Standard curve (
--standard-curve): per target, a least-squares line of Cq against log10(quantity) over the usable standard wells (≥ 2 quantities):slope,intercept,r2,efficiency_percent= (10^(−1/slope) − 1) × 100; the vendor’s slope, efficiency and r² are carried next to it when the file stores its fit.
Vocabulary
Public identifiers of crates/openreadout-qpcr (checked by cargo xtask vocab-check):
| identifier | meaning |
|---|---|
RdmlReader, EdsReader, PcrdReader, RexReader, IxoReader |
the five format readers |
RDML_FORMAT_ID, EDS_FORMAT_ID, PCRD_FORMAT_ID, REX_FORMAT_ID, IXO_FORMAT_ID |
their format ids (rdml, applied-biosystems-eds, bio-rad-pcrd, rotor-gene-rex, roche-lightcycler-ixo) |
ixo (dialect), lc96_negative_call, lc96_call_not_positive, lc480_call_<n> (flags), samples[].id |
the LightCycler 480 dialect id; flags of results whose stored number is not a Cq; the file’s GUID of a sample renamed to its description |
QpcrDataset, open, format_id, path |
an opened qPCR file; open with detection of the dialect; its format id; the path opened |
open_qpcr |
open a qPCR file through a registry (a .pcrd gets its exit-6 refusal, other formats a hint) |
RESULT_COLUMNS |
the column names of table 0 |
CQ_STATUSES, CQ_DETERMINED, CQ_UNDETERMINED, CQ_NO_RESULT |
the cq_status values (determined, undetermined, no result) and their order as table 0 codes |
export_rdml, default_rdml_output, RDML_WRITE_VERSION |
RDML writer, its default output name (name.rdml, name.export.rdml for RDML input), the RDML version written (1.3) |
RdmlExportReport |
what the writer reports: input, output, format, rdml_version, runs, reactions, data_elements, cq_values, amplification_points, melt_points, samples, targets, dyes, bytes_written, verified, notes |
qpcr_report, QpcrReportRequest |
the analyze qpcr computation and its request: well, target, sample, run (filters), compute_cq, threshold, baseline, reference_targets, control_sample, relative, standard_curve, max_records, undetermined_cq (a Cq to count undetermined results at in means and ΔΔCq) |
QpcrReport |
path, format, dialect, experiment, instrument, acquisition_temperature_c, cycles, reference_targets, control_sample, record_count, truncated, records, cq_counts, targets, undetermined_cq, cq_comparison, relative_quantities, standard_curves, notes |
QpcrAssayRecord |
one well × target: run, well, row, col, sample, target, dye, task, quantity, cq, cq_undetermined, cq_status, cq_stored (the number an SDS/7500 file stores for an undetermined result), cq_mean, cq_sd, tm, threshold, auto_threshold, baseline_start, baseline_end, amp_status, cq_confidence, calculated_quantity, efficiency, excluded, flags, vendor_delta_cq, vendor_rq, computed_cq, computed_threshold, computed_cq_status, cycles |
CqCounts |
records, determined, undetermined, no_result, excluded |
TargetCqSummary |
per target: target, counts (a CqCounts), averaged, cq_mean, cq_sd, cq_min, cq_max (over non-excluded records with a Cq, plus undetermined ones at undetermined_cq) |
CqComparison |
method, curves, both_cq, both_undetermined, only_vendor, only_ours, vendor_threshold_used, mean_difference, mean_abs_difference, median_abs_difference, max_abs_difference, within_0_1, within_0_5, pearson_r |
RelativeQuantity |
sample, target, replicates, undetermined, cq_mean, cq_sd, reference_cq_mean, delta_cq, delta_cq_sd, delta_delta_cq, rq, rq_min, rq_max, efficiency_corrected_rq, vendor_delta_cq, vendor_rq |
StandardCurveFit |
target, points, levels, slope, intercept, r2, efficiency_percent, vendor_slope, vendor_efficiency_percent, vendor_r2 |
Validation
cargo test -p openreadout-corpus-tests --features corpus --test qpcr compares every corpus file with corpus/oracle/qpcr/<id>.json (oracle/qpcr.py): RDML against rdmlpython (every reaction’s sample, target, dye, task, Cq, excluded flag, Tm and the count and sum of its amplification and melt points), .eds against the vendor’s own result files read with the Python standard library (Cq, undetermined calls, Tm list, Rn and ΔRn sums, melt sums, sample, target and task per well, program cycles and read temperature) and, where qslib opens the file, the per-dye multicomponent signal against qslib’s; .rex against ElementTree sums of the raw readings. The same test exports every readable file to RDML, validates it with rdmlpython’s schema check (when the oracle venv is present) and re-reads it. Results and agreement statistics of our analyses are in docs/provenance/qpcr.md (2026-09-24 entry).
tests/qpcr_exports.rs compares 8 StepOnePlus runs (7500 layout) with the StepOne Software Results export of each (oracle/qpcr_exports.py, xlrd): every Ct and “Undetermined” (574 wells, 76 Undetermined), Ct Mean and SD, threshold, baseline window and Tm1–Tm3 (780 values).
Our Cq agrees with the vendor’s (median |d| 0.01–0.03 cycles, max 0.13, r ≥ 0.9999) only where the file records the vendor’s threshold and baseline, or they can be recovered: JSON-layout .eds per well, RDML with quantFluor/bgFluor, and SDS/7500 .eds from Rn − ΔRn and the ΔRn at the vendor’s Ct. For files that record neither (Bio-Rad CFX, LightCycler 96, StepOne RDML), our automatic 10 SD threshold is a different method. The comparison there (cq_comparison.vendor_threshold_used = 0) shows method differences of one to several cycles, not reader errors.
Measured 2026-09-26 (tests/qpcr_roche.rs): LightCycler 480 (vendor “2nd Derivative Max” Cp, 4 runs): 160 of 160 vendor Cqs also ours, ours later by a median 0.83–1.14 cycles (max 1.66), 1 Cq of ours on 224 vendor-negative wells; LightCycler 96 (3 runs): 104 of 108 vendor Cqs also ours, median |d| 2.3–4.2 cycles, ours earlier. Use the vendor cq of these files; ours is a cross-check that the curves amplify.
How this reader was derived, file by file: provenance log.