qpictures 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,45 @@
1
+ # Changelog
2
+
3
+ All notable changes to **qPiCtuRes** are documented here. This project
4
+ follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and the
5
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format.
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-06-10
10
+
11
+ Initial public release.
12
+
13
+ ### Added
14
+
15
+ - `qpictures inspect` — quick per-sample Cq summary of a Thermo QuantStudio
16
+ Design & Analysis "Well Results" CSV.
17
+ - `qpictures relative` — relative quantification (Livak ΔCq) against a
18
+ single reference sample, with asymmetric error bars on a log y-axis,
19
+ optional Welch / Student / Mann–Whitney significance testing, optional
20
+ multiple-testing correction (BH / Holm / Bonferroni), and an optional
21
+ background-control threshold.
22
+ - `qpictures paired` — per-condition paired RQ plot. Splits sample names
23
+ on a separator (e.g. `BB_pbs` → base=`BB`, condition=`pbs`) and
24
+ normalizes every base in each condition against a reference base in the
25
+ same condition. Adds visual grouping cues (alternating shading,
26
+ reference-bar hatching, per-group `vs <ref_sample>` x-tick labels).
27
+ - `qpictures standcurve` — absolute quantification via an OLS standard
28
+ curve (`Cq = slope · log10(Q) + intercept`). Reports slope, intercept,
29
+ R², PCR efficiency, residual SD; back-calculates per-sample quantities
30
+ with error bars propagated from Cq SD; flags extrapolations outside the
31
+ calibrated standard range. Combined HTML stacks the curve diagnostic
32
+ with the per-sample bar plot.
33
+ - Python API: `read_thermo_well_results`, `relative_quantification`,
34
+ `paired_relative_quantification`, `fit_standard_curve`,
35
+ `absolute_quantification`, plus matching plot builders
36
+ (`relative_quantity_barplot`, `paired_relative_quantity_barplot`,
37
+ `standard_curve_plot`, `absolute_quantity_barplot`,
38
+ `standard_curve_with_quantities_figure`).
39
+ - Optional static-image export (PDF / PNG / SVG) for every plotting
40
+ command via Plotly + `kaleido` (installed by `pip install qpictures[export]`).
41
+ - Tolerant CSV parser that auto-skips the Design & Analysis metadata
42
+ preamble and re-glues sample names containing unquoted commas.
43
+
44
+ [Unreleased]: https://github.com/SemiQuant/qPiCtuRes/compare/v0.1.0...HEAD
45
+ [0.1.0]: https://github.com/SemiQuant/qPiCtuRes/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jason Limberis (SemiQuant) <JasonLimberis@ucsf.edu>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,17 @@
1
+ include README.md
2
+ include LICENSE
3
+ include CHANGELOG.md
4
+ include pyproject.toml
5
+ include qpictures/py.typed
6
+
7
+ # Source distribution should not ship local outputs, raw data, or env files.
8
+ prune data
9
+ prune results
10
+ prune build
11
+ prune dist
12
+ prune .github
13
+ prune .venv
14
+ recursive-exclude qpictures __pycache__
15
+ recursive-exclude qpictures *.py[cod]
16
+ global-exclude .DS_Store
17
+ global-exclude *.egg-info
@@ -0,0 +1,383 @@
1
+ Metadata-Version: 2.4
2
+ Name: qpictures
3
+ Version: 0.1.0
4
+ Summary: Interactive plots and analyses for qPCR data exported from Thermo QuantStudio Design & Analysis.
5
+ Author-email: "SemiQuant (Jason Limberis)" <JasonLimberis@ucsf.edu>
6
+ Maintainer-email: "SemiQuant (Jason Limberis)" <JasonLimberis@ucsf.edu>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/SemiQuant/qPiCtuRes
9
+ Project-URL: Source, https://github.com/SemiQuant/qPiCtuRes
10
+ Project-URL: Issues, https://github.com/SemiQuant/qPiCtuRes/issues
11
+ Project-URL: Changelog, https://github.com/SemiQuant/qPiCtuRes/blob/main/CHANGELOG.md
12
+ Keywords: qpcr,rt-qpcr,plotly,bioinformatics,thermo,quantstudio,relative-quantification,standard-curve,absolute-quantification
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
17
+ Classifier: Topic :: Scientific/Engineering :: Visualization
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: pandas>=2.0
30
+ Requires-Dist: numpy>=1.24
31
+ Requires-Dist: scipy>=1.10
32
+ Requires-Dist: plotly>=5.20
33
+ Requires-Dist: click>=8.1
34
+ Requires-Dist: rich>=13.0
35
+ Provides-Extra: export
36
+ Requires-Dist: kaleido>=0.2.1; extra == "export"
37
+ Provides-Extra: dev
38
+ Requires-Dist: build>=1.2; extra == "dev"
39
+ Requires-Dist: twine>=5.0; extra == "dev"
40
+ Requires-Dist: pytest>=8.0; extra == "dev"
41
+ Requires-Dist: kaleido>=0.2.1; extra == "dev"
42
+ Dynamic: license-file
43
+
44
+ # qPiCtuRes
45
+
46
+ [![PyPI - Version](https://img.shields.io/pypi/v/qpictures.svg)](https://pypi.org/project/qpictures/)
47
+ [![Python](https://img.shields.io/pypi/pyversions/qpictures.svg)](https://pypi.org/project/qpictures/)
48
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
49
+
50
+ Interactive plots and analyses for qPCR data exported from Thermo Fisher
51
+ QuantStudio Design & Analysis.
52
+
53
+ The first module turns a *Well Results* CSV into an interactive Plotly bar plot
54
+ of **relative quantities** (RQ = 2<sup>−ΔCq</sup>) against a chosen reference
55
+ sample, with log-10 y-axis and asymmetric error bars propagated from the Cq
56
+ SDs (Livak & Schmittgen, 2008).
57
+
58
+ ## Install
59
+
60
+ ### From PyPI (end users)
61
+
62
+ ```bash
63
+ pip install qpictures
64
+ # with optional static-image export (PDF / PNG / SVG via kaleido)
65
+ pip install "qpictures[export]"
66
+ ```
67
+
68
+ ### From source (developers)
69
+
70
+ The repo ships with an `environment.yml` (Python 3.11) for `micromamba`:
71
+
72
+ ```bash
73
+ micromamba env create -f environment.yml
74
+ micromamba activate qpictures
75
+ # the env file already runs `pip install -e .`; if you skipped that:
76
+ pip install -e ".[dev]"
77
+ ```
78
+
79
+ ## CLI
80
+
81
+ ```text
82
+ qpictures inspect <input.csv> # per-sample Cq summary
83
+ qpictures relative <input.csv> -r BB_PBS [-t atpE] # RQ vs one reference sample
84
+ qpictures paired <input.csv> -b BB [-t atpE] # per-condition paired RQ
85
+ qpictures standcurve <input.csv> [-t atpE] # absolute Q from a std curve
86
+ ```
87
+
88
+ ### `relative` — single reference sample
89
+
90
+ ```bash
91
+ qpictures relative data/ex1/WellResult-2026-06-09-144852.csv \
92
+ --reference BB_PBS \
93
+ --out results/ex1_relative.html \
94
+ --csv-out results/ex1_relative.csv
95
+ ```
96
+
97
+ Useful flags:
98
+
99
+ | Flag | Default | What it does |
100
+ | --- | --- | --- |
101
+ | `-r, --reference` | *required* | Reference sample (case-insensitive). |
102
+ | `-t, --target` | auto | Which assay/target to use (auto if only one). |
103
+ | `-o, --out` | `<input>.relative.html` | Output Plotly HTML. |
104
+ | `--csv-out` | *(off)* | Also write the RQ table as CSV. |
105
+ | `--pdf [PATH]` | *(off)* | Also write a PDF (auto-named from `--out` if no path). |
106
+ | `--png [PATH]` | *(off)* | Also write a PNG. |
107
+ | `--svg [PATH]` | *(off)* | Also write an SVG. |
108
+ | `--test` | `welch` | Per-sample significance test vs reference: `welch`, `student`, `mannwhitney`, `none`. |
109
+ | `--padjust` | `none` | Multiple-testing correction: `none`, `bh` (FDR), `holm`, `bonferroni`. |
110
+ | `--no-signif` | off | Hide significance stars on the plot. |
111
+ | `--background-rem [NAME]` | off | Use a named control sample as the background threshold. As a bare flag it looks up a sample called `control` (case-insensitive); pass an explicit name to override, e.g. `--background-rem PBS`. Any sample whose mean Cq exceeds the control's mean Cq is flagged with a `below_background` column and rendered as a dimmed grey bar. The control is **independent of `--reference`**, so you can normalize against one sample and threshold against another. |
112
+ | `--task TEXT` (repeatable) | `Unknown` | Which `Task` values to keep. |
113
+ | `--all-tasks` | off | Keep every `Task` (NTC / standards / etc.). |
114
+ | `--no-log` | log10 on | Linear y-axis instead of log10. |
115
+ | `--no-open` | opens | Do not auto-open the resulting HTML. |
116
+
117
+ The bars show the significance star (e.g. `**`) above the RQ value when a
118
+ test is run. See **Significance testing** below for the star thresholds and
119
+ caveats — the same rules apply in both `relative` and `paired` modes.
120
+
121
+ ### `paired` — per-condition paired comparisons
122
+
123
+ For sample names of the form `<base><sep><condition>` (e.g. `BB_pbs`,
124
+ `TL_culture_swab`), normalize every base within each condition against a
125
+ **reference base** (e.g. `BB`). Each condition gets a group on the x-axis,
126
+ bars within a group are colored by base, and the reference base sits at
127
+ RQ = 1 (with its own SD propagated).
128
+
129
+ ```bash
130
+ qpictures paired data/ex1/WellResult-2026-06-09-144852.csv \
131
+ --reference-base BB \
132
+ --out results/ex1_paired.html \
133
+ --csv-out results/ex1_paired.csv
134
+ ```
135
+
136
+ | Flag | Default | What it does |
137
+ | --- | --- | --- |
138
+ | `-b, --reference-base` | *required* | Reference base (case-insensitive), e.g. `BB`. |
139
+ | `-s, --separator` | `_` | Split `base<sep>condition` on this. |
140
+ | `-t, --target` | auto | Which assay/target to use. |
141
+ | `-o, --out` | `<input>.paired.html` | Output Plotly HTML. |
142
+ | `--csv-out` | *(off)* | Also write the paired RQ table as CSV. |
143
+ | `--pdf [PATH]` | *(off)* | Also write a PDF (auto-named from `--out` if no path). |
144
+ | `--png [PATH]` | *(off)* | Also write a PNG. |
145
+ | `--svg [PATH]` | *(off)* | Also write an SVG. |
146
+ | `--test` | `welch` | Per-pair significance test: `welch`, `student`, `mannwhitney`, `none`. |
147
+ | `--padjust` | `none` | Multiple-testing correction: `none`, `bh` (FDR), `holm`, `bonferroni`. |
148
+ | `--no-signif` | off | Hide significance stars on the plot. |
149
+ | `--background-rem [NAME]` | off | Use a named control sample as a single global background threshold. Bare flag defaults to a sample called `control`; pass an explicit name (e.g. `--background-rem PBS`) to override. Lookup happens *before* the separator split, so a control sample without `_` (e.g. `Control`) is still found. Any (base, condition) row with a mean Cq above the control's mean Cq is flagged in `below_background` and dimmed in the plot. |
150
+ | `--no-split` | off | Suppress splitting on `--separator`. Every sample is treated as its own base under one synthetic condition (`all`), and `--reference-base` must match a *full* sample name (e.g. `BB_pbs`). Equivalent to a single-reference comparison rendered through the paired layout. |
151
+ | `--task TEXT` (repeatable) | `Unknown` | Which `Task` values to keep. |
152
+ | `--all-tasks` | off | Keep every `Task`. |
153
+ | `--no-log` | log10 on | Linear y-axis. |
154
+ | `--no-open` | opens | Don't auto-open the HTML. |
155
+
156
+ Samples lacking the separator (`Input`, etc.) are skipped with a notice,
157
+ as are any conditions where the reference base is missing.
158
+
159
+ The paired plot adds three visual cues so the comparisons inside each group
160
+ are unambiguous:
161
+
162
+ - **Alternating background bands** behind each condition box each comparison
163
+ unit as a single visual group.
164
+ - **Diagonal hatching** on the reference-base bars marks the baseline within
165
+ every group at a glance.
166
+ - A small **`vs <ref_sample>` label under each x-tick** (e.g. `vs BB_pbs`)
167
+ spells out the specific reference sample that group is being compared
168
+ against.
169
+
170
+ The Python API exposes these as `group_shading`, `mark_reference_pattern`
171
+ and `show_per_group_reference` on `paired_relative_quantity_barplot` (all
172
+ default `True`).
173
+
174
+ #### Example: `--background-rem` and `--no-split`
175
+
176
+ ```bash
177
+ # Bare flag: look up a sample called "control" and use its mean Cq as the threshold
178
+ qpictures paired data/ex1/WellResult-2026-06-09-14570.csv \
179
+ --reference-base BB --background-rem
180
+
181
+ # Explicit control sample (case-insensitive)
182
+ qpictures paired data/ex1/WellResult-2026-06-09-14570.csv \
183
+ --reference-base BB --background-rem PBS
184
+
185
+ # Don't split: use BB_pbs as a single reference, every other sample compared to it
186
+ qpictures paired data/ex1/WellResult-2026-06-09-14570.csv \
187
+ --reference-base BB_pbs --no-split --background-rem Control
188
+ ```
189
+
190
+ #### Significance testing
191
+
192
+ For each non-reference (base, condition) pair we run a two-sided test on the
193
+ **raw `Cq` values** (Cq is already log-scaled, so a t-test on Cq is the
194
+ conventional choice). Multiple-testing correction across all non-reference
195
+ pairs is **off by default**; opt in with `--padjust bh` (FDR), `holm`, or
196
+ `bonferroni`:
197
+
198
+ | Threshold (adjusted p) | Star |
199
+ | --- | --- |
200
+ | p < 1e-4 | `****` |
201
+ | p < 1e-3 | `***` |
202
+ | p < 1e-2 | `**` |
203
+ | p < 0.05 | `*` |
204
+ | else | `ns` |
205
+
206
+ Output table columns: `p_value`, `p_adj`, `signif`. The star is drawn above
207
+ the RQ value inside each non-reference bar.
208
+
209
+ **Caveat:** typical qPCR designs have very few replicates (often `n = 3–4`),
210
+ so these p-values have low statistical power. Treat them as a guide, not a
211
+ verdict. With `n=2`, no test can run and `p_value` will be `NaN`.
212
+
213
+ ### `standcurve` — absolute quantification from a standard curve
214
+
215
+ For runs that include a serial dilution of a known quantity (e.g. `DNASD_4`,
216
+ `DNASD_6`, … with `Task == Standard` and a `Quantity` column), fit
217
+
218
+ ```text
219
+ Cq = slope · log10(Quantity) + intercept
220
+ ```
221
+
222
+ and back-calculate the **absolute Quantity** for every `Unknown` sample.
223
+
224
+ ```bash
225
+ qpictures standcurve data/sc_well_table_results-2026-06-10-092019.csv \
226
+ --out results/std.html \
227
+ --csv-out results/std.csv \
228
+ --curve-csv results/std_curve.csv \
229
+ --unit "copies/µL"
230
+ ```
231
+
232
+ The output HTML stacks two interactive panels:
233
+
234
+ 1. **Standard curve**: scatter of the standard replicates plus the OLS fit,
235
+ a ± 1 residual-SD band, and an annotation box summarising
236
+ `slope`, `intercept`, `R²`, `efficiency = 10^(-1/slope) - 1`,
237
+ `slope SE`, `residual SD`, `n` and number of levels. Unknown samples are
238
+ overlaid at their `(log10 Q_hat, Cq_mean)` so you can see whether each one
239
+ falls inside the calibrated range.
240
+ 2. **Absolute-quantity bar plot**: one bar per sample, sorted by quantity,
241
+ with asymmetric error bars from `SD(Cq) / |slope|` propagated through the
242
+ curve. Bars whose mean Cq lies *outside* the standard range are recolored
243
+ (`extrapolated == True`); dotted horizontal guide lines mark the
244
+ calibrated min/max.
245
+
246
+ | Flag | Default | What it does |
247
+ | --- | --- | --- |
248
+ | `-t, --target` | auto | Which assay/target to fit (auto if only one is present). |
249
+ | `-o, --out` | `<input>.standcurve.html` | Combined interactive HTML (curve + bars). |
250
+ | `--curve-html FILE` | *(off)* | Also write a standalone HTML containing only the standard curve. |
251
+ | `--bars-html FILE` | *(off)* | Also write a standalone HTML containing only the bar plot. |
252
+ | `--csv-out FILE` | *(off)* | Per-sample absolute-quantity table as CSV. |
253
+ | `--curve-csv FILE` | *(off)* | Per-replicate standard wells used for the fit. |
254
+ | `--pdf [PATH]` | *(off)* | Also write the combined figure to PDF. |
255
+ | `--png [PATH]` | *(off)* | Also write PNG. |
256
+ | `--svg [PATH]` | *(off)* | Also write SVG. |
257
+ | `--unit TEXT` | *(none)* | Unit string appended to the y-axis title (e.g. `copies/µL`). |
258
+ | `--standard-task TEXT` | `Standard` | `Task` value identifying the standards. |
259
+ | `--unknown-task TEXT` | `Unknown` | `Task` value identifying the samples. |
260
+ | `--background-rem [NAME]` | *(off)* | Use a named control sample (e.g. `Water`) as the background threshold; samples with mean Cq above it are flagged in `below_background` and dimmed. |
261
+ | `--task TEXT` (repeatable) | *(none)* | Extra `Task` values to keep when reading the CSV (in addition to the standard and unknown tasks, which are always kept). |
262
+ | `--no-log` | log10 on | Linear y-axis for the bar plot. |
263
+ | `--no-open` | opens | Do not auto-open the HTML. |
264
+
265
+ **Output table columns** (`--csv-out`):
266
+
267
+ `Target, Sample, n, Cq_mean, Cq_sd, log10_Q, log10_Q_sd, Quantity,
268
+ Quantity_low, Quantity_high, Q_err_minus, Q_err_plus, extrapolated,
269
+ Background_control, below_background`.
270
+
271
+ **Quality cues to watch for**:
272
+
273
+ - `Efficiency` should sit around 90–110 % (slope ≈ −3.32 for 100 %). The
274
+ reported value reflects the *slope*, not the absolute accuracy of any
275
+ single quantity.
276
+ - `R²` should be ≥ 0.98 for a well-behaved dilution series.
277
+ - Any row with `extrapolated == True` had a mean Cq outside the calibrated
278
+ log10-Quantity range — its back-calculated Quantity is an extrapolation
279
+ and should be interpreted with care (the bar is recolored).
280
+
281
+ ### Filtering defaults
282
+
283
+ `read_thermo_well_results()` (and every CLI command) by default keeps **only
284
+ `Omit == false`** wells and **only `Task == "Unknown"`** rows, so NTC /
285
+ standard / endogenous-control wells are excluded automatically. Override
286
+ with `--all-tasks` or `--task <value>` (repeatable) on the CLI, or
287
+ `tasks=None` / `tasks=("Unknown", "Standard")` from Python.
288
+
289
+ `qpictures standcurve` automatically keeps both `Standard` and `Unknown`
290
+ wells (configurable via `--standard-task` / `--unknown-task`); use
291
+ `--task <value>` to also keep additional task categories.
292
+
293
+ ## Python API
294
+
295
+ ```python
296
+ from qpictures import (
297
+ read_thermo_well_results,
298
+ relative_quantification,
299
+ relative_quantity_barplot,
300
+ paired_relative_quantification,
301
+ paired_relative_quantity_barplot,
302
+ fit_standard_curve,
303
+ absolute_quantification,
304
+ standard_curve_plot,
305
+ absolute_quantity_barplot,
306
+ standard_curve_with_quantities_figure,
307
+ )
308
+
309
+ df = read_thermo_well_results("data/ex1/WellResult-2026-06-09-144852.csv")
310
+
311
+ # 1) Single reference sample
312
+ rq, ref = relative_quantification(df, reference="BB_PBS") # case-insensitive
313
+ relative_quantity_barplot(rq, log_y=True).write_html("rq.html")
314
+
315
+ # 2) Per-condition paired (e.g. TL_pbs vs BB_pbs, TL_sputum vs BB_sputum, ...)
316
+ # Welch's t-test on Cq by default; multiple-testing correction is off
317
+ # by default — pass padjust="bh" for Benjamini–Hochberg FDR.
318
+ pairs, summary = paired_relative_quantification(
319
+ df, reference_base="BB", test="welch", padjust="none"
320
+ )
321
+ paired_relative_quantity_barplot(
322
+ pairs, reference_base=summary.reference_base_resolved, log_y=True
323
+ ).write_html("paired.html")
324
+
325
+ # 3) Absolute quantification from a standard curve. The CSV here includes
326
+ # Task == Standard wells (DNASD_*) with known Quantity values plus the
327
+ # usual Unknown wells; we must read both tasks to fit a curve.
328
+ df_sc = read_thermo_well_results(
329
+ "data/sc_well_table_results-2026-06-10-092019.csv",
330
+ tasks=("Unknown", "Standard"),
331
+ )
332
+ curve = fit_standard_curve(df_sc) # auto-picks target
333
+ print(f"slope={curve.slope:.3f} R^2={curve.r_squared:.4f}"
334
+ f" efficiency={curve.efficiency*100:.1f}%")
335
+ abs_table, _ = absolute_quantification(df_sc, curve=curve)
336
+ standard_curve_with_quantities_figure(
337
+ curve, abs_table, quantity_unit="copies/µL"
338
+ ).write_html("standcurve.html")
339
+ ```
340
+
341
+ ## Math
342
+
343
+ For a single target (no endogenous control):
344
+
345
+ - `Cq_mean(sample)` and `Cq_sd(sample)` are computed across technical replicates.
346
+ - `ΔCq = Cq_mean(sample) - Cq_mean(reference)`
347
+ - `SD(ΔCq) = sqrt( SD(sample)^2 + SD(reference)^2 )`
348
+ - `RQ = 2^(-ΔCq)`
349
+ - `RQ_high = 2^( -(ΔCq - SD(ΔCq)) )`
350
+ - `RQ_low = 2^( -(ΔCq + SD(ΔCq)) )`
351
+
352
+ The bar plot uses asymmetric error bars `[RQ - RQ_low, RQ_high - RQ]` so that
353
+ the visual error is symmetric on the log axis.
354
+
355
+ ### Standard curve (absolute quantification)
356
+
357
+ For runs that include serial dilutions of a known input (`Task == Standard`,
358
+ `Quantity` known), OLS on individual replicates yields
359
+
360
+ - `Cq = slope · log10(Q) + intercept`
361
+ - `efficiency = 10^(-1/slope) - 1` (1.0 == 100 %; slope ≈ −3.32 ideal)
362
+
363
+ For each unknown sample with mean Cq `Cq_mean` and SD `Cq_sd`:
364
+
365
+ - `log10(Q_hat) = (Cq_mean - intercept) / slope`
366
+ - `Q_hat = 10 ^ log10(Q_hat)`
367
+ - `SD(log10 Q) = Cq_sd / |slope|` (replicate scatter only)
368
+ - `Q_low = 10 ^ (log10(Q_hat) - SD(log10 Q))`
369
+ - `Q_high = 10 ^ (log10(Q_hat) + SD(log10 Q))`
370
+
371
+ Uncertainty in `slope` / `intercept` is *not* propagated into per-sample
372
+ bounds — they reflect technical-replicate scatter only. The curve's
373
+ `slope_se`, `intercept_se` and `residual_sd` are still surfaced as part of
374
+ the fit (and the ± 1 residual-SD band on the standard-curve plot).
375
+
376
+ ## Notes on the Thermo export
377
+
378
+ The parser is tolerant of two common quirks:
379
+
380
+ 1. A metadata header block above the actual table is skipped automatically.
381
+ 2. Sample names containing **unquoted commas** (e.g. `beads,3mm,WS, Slow`) are
382
+ detected (row has more fields than the header) and re-glued into the
383
+ `Sample` column.