Usage
nextflow run . -profile sherlock -params-file config/config.yml
That is the whole pipeline. Every step is also a console script taking explicit arguments and
printing --help, so any one of them can be run, re-run or debugged on its own; nothing reads a
global config object.
Getting a dataset in
The pipeline’s input is a .h5mu, not a sceptre object. Producing one is a one-off step per
dataset, and it is the only place R appears:
Rscript pysceptre/scripts/export_sceptre_dataset.R \
--sceptre-object results/sample1/sceptre_object.rds \
--out-dir export/ --all-genes --all-cells
python pysceptre/scripts/make_h5mu.py export/
The object must have been through assign_grnas() and run_qc() with
grna_integration_strategy = "union".
Both flags are required, and for different reasons.
--all-genes because the poscounts size factors are a per-cell reduction over the whole gene set.
--all-cells because they are also computed against a per-gene geometric mean taken over every
cell in the object, including the ones QC removed. An export restricted to the QC-passing cells
gives different size factors for the cells that remain, and different normalised means for every
gene — measured on day0, a 0.46 % shift in size factor and 2.2 % in the raw gene mean. Nothing
downstream would report it. watteg-prepare-sim-input warns if it is handed an export without
them.
The export handles odm-backed (out-of-core) matrices itself, which is why nothing in the pipeline
takes an --response-odm flag any more.
The five steps
dataset.h5mu
|
| watteg-prepare-sim-input once per sample
+--> sim_input.h5 per-gene and per-cell statistics, the model
| coefficients, and the perturbation matrices
+--> pairs.tsv QC-passing discovery pairs
+--> pairs_with_info.tsv every pair with its real-data QC counts
+--> grna_targets.tsv gRNA -> target mapping (many-to-many)
+--> discovery_threshold.txt the p-value a simulation must beat
+--> analysis_mode.tsv which test the screen ran, and its resampling budget
|
| watteg-split-pairs once per sample
+--> split_001.tsv ... balanced chunks of targets
|
| watteg-fit-null-models once per sample, under --null-fits reuse
+--> null_fits.h5 each gene's null-model fit, per simulation
|
| watteg-run-power-simulation once per (split, effect size, simulation chunk)
+--> *.partial.tsv.gz per-pair counts (--partials-out), by default
+--> *.tsv.gz one row per (pair, simulation) (--out), optional
|
| watteg-consolidate-replicates once per effect size, only if the rows were written
+--> replicates_es*.parquet the same rows, as one file
|
| watteg-compute-power once per effect size, from the counts or the rows
+--> power_es*.tsv one row per pair, with confidence intervals
|
| watteg-summarize-power once per sample
+--> power_summary.tsv one row per pair, one column per effect size
There is no sceptre_template.rds. It was an R object carrying the covariate matrix and the
analysis parameters, which now live in sim_input.h5 and analysis_mode.tsv.
watteg-fit-null-models is the Python version of R’s FIT_NULL_MODELS: each gene’s null model is
fitted once per simulation, on an independent draw with no knockdown, and every target the gene is
tested with reuses that fit (--null-fits reuse). Without it (--null-fits refit) each simulation
refits every gene once per target, the exact configuration, at about 1.7× the cost of a simulated
pair-test. See Methods.
1. watteg-prepare-sim-input
watteg-prepare-sim-input --dataset export/dataset.h5mu --outdir prepared/
| Option | Default | Meaning |
|---|---|---|
--dataset |
required | An --all-genes --all-cells export. |
--outdir |
required | Directory for all six outputs. |
--threshold |
derived | Set the significance threshold instead of deriving it from the export’s discovery result. |
--n-jobs |
1 | Workers for the per-gene fits. |
Fits each gene’s Poisson GLM and negative-binomial theta — the same model sceptre’s own
perform_response_precomputation fits, reproduced to about ten significant digits — and stores the
coefficients. A gene whose theta hits the estimator’s bounds is refused rather than simulated
from: a clamped theta is not an estimate, and drawing counts from it would state a noise level the
data never supported. (sceptre clamps and carries on, which is reasonable for an analysis and not
for a simulation.)
2. watteg-split-pairs
watteg-split-pairs --pairs prepared/pairs.tsv --n-splits 280 --outdir splits/
| Option | Default | Meaning |
|---|---|---|
--n-splits |
required | Number of chunks. Must not exceed the number of targets. |
--target-overhead |
2.0 | Pairs-equivalent fixed cost of a target, from the measured cost model. |
--prefix |
split_ |
Filename prefix; files are zero-padded so lexicographic order is numeric order. |
A target’s pairs cannot be separated — the simulation draws one count matrix per (target,
simulation) and tests every one of that target’s genes against it — so targets are the unit and the
job is bin packing. Weighted by pairs plus an overhead, because a task’s cost is
intercept + slope × pairs and weighting by pairs alone over-fills the splits that hold many small
targets.
3. watteg-run-power-simulation
watteg-run-power-simulation \
--prepared prepared/ --pairs splits/split_001.tsv \
--effect-size 0.15 --reps 100 --seed 20250812 \
--null-fits-file prepared/null_fits.h5 \
--out sim/split_001_es0.15.tsv
| Option | Default | Meaning |
|---|---|---|
--effect-size |
required | A fractional decrease: 0.15 is a 15 % knockdown. |
--reps |
required | Simulations in this chunk. |
--rep-offset |
0 | Simulations already covered by earlier chunks, so rep stays unique. |
--seed |
required | Results are stochastic and must be reproducible. |
--estimand |
fixed |
What the power is for: an element whose effect is es (fixed, the guides’ mean pinned), or is es on average (random). See Methods. |
--guide-spread-c |
0.65 | Guide-to-guide spread: each guide’s knockdown is Beta with mean es and sd c·es·(1−es), so zero at es = 0. Replaces --guide-sd, which is refused. See Methods. |
--permutations |
per-target |
One permutation set per target, drawn from the seed, shared by its simulations. per-replicate draws one per simulation (engine only). |
--nulls |
sparse |
How the engine computes the permutation nulls; scan is pysceptre’s default. Identical results. |
--driver |
fast |
fast tests a target’s simulations together (byte-identical to engine under per-target / sparse); engine calls pysceptre once per (target, simulation). |
--null-fits |
reuse |
reuse: each gene’s null model fitted once per simulation and shared across targets (needs --driver fast). refit: per target, exactly. |
--null-fits-file |
none | watteg-fit-null-models’ output. Without it, a reuse task fits its own genes first; same output. |
--n-jobs |
8 | Worker processes for the task. |
--expression-model |
fitted |
Where a gene’s unperturbed expected counts come from. |
--out |
none | One row per (pair, simulation), as TSV. |
--partials-out, --threshold-file |
none | Per-pair counts for watteg-compute-power --partials, taken at the discovery threshold. Complete when the task holds all simulations of its pairs. At least one of --out and --partials-out is required. |
The defaults are the fast configuration, each part measured before it was adopted
(Methods). The previous configuration is
--driver engine --null-fits refit, plus --permutations per-replicate --nulls scan for the exact
earlier behaviour. A CRT screen needs --driver engine --null-fits refit: the fast driver runs
the permutation test only, and says so rather than testing a CRT screen by permutations.
--effect-size 0 is allowed on purpose. It simulates no effect: perturbed cells are drawn from
the unperturbed mean and the screen’s own test is run on them, so the p-values should be uniform.
It is the only way to measure this pipeline’s type-I error from its own output. Run it as its own
sweep, not as another point on a curve. The two estimands are the same simulation there.
--expression-model. fitted draws from exp(X·β), the expected count sceptre’s own null
model gives that cell, so the simulation and the test that judges it are on one scale.
size_factor reproduces the pre-2026-09-21 behaviour — a size-factor-normalised gene mean scaled
by the cell’s poscounts factor — and exists only to compare against sweeps already run with it. It
mixes two models, runs about 4 % low, and reproduces 86.5 % of the observed count variance against
the fitted model’s 99.5 %.
3b. watteg-fit-null-models
watteg-fit-null-models --prepared prepared/ --reps 100 --seed 20250812 --n-jobs 8 \
--out prepared/null_fits.h5
| Option | Default | Meaning |
|---|---|---|
--reps, --rep-offset |
required, 0 | Fits simulations offset+1 .. offset+reps. |
--seed |
required | The simulation run’s seed; recorded, and checked by the simulation. |
--expression-model |
fitted |
The simulation run’s baseline; recorded and checked. |
--pairs |
pairs.tsv in --prepared |
Whose genes to fit. |
--n-jobs |
8 | Workers; one simulation’s fits are one unit of work. |
Pass the file to watteg-run-power-simulation --null-fits reuse --null-fits-file. A task given no
file fits its own genes first with the same keyed draws, so the output is the same bytes either way;
the file only saves refitting a gene in every task that tests it. Refused for a CRT screen, whose
simulations the fast driver (the file’s only reader) does not run.
4. watteg-compute-power and watteg-summarize-power
watteg-compute-power \
--simulations sim/ --threshold-file prepared/discovery_threshold.txt \
--out power_es0.15.tsv
watteg-summarize-power \
--power power_es0.05.tsv power_es0.15.tsv \
--sim-input prepared/sim_input.h5 --out power_summary.tsv
watteg-compute-power takes either --partials (the simulation tasks’ per-pair counts) or
--simulations (one row per pair and simulation), and gives the same table from both when each
pair’s simulations came from one task. Both take files or directories; a directory expands to the
.tsv, .tsv.gz and .parquet files inside it, sorted. In the pipeline the rows are written only
under keep_per_simulation: true, or when reps_per_chunk < num_replicates. --power-threshold (default 0.8) sets what “detectable” means
for the minimum-detectable-effect-size columns.
Reproducibility
A pair’s power does not depend on how the work was split up. Every draw is keyed on
(seed, target, simulation, effect size) and nothing else, so running simulations 1–100 in one
task and in five tasks of twenty gives byte-identical results, and so does moving a target from
one split to another. A reused null-model fit is keyed on (seed, gene, simulation), so it is the
same wherever it was made. That is checked end to end:
WATTEG_PREPARED=prepared/ pytest -m realdata
The streams differ from the R implementation’s and always would — different generators — so the two agree statistically rather than draw for draw. See Plan - pysceptre backend for what has been measured.
Sizing a cluster run
The unit of work is (split × effect size × simulation chunk), and essentially all of the compute is
in step 3. A task’s cost follows intercept + slope × pairs, so:
- halving the pairs saves less than half, because the intercept does not move;
- halving the simulations saves exactly half;
- effect sizes cost the same as each other.
--n-jobs buys wall clock rather than CPU time: measured on a 10-performance-core machine, 8
workers gave 2.5× the wall-clock speed for 1.5× the CPU, and 14 was slower than 8.
A task’s wall time is bounded by its largest target, whose simulations are one unit of work. On one
machine, several narrower tasks at once therefore beat one wide one: on a 14-core laptop, three
330-pair cis tasks at 4 workers each finished in 727 s, against 296 s each at 8 workers one after
another. For a local sweep, -profile local --power_simulation_cpus 4 with Nextflow’s
executor.cpus = 12 runs three at once; each took ~4.2 GB on macOS.