Extracted-Ion Chromatograms (XIC) View¶
An extracted-ion chromatogram (XIC) is the intensity of one precursor or fragment ion as a function of retention time. In DIA experiments every quantified precursor is supported by such traces, and they are the evidence behind a peak boundary, an apex, an area, and a shape-based score. The xic view stores these traces as a first-class QPX data view so they can be exported by DIA tools, validated, joined to features, and plotted without re-extracting from the raw file.
Why trace-level rows¶
The view stores one row per chromatogram trace, not one row per retention-time point. A trace is a single entity: it has one precursor, one transition annotation, and one run. Storing it as a row with aligned rt/intensity arrays (and an optional per-point mz) has three practical advantages over the long (precursor, transition, rt, intensity) form emitted by DIA-NN:
- One row per entity. A DIA-NN XIC export for one 2-hour run has ~1.8 million point rows; the same data is ~350,000 trace rows here (the diann-export test run: 27,009 precursors, 348,418 traces). Fewer rows mean fewer repeated precursor/transition strings and a row that matches the entity consumers reason about.
- One fetch per plot. A viewer asking for one precursor's traces reads a handful of rows instead of scanning a precursor's worth of points.
- Per-trace operations are natural. Apex, area, RT span, and point count are computed without a
GROUP BYover millions of rows.
The row reduction does not by itself guarantee a smaller file: the long Parquet form already dictionary-encodes the repeated strings, and in the DIA-NN test export the trace-level QPX file was about 10% larger (~6.7 MB vs ~6.1 MB). The gain is in query shape and entity semantics, not compression.
Point-level access is still one UNNEST away; see Querying.
Aligned arrays and point rules
rt, intensity and, when present, mz MUST have the same length within a row; element i is the measurement at rt[i]. Retention times are stored in seconds, are raw run RTs (not aligned or iRT-corrected), and must be strictly increasing. Validation reports length mismatches as chromatogram_length_mismatch and non-increasing RTs as rt_not_increasing. Zero-intensity points are kept, and QPX does not smooth the arrays; a producer that smooths should declare it in the file metadata. Note that mz.scan_start_time is in minutes while xic.rt and feature.rt are in seconds.
Selection and size
A trace-level file is not automatically smaller than a long export. More importantly, the 17 GB projection in the proposal assumes DIA-NN apex windows (~5 points per trace). A full extraction window (e.g. ±300 s) has tens of times more points per trace and can make the file hundreds of GB, approaching the size of the inline spectra. The XIC file therefore declares its selection in the footer: xic_selection (targets, targets_and_decoys, all), xic_window (apex, full), optionally xic_extraction_window_seconds and xic_qvalue_threshold, and transition_format (default mzPAF). Rescoring that needs decoys or sub-threshold targets must use an export that contains them.
Schema¶
See the full YAML schema in xic.yaml.
Identity and links¶
| Field | Description | Type | Required |
|---|---|---|---|
xic_id |
Opaque identifier of the trace, unique within the file; the primary key | string |
Yes |
run_file_name |
Raw file containing the trace (without extension); links to run, feature, psm, mz |
string |
Yes |
label |
Channel label: "LFQ" for label-free DIA, or a reporter/plex label; matches feature.intensities[].label |
string |
Yes |
feature_ids |
Optional links to the feature.feature_id rows this trace supports; a trace can support several candidate peak groups |
array[int64] |
Optional |
feature_scores |
Per-feature scores for this trace (area, apex, mass error, shape, …) when they depend on the feature | array[struct] |
Optional |
compensation_voltage |
FAIMS compensation voltage; part of the trace identity when the acquisition uses FAIMS | float32 |
Optional |
Precursor identity¶
| Field | Description | Type | Required |
|---|---|---|---|
precursor_id |
Producer's precursor identifier, preserved verbatim (DIA-NN pr: modified sequence + charge) |
string |
Yes |
peptidoform |
Precursor in ProForma notation; links to feature.peptidoform and psm.peptidoform |
string |
Yes |
charge |
Precursor charge state | int16 |
Yes |
Traced ion¶
| Field | Description | Type | Required |
|---|---|---|---|
transition |
Transition annotation: "ms1" for the precursor trace, or a fragment label such as "y7^1" / "b4^2"; fragment annotations follow mzPAF where the producer supports it |
string |
Yes |
transition_type |
"ms1" for the precursor trace, "fragment" for a fragment-ion trace |
string |
Yes |
fragment_type |
Fragment-ion series (b, y, …); null for MS1 traces |
string |
Optional |
fragment_number |
Fragment ordinal from the terminus (y7 → 7) |
int16 |
Optional |
fragment_charge |
Charge of the traced fragment (y7^1 → 1) |
int16 |
Optional |
neutral_loss |
Neutral-loss annotation (-H2O); null when none |
string |
Optional |
isotope |
Isotope index: 0 monoisotopic, 1 = M+1, …; applies to precursor and fragment traces |
int16 |
Optional |
target_mz |
m/z the trace was extracted at: precursor m/z for ms1, product m/z for a fragment |
float32 |
Optional |
target_rt |
Expected/target retention time in seconds (library or predicted apex) | float32 |
Optional |
rt_start |
Start of the extraction window in seconds | float32 |
Optional |
rt_stop |
End of the extraction window in seconds | float32 |
Optional |
target_ion_mobility |
Target ion-mobility value (1/K0) | float32 |
Optional |
ion_mobility_start |
Start of the ion-mobility extraction window (1/K0) | float32 |
Optional |
ion_mobility_stop |
End of the ion-mobility extraction window (1/K0) | float32 |
Optional |
library_intensity |
Library or predicted relative intensity of the fragment | float32 |
Optional |
used_for_quantification |
True when the producer used the trace for quantification/scoring, false when only extracted | boolean |
Optional |
Measurement and metadata¶
| Field | Description | Type | Required |
|---|---|---|---|
rt |
Raw retention times in seconds, strictly increasing; aligned with intensity and mz |
array[float32] |
Yes |
intensity |
Raw intensity at each retention time; zeros kept, not smoothed by QPX | array[float32] |
Yes |
mz |
Optional observed m/z at each retention time, aligned with rt |
array[float32] |
Optional |
is_decoy |
Decoy flag for the precursor when the producer reports one | boolean |
Optional |
additional_scores |
Trace-level producer scores that do not depend on a specific feature | array[struct] |
Optional |
cv_params |
Trace-specific controlled-vocabulary metadata; run-level extraction settings belong in run/provenance metadata | array[struct] |
Optional |
Tracing one entity through QPX¶
graph LR
RUN["run<br/>run_file_name"] --> XIC["xic<br/>one trace per row"]
FEAT["feature<br/>peptidoform + charge<br/>+ run_file_name"] --> XIC
XIC --> PTS["XIC points<br/>UNNEST(rt), UNNEST(intensity)"]
style XIC fill:#e8f5e9
style PTS fill:#e8f5e9
| From | To | Join predicate |
|---|---|---|
xic |
run |
xic.run_file_name = run.run_file_name |
xic |
feature |
xic.run_file_name = feature.run_file_name AND xic.peptidoform = feature.peptidoform AND xic.charge = feature.charge; match xic.label to the feature's intensity label when multiplexed |
xic |
feature (direct) |
feature.feature_id IN xic.feature_ids when the producer assigns them; a trace can support several candidate features |
xic |
sample |
resolve (run_file_name, label) through run.samples[] exactly as for feature and pg |
precursor_id is kept for round-tripping to the source tool. Join on the normalized peptidoform + charge, not on precursor_id, because identifier syntax is tool-specific.
Transition names¶
transition uses mzPAF-style fragment annotations where the producer supports them (y7^1, b4^2, neutral losses, isotopes); ms1 is reserved for the precursor trace. The structured columns make the annotation queryable without string parsing:
transition |
transition_type |
fragment_type |
fragment_number |
fragment_charge |
neutral_loss |
isotope |
|---|---|---|---|---|---|---|
ms1 |
ms1 |
null | null | null | null | 0 |
y7^1 |
fragment |
y |
7 | 1 | null | 0 |
b4^2 |
fragment |
b |
4 | 2 | null | 0 |
y9-H2O^1 |
fragment |
y |
9 | 1 | -H2O |
0 |
DIA-NN mapping¶
DIA-NN writes one *.xic.parquet per run with the columns pr, feature, info, rt, and value. Map it as follows.
| DIA-NN column | QPX field | Transformation |
|---|---|---|
file name <run>.xic.parquet |
run_file_name |
strip the .xic.parquet suffix |
pr |
precursor_id |
copy verbatim |
pr |
peptidoform + charge |
strip the trailing charge; convert (UniMod:N) to ProForma [UNIMOD:N] |
feature |
transition |
copy; ms1 reserved for the precursor trace; reorder charge/loss to mzPAF (y9^1-H2O → y9-H2O^1) |
feature |
transition_type, fragment fields |
ms1 → ms1; y7^1 / b4^2 → fragment plus parsed fields |
| — | isotope |
0 (DIA-NN extracts monoisotopic targets) |
info |
— | 0 on signal rows; values 1..N occur only on feature = index rows (see below) |
rt |
rt |
multiply by 60 (DIA-NN reports minutes; QPX stores seconds) |
value |
intensity |
copy |
| — | label |
set to "LFQ" for label-free DIA-NN output |
| — | xic_id |
<run_file_name>|<pr>|<feature>|<isotope>|<compensation_voltage> |
| — | feature_ids |
optional; null unless the converter also links the trace to feature rows |
| — | target_mz, target_rt, windows, IM |
not reported by DIA-NN XIC; null |
| — | is_decoy |
optional, from the DIA-NN report/feature row |
The DIA-NN converter writes file-level selection metadata: xic_selection=targets, xic_window=apex, xic_smoothed=false, xic_zero_points=kept, xic_rt_unit=seconds, and transition_format=mzPAF. Override --xic-selection, --xic-window, --xic-extraction-window-seconds and --xic-qvalue-threshold when the export differs.
Exclude DIA-NN index rows
DIA-NN also writes rows with feature = "index", rt = 0, and value = 0. They are not chromatogram points: they chunk the precursors in blocks of 128 and carry the block number in info (0, 1, 2, …). Discard them when building QPX traces; only feature = "ms1" and fragment labels are measurements.
Example¶
One row for the y7^1 fragment trace of a precursor:
{
"xic_id": "RD139_Narrow_UPS1_0_1fmol_inj1|PEPTIDEK2|y7^1|0|None",
"run_file_name": "RD139_Narrow_UPS1_0_1fmol_inj1",
"label": "LFQ",
"feature_ids": [1048576],
"feature_scores": null,
"compensation_voltage": null,
"precursor_id": "PEPTIDEK2",
"peptidoform": "PEPTIDEK",
"charge": 2,
"transition": "y7^1",
"transition_type": "fragment",
"fragment_type": "y",
"fragment_number": 7,
"fragment_charge": 1,
"neutral_loss": null,
"isotope": 0,
"target_mz": 842.4512,
"target_rt": 63.52,
"rt_start": 58.0,
"rt_stop": 68.0,
"target_ion_mobility": null,
"ion_mobility_start": null,
"ion_mobility_stop": null,
"library_intensity": null,
"used_for_quantification": true,
"rt": [63.402126, 63.462021, 63.522034, 63.582001, 63.642014],
"intensity": [25123.576, 43031.266, 49921.051, 38102.734, 22010.117],
"mz": null,
"is_decoy": false,
"additional_scores": null,
"cv_params": null
}
Querying¶
Trace-level rows keep the common query simple:
-- All traces for one precursor in one run
SELECT transition, transition_type, rt, intensity
FROM 'PXD014414.xic.parquet'
WHERE run_file_name = 'RD139_Narrow_UPS1_0_1fmol_inj1'
AND precursor_id = 'PEPTIDEK2';
Expand to points with DuckDB's zipped UNNEST (the arrays are aligned, so both unnest to the same length):
SELECT xic_id, transition, feature_ids,
UNNEST(rt) AS rt_seconds,
UNNEST(intensity) AS intensity
FROM 'PXD014414.xic.parquet';
Compute an apex and a summed intensity per trace:
SELECT xic_id, transition,
MAX_BY(rt_seconds, intensity) AS apex_rt,
SUM(intensity) AS summed_intensity,
COUNT(*) AS points
FROM (
SELECT xic_id, transition,
UNNEST(rt) AS rt_seconds,
UNNEST(intensity) AS intensity
FROM 'PXD014414.xic.parquet'
)
GROUP BY ALL;
Join the traces to their quantified features:
SELECT f.feature_id, f.anchor_protein, x.transition, x.rt, x.intensity
FROM 'PXD014414.feature.parquet' AS f
JOIN 'PXD014414.xic.parquet' AS x
ON x.run_file_name = f.run_file_name
AND x.peptidoform = f.peptidoform
AND x.charge = f.charge
WHERE f.run_file_name = 'RD139_Narrow_UPS1_0_1fmol_inj1';
From Python, Xic.points() returns the expanded long form directly:
from qpx import Dataset
ds = Dataset("PXD014414/")
ds.xic.by_precursor("PEPTIDEK2").to_df() # one row per trace
ds.xic.by_precursor("PEPTIDEK2").points().to_df() # one row per RT point
Notes and boundaries¶
DIA and targeted data only
The view describes extracted traces. It does not store DDA profile spectra; use the mz view for those. A DIA feature may exist without an XIC (if the tool did not export traces), and xic.feature_ids is therefore optional.
Multiplexed DIA
One trace is measured per channel. Emit one row per label and keep label equal to the feature's intensity label so the traces resolve to the right sample through run.samples[]. Label-free DIA uses the single label "LFQ".
RT-only; mobilograms are out of scope
xic stores intensity versus retention time. Mobilograms (intensity versus ion mobility at a feature apex) are a different axis and are not represented by the rt/intensity arrays; a future xim view should cover them. For diaPASEF XIC traces, the ion-mobility window is carried as per-trace scalars (target_ion_mobility, ion_mobility_start, ion_mobility_stop) without changing the RT axis.
Feature links are many-to-many
A trace is precursor-level evidence and may support several candidate peak groups, so feature_ids is a list and may be null when the producer assigns no feature. Scores that depend on a feature go in feature_scores (one entry per feature_id), while additional_scores holds trace-level scores. The fallback join on (run_file_name, peptidoform, charge[, label]) is not unique in general: the default feature identity includes rt, and FAIMS producers can include compensation_voltage. Use feature_ids when it is present, and treat the fallback as a precursor-level association rather than a row-level foreign key.
Decoys
When a producer extracts traces for decoy precursors, keep them and set is_decoy = true; consumers filter explicitly. A null is_decoy means the trace was not classified.
Converting DIA-NN XIC exports¶
DIA-NN writes one *.xic.parquet per run when --xic is enabled. Merge them into one QPX view with the standalone converter:
qpxc convert diann-xic \
--xic-folder ./diann_xic \
--output-folder ./qpx \
--output-prefix PXD014414 \
--xic-selection targets \
--xic-window apex
To add the XIC view to a full report conversion, pass the same folder to the DIA-NN converter:
qpxc convert diann \
--report-path report.tsv \
--sdrf-file data.sdrf.tsv \
--xic-folder ./diann_xic \
--output-folder ./qpx
File naming¶
The view follows the standard QPX convention:
For example, PXD014414.xic.parquet. A QPX dataset has one dataset-level XIC file for all runs, not one file per run: run_file_name identifies the run of each trace, and xic_id embeds it so traces from different runs stay unique. A DIA-NN per-run export is also named <run>.xic.parquet; convert those into the single dataset-level QPX XIC file rather than mixing raw DIA-NN exports into a QPX dataset directory.
See Also¶
- Feature View -- the quantified peaks these traces support.
- Mass Spectra -- scan-level spectra for DDA and DIA.
- Intensities -- how
labelties a quantity to a sample. - Serialization -- Parquet encoding and file metadata.