SigProfilerExtractor 1.4.1__tar.gz → 1.5.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.
Files changed (111) hide show
  1. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/CHANGELOG.md +26 -1
  2. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/PKG-INFO +5 -5
  3. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/README.md +4 -4
  4. sigprofilerextractor-1.5.0/SigProfilerExtractor/_version.py +1 -0
  5. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/controllers/cli_controller.py +16 -5
  6. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/nmf_cpu.py +20 -6
  7. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/nmf_gpu.py +110 -15
  8. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/sigpro.py +151 -33
  9. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/subroutines.py +248 -218
  10. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor.egg-info/PKG-INFO +5 -5
  11. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor.egg-info/SOURCES.txt +16 -1
  12. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor.egg-info/scm_file_list.json +70 -55
  13. sigprofilerextractor-1.5.0/SigProfilerExtractor.egg-info/scm_version.json +8 -0
  14. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/3. Using the Tool - Input.md +129 -129
  15. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/4. Using the Tool - Output.md +6 -3
  16. sigprofilerextractor-1.5.0/tests/test_activity_export.py +18 -0
  17. sigprofilerextractor-1.5.0/tests/test_assignment_forwarding.py +44 -0
  18. sigprofilerextractor-1.5.0/tests/test_context_labels.py +22 -0
  19. sigprofilerextractor-1.5.0/tests/test_default_parity.py +23 -0
  20. sigprofilerextractor-1.5.0/tests/test_import_side_effects.py +45 -0
  21. sigprofilerextractor-1.5.0/tests/test_input_samples.py +37 -0
  22. sigprofilerextractor-1.5.0/tests/test_matrix_normalization.py +30 -0
  23. sigprofilerextractor-1.5.0/tests/test_nmf_floor.py +69 -0
  24. sigprofilerextractor-1.5.0/tests/test_normalization_cutoff.py +27 -0
  25. sigprofilerextractor-1.5.0/tests/test_output_directory.py +34 -0
  26. sigprofilerextractor-1.5.0/tests/test_precision.py +22 -0
  27. sigprofilerextractor-1.5.0/tests/test_replicate_batches.py +25 -0
  28. sigprofilerextractor-1.5.0/tests/test_replicate_diagnostics.py +30 -0
  29. sigprofilerextractor-1.5.0/tests/test_seed_file.py +32 -0
  30. sigprofilerextractor-1.5.0/tests/test_silhouette_stability.py +44 -0
  31. sigprofilerextractor-1.5.0/tests/test_sv_output_path.py +16 -0
  32. sigprofilerextractor-1.4.1/SigProfilerExtractor/_version.py +0 -1
  33. sigprofilerextractor-1.4.1/SigProfilerExtractor.egg-info/scm_version.json +0 -8
  34. sigprofilerextractor-1.4.1/docs/assets/images/osf/h3tvx.png +0 -0
  35. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/.github/workflows/ci.yml +0 -0
  36. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/.github/workflows/create-version-tag.yml +0 -0
  37. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/.github/workflows/docs.yml +0 -0
  38. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/.github/workflows/release.yml +0 -0
  39. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/.github/workflows/static.yml +0 -0
  40. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/.gitignore +0 -0
  41. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/Dockerfile +0 -0
  42. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/LICENSE.txt +0 -0
  43. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/MANIFEST.in +0 -0
  44. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/__init__.py +0 -0
  45. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/CNVInput/Battenberg_test.tsv +0 -0
  46. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/CSVInput/csv_example.csv +0 -0
  47. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/MatObjInput/21_breast_WGS_substitutions.mat +0 -0
  48. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/ReferenceFiles/CNV_features.tsv +0 -0
  49. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/ReferenceFiles/CN_classes_dictionary.txt +0 -0
  50. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/TextInput/Samples_CNV.txt +0 -0
  51. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/TextInput/Samples_DBS.txt +0 -0
  52. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/TextInput/Samples_ID.txt +0 -0
  53. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/TextInput/Samples_SBS.txt +0 -0
  54. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/TextInput/Samples_SV.txt +0 -0
  55. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/VCFInput/PD3851a.vcf +0 -0
  56. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/VCFInput/PD3890a.vcf +0 -0
  57. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/VCFInput/PD3904a.vcf +0 -0
  58. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/VCFInput/PD3905a.vcf +0 -0
  59. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/VCFInput/PD3945a.vcf +0 -0
  60. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/estimate_best_solution.py +0 -0
  61. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/nndsvd.py +0 -0
  62. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/sigprofilerextractor_cli.py +0 -0
  63. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/version.py +0 -0
  64. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor.egg-info/dependency_links.txt +0 -0
  65. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor.egg-info/entry_points.txt +0 -0
  66. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor.egg-info/not-zip-safe +0 -0
  67. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor.egg-info/requires.txt +0 -0
  68. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/SigProfilerExtractor.egg-info/top_level.txt +0 -0
  69. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/1. Installation.md +0 -0
  70. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/2. Quick Start Example.md +0 -0
  71. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/5. Output - All Solutions.md +0 -0
  72. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/6. Output - Suggested Solution.md +0 -0
  73. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/4hnaj.png +0 -0
  74. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/5f8ep.png +0 -0
  75. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/5tr9y.png +0 -0
  76. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/5u7v8.png +0 -0
  77. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/9uf4s.png +0 -0
  78. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/a7gkf.png +0 -0
  79. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/akef5.png +0 -0
  80. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/c5hzf.png +0 -0
  81. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/dahws.png +0 -0
  82. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/h796w.png +0 -0
  83. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/kb4w2.png +0 -0
  84. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/ku93g.png +0 -0
  85. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/p4cfj.png +0 -0
  86. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/r9wcm.png +0 -0
  87. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/rz3ue.png +0 -0
  88. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/whn8t.png +0 -0
  89. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/xcm95.png +0 -0
  90. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/xqzfh.png +0 -0
  91. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/yhqnw.png +0 -0
  92. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/z98fk.png +0 -0
  93. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/zu6sm.png +0 -0
  94. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/assets/stylesheets/extra.css +0 -0
  95. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/home.md +0 -0
  96. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/docs/index.md +0 -0
  97. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/install_genome.py +0 -0
  98. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/mkdocs.yml +0 -0
  99. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/mkdocs_hooks.py +0 -0
  100. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/pyproject.toml +0 -0
  101. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/setup.cfg +0 -0
  102. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/setup.py +0 -0
  103. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/test.py +0 -0
  104. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/tests/test_cosmic_version.py +0 -0
  105. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/wikies/CLI-Usage.md +0 -0
  106. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/wikies/Citation.md +0 -0
  107. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/wikies/Home.md +0 -0
  108. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/wikies/Installation.md +0 -0
  109. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/wikies/Output.md +0 -0
  110. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/wikies/Python-API.md +0 -0
  111. {sigprofilerextractor-1.4.1 → sigprofilerextractor-1.5.0}/wikies/_Sidebar.md +0 -0
