Commit 2599d25
Add cluster metrics viz (#11372)
* New modules: cluster_metrics + cluster_viz
- cluster_metrics: computes clustering quality metrics + k-sweep
- cluster_viz: generates PCA, UMAP and t-SNE plots colored by cluster
- Both use conda environment.yml
- Full nf-test coverage
* Move custom clustering modules under custom
* Move custom clustering modules under custom
* Fix custom clustering module lint
* Fix custom clustering module lint and snapshots
* Address review comments for clustering custom modules
* Fix custom clustering module metadata
* Add Dockerfile for custom/clustermetrics and custom/clustervisualiation
* Add container directive for custom/clustermetrics and clustervisualiation
* Update modules/nf-core/custom/clustermetrics/main.nf
Co-authored-by: Jonathan Manning <pininforthefjords@gmail.com>
* Update modules/nf-core/custom/clustermetrics/main.nf
Co-authored-by: Jonathan Manning <pininforthefjords@gmail.com>
* Update modules/nf-core/custom/clustermetrics/main.nf
Co-authored-by: Jonathan Manning <pininforthefjords@gmail.com>
* Update modules/nf-core/custom/clustermetrics/main.nf
Co-authored-by: Jonathan Manning <pininforthefjords@gmail.com>
* Update modules/nf-core/custom/clustervisualiation/templates/cluster_viz.py
Co-authored-by: Jonathan Manning <pininforthefjords@gmail.com>
* Update modules/nf-core/custom/clustervisualiation/templates/cluster_viz.py
Co-authored-by: Jonathan Manning <pininforthefjords@gmail.com>
* Update modules/nf-core/custom/clustervisualiation/templates/cluster_viz.py
Co-authored-by: Jonathan Manning <pininforthefjords@gmail.com>
* fix: use template for cluster visualization module
* style: clean cluster visualization module main
* fix: address reviewer feedback for cluster modules
* fix: address pinin4fjords follow-up review - template escaping, drop PCA orphans, fix versions.yml, rename clustervisualiation -> clustervisualization
* feat(custom/clustervisualization): add UMAP and t-SNE cluster visualization module
* fix: apply ruff formatting to cluster_viz.py template
* fix: align clustermetrics and clustervisualization envs and containers
* fix: use docker:// prefix for singularity container to enable OCI conversion
* fix(custom/clustervisualization): set NUMBA_CACHE_DIR and MPLCONFIGDIR to fix numba caching in Singularity
* fix(custom/clustervisualization): move NUMBA_CACHE_DIR fix before any imports, and KMean n_init stable
* Apply suggestion from @pinin4fjords
Co-authored-by: Jonathan Manning <pininforthefjords@gmail.com>
* Apply suggestion from @pinin4fjords
Co-authored-by: Jonathan Manning <pininforthefjords@gmail.com>
* Prek and script fix
* Fixed pandas series problem in cluster_metrics.py
* fix: escape \n in f-strings for Groovy template compatibility
* Format CUSTOM_CLUSTERMETRICS template with ruff
* fix(clustermetrics,clustervisualization): update nf-test snapshots and test assertions
* fix environment conflict
* feat: add custom clustering and metrics modules
* Clean up cluster modules and fix CI
Templates:
- clustering.py: replace inline `${n_clusters}` / `${dbscan_eps}` /
`${dbscan_min_samples}` (which fail ruff and don't parse as Python source)
with locals assigned at the top of main(); replace yaml.dump with the
format_yaml_like helper used by the other two templates so pyyaml is no
longer needed in the env.
- cluster_metrics.py and cluster_viz.py: drop the PLINK-aware
_normalise_id_column / multi-mode load_clusters glue; require sample_id
+ cluster on the documented inputs. Drop the silent try/except plot
warning. Drop the cluster_mode / alignment_mode branching from main()
along with the redundant input_clusters / input_features / n_samples_used
/ alignment_mode metadata. Drop the argparse + sys.argv wrapper pattern
in cluster_metrics.py in favour of the direct template-substitution main()
that cluster_viz.py already uses.
Environments:
- Strip the misleading `# clustermetrics/environment.yml` headers; prune
per-module deps so the env declares what the script actually imports
(clustering: numpy/pandas/python/scikit-learn; clustermetrics: + matplotlib;
clustervisualization: + matplotlib/seaborn/umap-learn). Pin to the versions
Wave resolved unpinned: numpy 2.4.4, pandas 3.0.3, python 3.12.13,
scikit-learn 1.8.0, matplotlib 3.10.9, seaborn 0.13.2, umap-learn 0.5.12.
Containers:
- Rebuild Wave containers per module and replace the shared `_pruned:0...`
URL (which didn't actually contain the packages the env declared) with
the matching freshly-built Docker tag. Switch the singularity branch from
`docker://...` to the proper https blob URL per nf-core convention.
Tests:
- Drop the orphan `test_pca.eigenvec` test data file from
clustervisualization now that the PCA piece is no longer in the script.
- Regenerate snapshots (run on AWS x86 Linux via docker profile).
- Pre-commit autofix (prettier, end-of-file-fixer, ruff-format).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Use yaml.dump for versions.yml in cluster modules
Adds pyyaml to all three environment.yml files and replaces the embedded
format_yaml_like helper in clustering.py / cluster_metrics.py / cluster_viz.py
with a one-line `yaml.dump(versions, fh, default_flow_style=False, sort_keys=False)`.
format_yaml_like dates from before Seqera Containers / Wave made it cheap
to add deps. Now that we control the env precisely, pyyaml is a clean
swap and the templates are ~10 lines shorter each.
Rebuilt Wave Docker + Singularity containers per module with pyyaml=6.0.3
pinned (matching what Wave resolved unpinned) and updated the container URLs.
Regenerated snapshots on the AWS x86 Linux VM with --profile docker.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix portability of CUSTOM_CLUSTERING and CUSTOM_CLUSTERVISUALIZATION CI
Two CPU/container-environment portability issues surfaced on CI that didn't
appear on the AWS x86 VM I used to regenerate snapshots:
1. CUSTOM_CLUSTERING `*_clustering_info.json` md5 differed between CI and
the snapshot. KMeans inertia is a sum of squared distances, and BLAS
reduction order varies by a few ULPs across CPU instruction sets even
with a fixed `random_state`. Keep full precision in the production
output and round the value in the test snapshot only, matching the
`Math.round(... / 1000) * 1000` pattern used in `custom/basicpy`.
2. CUSTOM_CLUSTERVISUALIZATION stub `versions.yml` had `umap-learn: null`
under singularity. The stub heredoc ran `python3 -c "import umap"`,
which triggers numba's JIT cache write to a path that is read-only
inside the singularity image, so the import silently failed and the
bash substitution expanded to an empty string. Switched the stub
version checks to `importlib.metadata.version(<pkg>)` for every package
so they read dist-info without importing the module. Same answer,
no import side effects.
Regenerated the clustering snapshot on the VM.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Tidy cluster module templates
Reuse commodity functions and reduce hand-rolled glue:
- clustering.py: replace the 45-line manual eigenvec line parser with
`pd.read_csv(sep=r"\\s+")` + a small header check. Same accepted
layouts (FID/IID and IID-only, with the leading '#' on the header
line); drops the no-header inference path PLINK2 doesn't actually
emit.
- clustering.py: `len(set(labels) - {-1})` instead of the
`len(set(...)) - (1 if -1 in labels else 0)` puzzle; drop redundant
`int()` wraps around `len()`; replace `Path(...).write_text(json.dumps())`
with `json.dump` + `open()` to match the versions.yml write style.
- cluster_metrics.py / cluster_viz.py: replace the positional-index
alignment dance with `load_features().join(load_clusters(), how="inner")`.
Both helpers now return DataFrame / Series indexed by sample_id, so
pandas does the join. Drops ~10 lines of indexing logic per module
and removes a subtle reliance on `common.index` preserving positional
identity.
- cluster_metrics.py: tighten `cluster_quality()` to one expression and
drop the early-return + shared dict pattern. Returns just the three
scores; `n_clusters` is set at the call site, so the k-sweep no longer
needs to filter the result down before merging.
- cluster_metrics.py / cluster_viz.py: same json.dump cleanup.
Regenerated snapshots on the AWS x86 VM; all six tests pass under
`--profile docker`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Revert .gitignore drift unrelated to the PR
The .gitignore picked up entries for local snpclustering subworkflow
scaffolding and a stray `modules/nf-core/clustering/` ignore. None of
those should land in nf-core/modules master; restoring the file to
match origin/master.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address findings from fresh review pass
- clustering tests: add a DBSCAN scenario alongside the existing kmeans
one. The DBSCAN branch of clustering.py was previously untested.
- clustermetrics + clustervisualization meta.yml: tighten the `features`
and `clusters` input descriptions and patterns. Both modules require a
TSV with a `sample_id` column and numeric features, and a CSV with
`sample_id` + integer `cluster` columns; the old `"Feature matrix
file"` / `pattern: "*"` did not advertise the schema or noise-label
convention.
- All three modules' stub `versions.yml` blocks: switch every package
lookup to `importlib.metadata.version(...)`. clustervisualization
already did this for umap-learn to dodge numba's read-only cache
issue under singularity; using it everywhere is more robust against
similar import-side-effect surprises and keeps the three stubs
consistent.
All 7 nf-tests pass under `--profile docker` on the AWS x86 VM.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Align modules with nf-core spec: ext.args + dot-separated outputs
nf-core spec (modules/general.md): "All non-mandatory command-line tool
non-file arguments MUST be provided as a string via the $task.ext.args
variable." Adds argparse-on-task.ext.args plumbing in two template
scripts so the previously-hardcoded tuning parameters can be overridden
via modules.config:
- cluster_metrics.py: --k-min (default 2) and --k-max (default 12) for
the KMeans sweep range.
- cluster_viz.py: --umap-neighbors (default 15) and --tsne-perplexity
(default 30) for the embedding parameters. Both are still clamped
against sample count so tiny inputs work.
The `task.ext.args` substitution is parsed with shlex+argparse from
inside the template (matrixfilter does the same with R's parse_args),
which keeps required inputs as `val` channels (per
input-output-options.md) and optional ones via `ext.args` (per
general.md). `clustering` has no optional non-file args; its required
algorithm/n_clusters/dbscan_eps/dbscan_min_samples remain `val` channel
inputs.
Output filename style also unified to `${prefix}.<descriptor>.<ext>`
(dot-separated) across all three modules, matching the spec example
(`${prefix}.fq.gz`) and the existing dot-separated pattern in
`clustervisualization` / `custom/tx2gene` / `custom/matrixfilter`:
- clustering: `*_clusters.csv` -> `*.clusters.csv`,
`*_clustering_info.json` -> `*.clustering_info.json`.
- clustermetrics: `*_metrics.tsv` -> `*.metrics.tsv`,
`*_k_sweep.csv` -> `*.k_sweep.csv`,
`*_selected.json` -> `*.selected.json`, and plot files from
`*_<name>.png` -> `*.<name>.png`. Also renamed
`_calinski.png` -> `.calinski_harabasz.png` to match the other metric
files (was the only truncated one).
Updated meta.yml output patterns and stub touches to match. Regenerated
snapshots on the AWS x86 VM with `--profile docker`; all 7 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: switch custom/clustering* modules to nf-core/test-datasets
- remove local tests/data/
- update main.nf.test to use params.modules_testdata_base_path
- update snapshots
* update snapshot v2
* Update test and snapshots
* restore test and snapshot
* fix(custom/clustervisualization): regenerate snapshot on Linux x86
UMAP and t-SNE outputs are not bit-identical across CPU
architectures despite the fixed random_state, so snapshots
must be generated on the same platform CI uses.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(custom/clustering): rename to custom/pcaclustering
The module is specifically a clusterer for PLINK2 .eigenvec
output (PCA components). The previous name implied generality
that the module doesn't have.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add copyright and license headers
Add copyright and license information to clustering.py
* Add copyright and license information
Added copyright and licensing information to the file.
* Add copyright and license comments to cluster_viz.py
Added copyright and licensing information to the top of the file.
* Rename pcaclustering in plink2pcaclustering
* Fix tag for plink2pcaclustering in test file
* snapshot update plink2pcaclustering
* refactor(custom/pcaclustering): accept generic sample_id features TSV
Replace the PLINK2 .eigenvec parser with a plain pandas read of a TSV
that has a `sample_id` column plus numeric feature columns - the same
input contract `custom/clustermetrics` already uses. PLINK2 eigenvec
output is one supported source (drop FID, rename IID -> sample_id), not
the only one.
Also removes the duplicate `plink2pcaclustering/` directory left behind
by an in-flight rename.
clusters.csv / clustering_info.json md5s are byte-identical to those
recorded in the existing snapshot; the .snap file is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(custom/pcaclustering): point test at existing `test_features.tsv` fixture
The earlier generic-input refactor (PR #11372 / dbaku42#1) pointed at
`popgen/clustering/test.tsv`, but the actual fixture in
nf-core/test-datasets (added in nf-core/test-datasets#2051) is named
`test_features.tsv`. CI was failing with "No such file or directory" on
all three pcaclustering tests as a result.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(custom/pcaclustering): drop hardcoded `sample_id` column name
Use the first column of the features TSV as sample IDs regardless of its
header name, and treat all remaining columns as numeric features. The
required column name was a hidden assumption about input shape - this
relaxes it without adding inputs, so callers can feed in e.g. a
reformatted PLINK eigenvec (with `IID` or any other ID header) directly,
as long as the ID column is first.
Output schema (clusters.csv emits `sample_id,cluster`) is unchanged, so
downstream consumers (clustermetrics, clustervisualization) see the same
contract. Verified clusters.csv / clustering_info.json md5s remain
byte-identical to the snapshot against the existing test fixture.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(custom/pcaclustering): preserve zero-padded numeric sample IDs
Pin the first column to `dtype=str` at read time so pandas doesn't
type-infer it as int and strip leading zeros (e.g. "0001" -> 1).
Same output for the existing fixture (which has string IDs already);
snapshot md5s unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Jonathan Manning <pininforthefjords@gmail.com>
Co-authored-by: Jonathan Manning <jonathan.manning@seqera.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 2dc2369 commit 2599d25
18 files changed
Lines changed: 1321 additions & 0 deletions
File tree
- modules/nf-core/custom
- clustermetrics
- templates
- tests
- clustervisualization
- templates
- tests
- pcaclustering
- templates
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
Lines changed: 143 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
0 commit comments