@@ -5,11 +5,36 @@ All notable changes to this project will be documented in this file.
5
5
 
6
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [1.5.0] - 2026-09-28
9
9
 
10
10
  ### Changed
11
11
  - Updated the default COSMIC reference signature version from 3.5 to 3.6.
12
12
  - Updated the minimum SigProfilerAssignment version to 1.1.5 for COSMIC v3.6 support.
13
+ - Changed the CLI default for `--maximum_signatures` from 10 to 25 so it matches the Python API.
14
+ - GPU batches now give every NMF replicate its own resampling and initialization generators, sample totals, and convergence stopping point, matching CPU and single-replicate GPU behavior.
15
+ - A seeds file now contains exactly one root seed. All random generators used by a run are derived reproducibly from that seed.
16
+ - Existing results for a mutation context are moved to `<context>_previous_<timestamp>` before a rerun writes new results, preventing files from different runs from being mixed.
17
+ - Importing SigProfilerExtractor no longer suppresses warnings globally or changes the host program's multiprocessing start method.
18
+
19
+ ### Fixed
20
+ - Added a lower bound (1e-16) to W, H and W @ H in the KL multiplicative updates, on CPU and GPU. A multiplicative update cannot move an entry away from exactly zero, so zeros from the NNDSVD initialization (about half of W and H) or from underflow stayed zero for the whole run. The fit could then stall and pass the convergence test early at a worse solution; with `nmf_init="nndsvd"` this could happen at the first allowed convergence check. Results and run times can change, including with the default random initialization.
21
+ - Fixed the KL loss returning NaN when the matrix passed to NMF contains a zero (`0 * log 0`). The extraction pipeline raises every value to at least 1e-4 before NMF, so this affected only direct use of `nmf_cpu` and `nmf_gpu`.
22
+ - If a cell of W @ H reached zero, the update produced inf or NaN that spread to all of W and H with no error. An NMF replicate whose W or H contains NaN or inf now raises `FloatingPointError` instead of entering clustering.
23
+ - CPU extraction now runs the requested number of NMF replicates when `batch_size` is greater than 1. The setting only controls GPU batching.
24
+ - Forwarded `nnls_add_penalty`, `nnls_remove_penalty`, `initial_remove_penalty`, and `collapse_to_SBS96` to SigProfilerAssignment.
25
+ - Recomputed the signature-rank range for each mutation context so a context with fewer samples cannot narrow the range used by subsequent contexts.
26
+ - Seeded the Gaussian mixture model used to choose the normalization cutoff, making it reproducible from `Seeds.txt`.
27
+ - Stopped treating failed silhouette calculations as perfect stability. Rank one remains stable by convention; invalid higher-rank calculations now raise a clear error.
28
+ - Removed two unused per-replicate arrays that could each consume several gigabytes for large mutation matrices.
29
+ - Validated `matrix_normalization`. Accepted values are `"gmm"`, `"100X"`, `"log2"`, `"none"`, or a positive integer cutoff; unsupported values now raise `ValueError`.
30
+ - Samples containing some missing values now raise a clear error instead of being dropped silently. Completely empty columns and samples with zero mutations are reported and removed.
31
+ - Preserved input values as float64 throughout NMF when `precision="double"` is selected.
32
+ - Labelled matrices with unrecognized channel counts as `CH<n>` instead of incorrectly labelling every such matrix as SBS.
33
+ - Fixed loading `Seeds.txt` with current NumPy versions and added clear validation for a missing `Seed` column or more than one root seed.
34
+ - A run now raises an error instead of reporting success when none of the requested mutation contexts can be analyzed.
35
+ - Rounded exported NMF activities to the nearest integer instead of truncating them.
36
+ - Made the `SV_Matrices` output location independent of whether a BEDPE input directory has a trailing slash.
37
+ - Unified CPU and GPU per-replicate diagnostics so both paths calculate them from the matrix actually fitted by NMF.
13
38
 
14
39
  ## [1.2.7] - 2026-01-22
15
40
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SigProfilerExtractor
3
- Version: 1.4.1
3
+ Version: 1.5.0
4
4
  Summary: Extracts mutational signatures from mutational catalogues
5
5
  Home-page: https://github.com/SigProfilerSuite/SigProfilerExtractor.git
6
6
  Author: S Mishu Ashiqul Islam
@@ -121,7 +121,7 @@ Extracts mutational signatures from an array of samples.
121
121
 
122
122
  ```python
123
123
  sigProfilerExtractor(input_type, out_put, input_data, reference_genome="GRCh37", opportunity_genome = "GRCh37", cosmic_version=3.6, context_type = "default", exome = False,
124
- minimum_signatures=1, maximum_signatures=10, nmf_replicates=100, resample = True, batch_size=1, cpu=-1, gpu=False,
124
+ minimum_signatures=1, maximum_signatures=25, nmf_replicates=100, resample = True, batch_size=1, cpu=-1, gpu=False,
125
125
  nmf_init="random", precision= "single", matrix_normalization= "gmm", seeds= "random",
126
126
  min_nmf_iterations= 10000, max_nmf_iterations=1000000, nmf_test_conv= 10000, nmf_tolerance= 1e-15, get_all_signature_matrices= False)
127
127
  ```
@@ -141,9 +141,9 @@ sigProfilerExtractor(input_type, out_put, input_data, reference_genome="GRCh37",
141
141
  | | **maximum_signatures** | Positive Integer | The maximum number of signatures to be extracted (default: `25`). |
142
142
  | | **nmf_replicates** | Positive Integer | The number of iteration to be performed to extract each number signature (default: `100`). |
143
143
  | | **resample** | Boolean | If `True`, add poisson noise to samples by resampling (default: `True`). |
144
- | | **seeds** | String | Ensures reproducible NMF replicate resamples. Provide the path to the `Seeds.txt` file (found in the results folder from a previous analysis) to reproduce results (default: `"random"`). |
144
+ | | **seeds** | String | Ensures reproducible NMF resampling and initialization. Provide the path to a `Seeds.txt` file containing exactly one root seed in the `Seed` column (the file generated by a previous analysis can be reused) to reproduce results (default: `"random"`). |
145
145
  | **NMF Engines** | | | |
146
- | | **matrix_normalization** | String | Method of normalizing the genome matrix before it is analyzed by NMF (default: `"gmm"`). Options are, `"log2"`, `"custom"` or `"none"`. |
146
+ | | **matrix_normalization** | String | Method of normalizing the genome matrix before it is analyzed by NMF (default: `"gmm"`). Options are `"100X"`, `"log2"`, `"none"`, or a positive integer used as a manual cutoff (for example `5000`). |
147
147
  | | **nmf_init** | String | The initialization algorithm for W and H matrix of NMF (default: `"random"`). Options are `"random"`, `"nndsvd"`, `"nndsvda"`, `"nndsvdar"` and `"nndsvd_min"`. |
148
148
  | | **precision** | String | Values should be single or double (default: `"single"`). |
149
149
  | | **min_nmf_iterations** | Integer | Value defines the minimum number of iterations to be completed before NMF converges (default: `10000`). |
@@ -154,7 +154,7 @@ sigProfilerExtractor(input_type, out_put, input_data, reference_genome="GRCh37",
154
154
  | | **cpu** | Integer | The number of processors to be used to extract the signatures (default: all processors). |
155
155
  | | **assignment_cpu** | Integer | Number of processors to be used by SigProfilerAssignment for the final signature assignment step (default: all available). This is independent of the `cpu` parameter. |
156
156
  | | **gpu** | Boolean | Defines if the GPU resource will used if available (default: `False`). If `True`, the GPU resources will be used in the computation. *Note: All available CPU processors are used by default, which may cause a memory error. This error can be resolved by reducing the number of CPU processes through the `cpu` parameter.*|
157
- | | **batch_size** | Integer | Will be effective only if the GPU is used. Defines the number of NMF replicates to be performed by each CPU during the parallel processing (default: `1`). *Note: For `batch_size` values greater than 1, each NMF replicate will update until `max_nmf_iterations` is reached.*|
157
+ | | **batch_size** | Integer | Effective only when the GPU is used. Defines how many NMF replicates are grouped into each GPU batch (default: `1`). Every replicate has its own resampling and initialization generators, sample totals, and convergence stopping point, whether it runs alone or in a batch. |
158
158
  | **Solution Estimation Thresholds** | | | |
159
159
  | | **stability** | Float | The cutoff thresh-hold of the average stability (default: `0.8`). Solutions with average stabilities below this thresh-hold will not be considered. |
160
160
  | | **min_stability** | Float | The cutoff thresh-hold of the minimum stability (default: `0.2`). Solutions with minimum stabilities below this thresh-hold will not be considered. |
@@ -89,7 +89,7 @@ Extracts mutational signatures from an array of samples.
89
89
 
90
90
  ```python
91
91
  sigProfilerExtractor(input_type, out_put, input_data, reference_genome="GRCh37", opportunity_genome = "GRCh37", cosmic_version=3.6, context_type = "default", exome = False,
92
- minimum_signatures=1, maximum_signatures=10, nmf_replicates=100, resample = True, batch_size=1, cpu=-1, gpu=False,
92
+ minimum_signatures=1, maximum_signatures=25, nmf_replicates=100, resample = True, batch_size=1, cpu=-1, gpu=False,
93
93
  nmf_init="random", precision= "single", matrix_normalization= "gmm", seeds= "random",
94
94
  min_nmf_iterations= 10000, max_nmf_iterations=1000000, nmf_test_conv= 10000, nmf_tolerance= 1e-15, get_all_signature_matrices= False)
95
95
  ```
@@ -109,9 +109,9 @@ sigProfilerExtractor(input_type, out_put, input_data, reference_genome="GRCh37",
109
109
  | | **maximum_signatures** | Positive Integer | The maximum number of signatures to be extracted (default: `25`). |
110
110
  | | **nmf_replicates** | Positive Integer | The number of iteration to be performed to extract each number signature (default: `100`). |
111
111
  | | **resample** | Boolean | If `True`, add poisson noise to samples by resampling (default: `True`). |
112
- | | **seeds** | String | Ensures reproducible NMF replicate resamples. Provide the path to the `Seeds.txt` file (found in the results folder from a previous analysis) to reproduce results (default: `"random"`). |
112
+ | | **seeds** | String | Ensures reproducible NMF resampling and initialization. Provide the path to a `Seeds.txt` file containing exactly one root seed in the `Seed` column (the file generated by a previous analysis can be reused) to reproduce results (default: `"random"`). |
113
113
  | **NMF Engines** | | | |
114
- | | **matrix_normalization** | String | Method of normalizing the genome matrix before it is analyzed by NMF (default: `"gmm"`). Options are, `"log2"`, `"custom"` or `"none"`. |
114
+ | | **matrix_normalization** | String | Method of normalizing the genome matrix before it is analyzed by NMF (default: `"gmm"`). Options are `"100X"`, `"log2"`, `"none"`, or a positive integer used as a manual cutoff (for example `5000`). |
115
115
  | | **nmf_init** | String | The initialization algorithm for W and H matrix of NMF (default: `"random"`). Options are `"random"`, `"nndsvd"`, `"nndsvda"`, `"nndsvdar"` and `"nndsvd_min"`. |
116
116
  | | **precision** | String | Values should be single or double (default: `"single"`). |
117
117
  | | **min_nmf_iterations** | Integer | Value defines the minimum number of iterations to be completed before NMF converges (default: `10000`). |
@@ -122,7 +122,7 @@ sigProfilerExtractor(input_type, out_put, input_data, reference_genome="GRCh37",
122
122
  | | **cpu** | Integer | The number of processors to be used to extract the signatures (default: all processors). |
123
123
  | | **assignment_cpu** | Integer | Number of processors to be used by SigProfilerAssignment for the final signature assignment step (default: all available). This is independent of the `cpu` parameter. |
124
124
  | | **gpu** | Boolean | Defines if the GPU resource will used if available (default: `False`). If `True`, the GPU resources will be used in the computation. *Note: All available CPU processors are used by default, which may cause a memory error. This error can be resolved by reducing the number of CPU processes through the `cpu` parameter.*|
125
- | | **batch_size** | Integer | Will be effective only if the GPU is used. Defines the number of NMF replicates to be performed by each CPU during the parallel processing (default: `1`). *Note: For `batch_size` values greater than 1, each NMF replicate will update until `max_nmf_iterations` is reached.*|
125
+ | | **batch_size** | Integer | Effective only when the GPU is used. Defines how many NMF replicates are grouped into each GPU batch (default: `1`). Every replicate has its own resampling and initialization generators, sample totals, and convergence stopping point, whether it runs alone or in a batch. |
126
126
  | **Solution Estimation Thresholds** | | | |
127
127
  | | **stability** | Float | The cutoff thresh-hold of the average stability (default: `0.8`). Solutions with average stabilities below this thresh-hold will not be considered. |
128
128
  | | **min_stability** | Float | The cutoff thresh-hold of the minimum stability (default: `0.2`). Solutions with minimum stabilities below this thresh-hold will not be considered. |
@@ -0,0 +1 @@
1
+ version = '1.5.0'
@@ -77,8 +77,11 @@ def parse_arguments_extractor(args: List[str], description: str) -> argparse.Nam
77
77
  parser.add_argument(
78
78
  "--maximum_signatures",
79
79
  type=int,
80
- default=10,
81
- help="Maximum number of signatures to be extracted (default: 10).",
80
+ default=sigpro.DEFAULT_MAXIMUM_SIGNATURES,
81
+ help=(
82
+ "Maximum number of signatures to be extracted "
83
+ f"(default: {sigpro.DEFAULT_MAXIMUM_SIGNATURES})."
84
+ ),
82
85
  )
83
86
  parser.add_argument(
84
87
  "--nmf_replicates",
@@ -97,13 +100,21 @@ def parse_arguments_extractor(args: List[str], description: str) -> argparse.Nam
97
100
  parser.add_argument(
98
101
  "--seeds",
99
102
  default="random",
100
- help="Seeds for reproducible resamples, file path or 'random' (default: 'random').",
103
+ help=(
104
+ "Root seed source for reproducible NMF resampling and initialization: "
105
+ "a path to a Seeds.txt file containing one root seed in the Seed "
106
+ "column, or 'random' (default: 'random')."
107
+ ),
101
108
  )
102
109
  parser.add_argument(
103
110
  "--batch_size",
104
111
  type=int,
105
112
  default=1,
106
- help="Batch size is for GPU only and defines the number of NMF replicates to be performed by each CPU during parallel processing (default: 1).",
113
+ help=(
114
+ "Number of NMF replicates grouped into each GPU batch. GPU only; "
115
+ "replicates keep independent resampling and initialization generators, "
116
+ "sample totals, and convergence stopping points (default: 1)."
117
+ ),
107
118
  )
108
119
  parser.add_argument(
109
120
  "--cpu",
@@ -138,7 +149,7 @@ def parse_arguments_extractor(args: List[str], description: str) -> argparse.Nam
138
149
  parser.add_argument(
139
150
  "--matrix_normalization",
140
151
  default="gmm",
141
- help="Method of normalizing the genome matrix before it is analyzed by NMF (default: 'gmm'). Options are 'custom', 'gmm', 'log2', or 'none'.",
152
+ help="Method of normalizing the genome matrix before it is analyzed by NMF (default: 'gmm'). Options are 'gmm', '100X', 'log2', 'none', or a positive integer used as a manual cutoff (for example 5000).",
142
153
  )
143
154
  parser.add_argument(
144
155
  "--min_nmf_iterations",
@@ -17,6 +17,15 @@ import torch
17
17
  import torch.nn
18
18
  from torch import nn
19
19
 
20
+ # Lower bound for W, H and W @ H in the KL multiplicative updates. A multiplicative
21
+ # update cannot move an entry away from exactly zero, so zeros from the NNDSVD
22
+ # initialization or from underflow stay locked for the whole run; the fit can then
23
+ # stall and pass the convergence test early at a worse solution. If a cell of W @ H
24
+ # reaches zero, V / (W @ H) gives inf (NaN where V is also zero), which the next
25
+ # matrix product spreads to every entry of W and H. The original CPU engine (nimfa)
26
+ # applied an equivalent floor after every iteration.
27
+ EPSILON = 1e-16
28
+
20
29
 
21
30
  class NMF:
22
31
  def __init__(
@@ -178,10 +187,13 @@ class NMF:
178
187
  @property
179
188
  def _kl_loss(self):
180
189
  # calculate kl_loss in double precision for better convergence criteria
190
+ # xlogy gives 0 where V == 0, where V * log(V / WH) would give NaN. pnmf()
191
+ # raises V to at least 1e-4, so this matters only when NMF is called directly.
192
+ reconstruction = self.reconstruction.clamp(min=EPSILON)
181
193
  return (
182
- (self._V * (self._V / self.reconstruction).log()).sum(dtype=torch.float64)
194
+ torch.xlogy(self._V, self._V / reconstruction).sum(dtype=torch.float64)
183
195
  - self._V.sum(dtype=torch.float64)
184
- + self.reconstruction.sum(dtype=torch.float64)
196
+ + reconstruction.sum(dtype=torch.float64)
185
197
  )
186
198
 
187
199
  @property
@@ -245,15 +257,17 @@ class NMF:
245
257
  ones = torch.ones(self._V.shape).type(self._tensor_type)
246
258
  for self._iter in range(self.max_iterations):
247
259
  ht = self.H.transpose(1, 2)
248
- numerator = (self._V / (self.W @ self.H)) @ ht
260
+ numerator = (self._V / (self.W @ self.H).clamp(min=EPSILON)) @ ht
249
261
 
250
- denomenator = ones @ ht
262
+ denomenator = (ones @ ht).clamp(min=EPSILON)
251
263
  self._W *= numerator / denomenator
264
+ self._W.clamp_(min=EPSILON)
252
265
 
253
266
  wt = self.W.transpose(1, 2)
254
- numerator = wt @ (self._V / (self.W @ self.H))
255
- denomenator = wt @ ones
267
+ numerator = wt @ (self._V / (self.W @ self.H).clamp(min=EPSILON))
268
+ denomenator = (wt @ ones).clamp(min=EPSILON)
256
269
  self._H *= numerator / denomenator
270
+ self._H.clamp_(min=EPSILON)
257
271
  if stop_iterations()[0]:
258
272
  self._conv = stop_iterations()[1]
259
273
  break
@@ -10,6 +10,15 @@ import torch
10
10
  import torch.nn
11
11
  from torch import nn
12
12
 
13
+ # Lower bound for W, H and W @ H in the KL multiplicative updates. A multiplicative
14
+ # update cannot move an entry away from exactly zero, so zeros from the NNDSVD
15
+ # initialization or from underflow stay locked for the whole run; the fit can then
16
+ # stall and pass the convergence test early at a worse solution. If a cell of W @ H
17
+ # reaches zero, V / (W @ H) gives inf (NaN where V is also zero), which the next
18
+ # matrix product spreads to every entry of W and H. The original CPU engine (nimfa)
19
+ # applied an equivalent floor after every iteration.
20
+ EPSILON = 1e-16
21
+
13
22
 
14
23
  class NMF:
15
24
  def __init__(
@@ -79,7 +88,32 @@ class NMF:
79
88
  """
80
89
  Initialise basis and coefficient matrices according to `init_method`
81
90
  """
82
- if init_method == "random":
91
+ if init_method == "random" and isinstance(self._generator, (list, tuple)):
92
+ # each replicate draws its W and then its H from its own generator, in
93
+ # the same order as nmf_cpu, so it starts from the same point on CPU
94
+ # and GPU however it is batched
95
+ if len(self._generator) != self._V.shape[0]:
96
+ raise ValueError("One generator per replicate is required.")
97
+ W = np.stack(
98
+ [
99
+ g.random((self._V.shape[1], self._rank), dtype=np.float64)
100
+ for g in self._generator
101
+ ]
102
+ )
103
+ H = np.stack(
104
+ [
105
+ g.random((self._rank, self._V.shape[2]), dtype=np.float64)
106
+ for g in self._generator
107
+ ]
108
+ )
109
+ W = torch.from_numpy(W).cuda()
110
+ H = torch.from_numpy(H).cuda()
111
+ if self._np_dtype is np.float32:
112
+ W = W.float()
113
+ H = H.float()
114
+ return W, H
115
+
116
+ elif init_method == "random":
83
117
  W = torch.from_numpy(
84
118
  self._generator.random(
85
119
  (self._V.shape[0], self._V.shape[1], self._rank), dtype=np.float64
@@ -102,7 +136,7 @@ class NMF:
102
136
  for i in range(self._V.shape[0]):
103
137
  vin = self._V.cpu().numpy()[i]
104
138
  W[i, :, :], H[i, :, :] = nv.initialize(
105
- vin, self._rank, options={"flag": 0, "generator": self._generator}
139
+ vin, self._rank, options={"flag": 0, "generator": self._replicate_generator(i)}
106
140
  )
107
141
 
108
142
  elif init_method == "nndsvda":
@@ -112,7 +146,7 @@ class NMF:
112
146
  for i in range(self._V.shape[0]):
113
147
  vin = self._V.cpu().numpy()[i]
114
148
  W[i, :, :], H[i, :, :] = nv.initialize(
115
- vin, self._rank, options={"flag": 1, "generator": self._generator}
149
+ vin, self._rank, options={"flag": 1, "generator": self._replicate_generator(i)}
116
150
  )
117
151
 
118
152
  elif init_method == "nndsvdar":
@@ -122,7 +156,7 @@ class NMF:
122
156
  for i in range(self._V.shape[0]):
123
157
  vin = self._V.cpu().numpy()[i]
124
158
  W[i, :, :], H[i, :, :] = nv.initialize(
125
- vin, self._rank, options={"flag": 2, "generator": self._generator}
159
+ vin, self._rank, options={"flag": 2, "generator": self._replicate_generator(i)}
126
160
  )
127
161
  elif init_method == "nndsvd_min":
128
162
  W = np.zeros([self._V.shape[0], self._V.shape[1], self._rank])
@@ -130,7 +164,7 @@ class NMF:
130
164
  nv = Nndsvd()
131
165
  for i in range(self._V.shape[0]):
132
166
  vin = self._V.cpu().numpy()[i]
133
- w, h = nv.initialize(vin, self._rank, options={"flag": 2, "generator": self._generator})
167
+ w, h = nv.initialize(vin, self._rank, options={"flag": 2, "generator": self._replicate_generator(i)})
134
168
  min_X = np.min(vin[vin > 0])
135
169
  h[h <= min_X] = min_X
136
170
  w[w <= min_X] = min_X
@@ -143,6 +177,12 @@ class NMF:
143
177
  H = torch.from_numpy(H).type(self._tensor_type).cuda(self._gpu_id)
144
178
  return W, H
145
179
 
180
+ def _replicate_generator(self, i):
181
+ """Generator of replicate i: its own one, or the one shared by the batch."""
182
+ if isinstance(self._generator, (list, tuple)):
183
+ return self._generator[i]
184
+ return self._generator
185
+
146
186
  @property
147
187
  def reconstruction(self):
148
188
  return self.W @ self.H
@@ -169,10 +209,13 @@ class NMF:
169
209
  @property
170
210
  def _kl_loss(self):
171
211
  # calculate kl_loss in double precision for better convergence criteria
212
+ # xlogy gives 0 where V == 0, where V * log(V / WH) would give NaN. pnmf()
213
+ # raises V to at least 1e-4, so this matters only when NMF is called directly.
214
+ reconstruction = self.reconstruction.clamp(min=EPSILON)
172
215
  return (
173
- (self._V * (self._V / self.reconstruction).log()).sum(dtype=torch.float64)
216
+ torch.xlogy(self._V, self._V / reconstruction).sum(dtype=torch.float64)
174
217
  - self._V.sum(dtype=torch.float64)
175
- + self.reconstruction.sum(dtype=torch.float64)
218
+ + reconstruction.sum(dtype=torch.float64)
176
219
  )
177
220
 
178
221
  @property
@@ -187,6 +230,47 @@ class NMF:
187
230
  self._prev_loss = self._kl_loss
188
231
  return False
189
232
 
233
+ def _replicate_kl_loss(self, i):
234
+ # _kl_loss of replicate i alone (the same expression, on a batch of one)
235
+ V = self._V[i : i + 1]
236
+ reconstruction = (self.W[i : i + 1] @ self.H[i : i + 1]).clamp(min=EPSILON)
237
+ return (
238
+ torch.xlogy(V, V / reconstruction).sum(dtype=torch.float64)
239
+ - V.sum(dtype=torch.float64)
240
+ + reconstruction.sum(dtype=torch.float64)
241
+ )
242
+
243
+ def _stop_converged_replicates(self):
244
+ """
245
+ The single-replicate stopping rule of _loss_converged, applied to every
246
+ replicate of the batch: every test_conv iterations, a replicate stops when
247
+ its relative loss change is below the tolerance and more than
248
+ min_iterations have run. A stopped replicate keeps its W and H, so each
249
+ replicate ends where it would have ended on its own.
250
+ """
251
+ if self._iter % self._test_conv != 0:
252
+ return
253
+ loss = torch.stack(
254
+ [self._replicate_kl_loss(i) for i in range(self._V.shape[0])]
255
+ )
256
+ if not self._iter:
257
+ self._loss_init = loss
258
+ self._prev_loss = loss
259
+ return
260
+ converged = ((self._prev_loss - loss) / self._loss_init) < self._tolerance
261
+ self._prev_loss = torch.where(converged, self._prev_loss, loss)
262
+ stop = converged & self._active & (self._iter > self.min_iterations)
263
+ self._replicate_conv[stop.cpu()] = self._iter
264
+ self._active &= ~stop
265
+
266
+ @property
267
+ def replicate_conv(self):
268
+ """Iteration at which each replicate converged (0 if it did not)."""
269
+ try:
270
+ return [int(c) for c in self._replicate_conv]
271
+ except AttributeError:
272
+ return [int(self.conv)] * self._V.shape[0]
273
+
190
274
  def fit(self, beta=1):
191
275
  """
192
276
  Fit the basis (W) and coefficient (H) matrices to the input matrix (V) using multiplicative updates and
@@ -232,20 +316,31 @@ class NMF:
232
316
  ones = (
233
317
  torch.ones(self._V.shape).type(self._tensor_type).cuda(self._gpu_id)
234
318
  )
319
+ self._active = torch.ones(
320
+ self._V.shape[0], dtype=torch.bool, device=self._V.device
321
+ )
322
+ self._replicate_conv = torch.zeros(self._V.shape[0], dtype=torch.long)
235
323
  for self._iter in range(self.max_iterations):
324
+ # replicates that have already converged keep their W and H
325
+ active = self._active[:, None, None]
236
326
  ht = self.H.transpose(1, 2)
237
- numerator = (self._V / (self.W @ self.H)) @ ht
327
+ numerator = (self._V / (self.W @ self.H).clamp(min=EPSILON)) @ ht
238
328
 
239
- denomenator = ones @ ht
240
- self._W *= numerator / denomenator
329
+ denomenator = (ones @ ht).clamp(min=EPSILON)
330
+ W = (self.W * (numerator / denomenator)).clamp(min=EPSILON)
331
+ self._W = torch.where(active, W, self._W)
241
332
 
242
333
  wt = self.W.transpose(1, 2)
243
- numerator = wt @ (self._V / (self.W @ self.H))
244
- denomenator = wt @ ones
245
- self._H *= numerator / denomenator
246
- if stop_iterations()[0]:
247
- self._conv = stop_iterations()[1]
334
+ numerator = wt @ (self._V / (self.W @ self.H).clamp(min=EPSILON))
335
+ denomenator = (wt @ ones).clamp(min=EPSILON)
336
+ H = (self.H * (numerator / denomenator)).clamp(min=EPSILON)
337
+ self._H = torch.where(active, H, self._H)
338
+
339
+ self._stop_converged_replicates()
340
+ if not self._active.any():
248
341
  break
342
+ if self._V.shape[0] == 1 and self._replicate_conv[0] > 0:
343
+ self._conv = int(self._replicate_conv[0])
249
344
 
250
345
  else:
251
346
  for self._iter in range(self.max_iterations):