SigProfilerExtractor 1.4.0__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.0 → sigprofilerextractor-1.5.0}/.github/workflows/ci.yml +5 -1
  2. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/.github/workflows/create-version-tag.yml +0 -0
  3. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/.github/workflows/docs.yml +0 -0
  4. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/.github/workflows/release.yml +0 -0
  5. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/.github/workflows/static.yml +0 -0
  6. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/.gitignore +0 -0
  7. sigprofilerextractor-1.5.0/CHANGELOG.md +109 -0
  8. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/Dockerfile +3 -0
  9. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/LICENSE.txt +0 -0
  10. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/MANIFEST.in +0 -0
  11. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/PKG-INFO +8 -8
  12. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/README.md +6 -6
  13. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/__init__.py +0 -0
  14. sigprofilerextractor-1.5.0/SigProfilerExtractor/_version.py +1 -0
  15. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/controllers/cli_controller.py +22 -7
  16. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/CNVInput/Battenberg_test.tsv +0 -0
  17. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/CSVInput/csv_example.csv +0 -0
  18. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/ReferenceFiles/CNV_features.tsv +0 -0
  19. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/ReferenceFiles/CN_classes_dictionary.txt +0 -0
  20. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/TextInput/Samples_CNV.txt +0 -0
  21. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/TextInput/Samples_DBS.txt +0 -0
  22. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/TextInput/Samples_ID.txt +0 -0
  23. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/TextInput/Samples_SBS.txt +0 -0
  24. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/TextInput/Samples_SV.txt +0 -0
  25. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/VCFInput/PD3851a.vcf +0 -0
  26. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/VCFInput/PD3890a.vcf +0 -0
  27. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/VCFInput/PD3904a.vcf +0 -0
  28. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/VCFInput/PD3905a.vcf +0 -0
  29. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/VCFInput/PD3945a.vcf +0 -0
  30. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/estimate_best_solution.py +0 -0
  31. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/nmf_cpu.py +20 -6
  32. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/nmf_gpu.py +110 -15
  33. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/nndsvd.py +0 -0
  34. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/sigpro.py +157 -34
  35. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/sigprofilerextractor_cli.py +0 -0
  36. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/subroutines.py +248 -218
  37. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/version.py +0 -0
  38. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor.egg-info/PKG-INFO +8 -8
  39. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor.egg-info/SOURCES.txt +19 -1
  40. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor.egg-info/dependency_links.txt +0 -0
  41. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor.egg-info/entry_points.txt +0 -0
  42. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor.egg-info/not-zip-safe +0 -0
  43. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor.egg-info/requires.txt +1 -1
  44. sigprofilerextractor-1.5.0/SigProfilerExtractor.egg-info/scm_file_list.json +100 -0
  45. sigprofilerextractor-1.5.0/SigProfilerExtractor.egg-info/scm_version.json +8 -0
  46. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor.egg-info/top_level.txt +0 -0
  47. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/1. Installation.md +0 -0
  48. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/2. Quick Start Example.md +0 -0
  49. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/3. Using the Tool - Input.md +130 -130
  50. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/4. Using the Tool - Output.md +6 -3
  51. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/5. Output - All Solutions.md +0 -0
  52. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/6. Output - Suggested Solution.md +0 -0
  53. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/4hnaj.png +0 -0
  54. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/5f8ep.png +0 -0
  55. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/5tr9y.png +0 -0
  56. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/5u7v8.png +0 -0
  57. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/9uf4s.png +0 -0
  58. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/a7gkf.png +0 -0
  59. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/akef5.png +0 -0
  60. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/c5hzf.png +0 -0
  61. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/dahws.png +0 -0
  62. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/h796w.png +0 -0
  63. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/kb4w2.png +0 -0
  64. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/ku93g.png +0 -0
  65. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/p4cfj.png +0 -0
  66. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/r9wcm.png +0 -0
  67. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/rz3ue.png +0 -0
  68. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/whn8t.png +0 -0
  69. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/xcm95.png +0 -0
  70. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/xqzfh.png +0 -0
  71. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/yhqnw.png +0 -0
  72. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/z98fk.png +0 -0
  73. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/images/osf/zu6sm.png +0 -0
  74. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/assets/stylesheets/extra.css +0 -0
  75. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/home.md +0 -0
  76. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/docs/index.md +0 -0
  77. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/install_genome.py +0 -0
  78. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/mkdocs.yml +0 -0
  79. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/mkdocs_hooks.py +0 -0
  80. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/pyproject.toml +0 -0
  81. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/setup.cfg +0 -0
  82. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/setup.py +1 -1
  83. sigprofilerextractor-1.5.0/tests/test_activity_export.py +18 -0
  84. sigprofilerextractor-1.5.0/tests/test_assignment_forwarding.py +44 -0
  85. sigprofilerextractor-1.5.0/tests/test_context_labels.py +22 -0
  86. sigprofilerextractor-1.5.0/tests/test_cosmic_version.py +112 -0
  87. sigprofilerextractor-1.5.0/tests/test_default_parity.py +23 -0
  88. sigprofilerextractor-1.5.0/tests/test_import_side_effects.py +45 -0
  89. sigprofilerextractor-1.5.0/tests/test_input_samples.py +37 -0
  90. sigprofilerextractor-1.5.0/tests/test_matrix_normalization.py +30 -0
  91. sigprofilerextractor-1.5.0/tests/test_nmf_floor.py +69 -0
  92. sigprofilerextractor-1.5.0/tests/test_normalization_cutoff.py +27 -0
  93. sigprofilerextractor-1.5.0/tests/test_output_directory.py +34 -0
  94. sigprofilerextractor-1.5.0/tests/test_precision.py +22 -0
  95. sigprofilerextractor-1.5.0/tests/test_replicate_batches.py +25 -0
  96. sigprofilerextractor-1.5.0/tests/test_replicate_diagnostics.py +30 -0
  97. sigprofilerextractor-1.5.0/tests/test_seed_file.py +32 -0
  98. sigprofilerextractor-1.5.0/tests/test_silhouette_stability.py +44 -0
  99. sigprofilerextractor-1.5.0/tests/test_sv_output_path.py +16 -0
  100. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/wikies/CLI-Usage.md +0 -0
  101. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/wikies/Citation.md +0 -0
  102. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/wikies/Home.md +0 -0
  103. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/wikies/Installation.md +0 -0
  104. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/wikies/Output.md +0 -0
  105. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/wikies/Python-API.md +0 -0
  106. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/wikies/_Sidebar.md +0 -0
  107. sigprofilerextractor-1.4.0/CHANGELOG.md +0 -80
  108. sigprofilerextractor-1.4.0/SigProfilerExtractor/_version.py +0 -1
  109. sigprofilerextractor-1.4.0/docs/assets/images/osf/h3tvx.png +0 -0
  110. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/SigProfilerExtractor/data/MatObjInput/21_breast_WGS_substitutions.mat +0 -0
  111. {sigprofilerextractor-1.4.0 → sigprofilerextractor-1.5.0}/test.py +0 -0
@@ -70,7 +70,11 @@ jobs:
70
70
  - name: Install genome
71
71
  run: |
72
72
  python install_genome.py ${{ github.workspace }}/src/
73
-
73
+
74
+ - name: Run unit tests
75
+ run: |
76
+ python -m unittest discover -s tests -v
77
+
74
78
  - name: Run tests
75
79
  run: |
76
80
  python3 test.py
@@ -0,0 +1,109 @@
1
+
2
+ # Changelog
3
+
4
+ All notable changes to this project will be documented in this file.
5
+
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
+
8
+ ## [1.5.0] - 2026-09-28
9
+
10
+ ### Changed
11
+ - Updated the default COSMIC reference signature version from 3.5 to 3.6.
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.
38
+
39
+ ## [1.2.7] - 2026-01-22
40
+
41
+ ### Fixed
42
+ - Fixed NumPy 2.0 compatibility issue by removing the `nimfa` dependency, which was incompatible with NumPy 2.0 due to use of deprecated `np.mat()` function.
43
+ - Fixed pandas 3.12 compatibility issues:
44
+ - Updated `to_csv()` calls to use `sep` as keyword argument instead of positional argument
45
+ - Fixed `set_index()` calls to work with pandas 3.12's stricter type checking by converting StringArray to list
46
+ - Fixed `iloc` assignment for string conversion operations
47
+ - Fixed Series indexing to use `.iloc[0]` for positional access instead of `[0]` for label-based access
48
+ - Fixed compatibility issues in SigProfilerAssignment and SigProfilerPlotting packages for pandas 3.12
49
+
50
+ ### Changed
51
+ - Removed `nimfa` dependency and implemented NNDSVD initialization directly in the codebase.
52
+ - Updated `sigProfilerPlotting` dependency to >=1.4.3 for pandas 3.12 compatibility.
53
+ - Removed TMB debug file output.
54
+ - Migrated CI/CD pipeline from Travis CI to GitHub Actions for improved reliability and modern workflow management.
55
+
56
+ ### Added
57
+ - Added `SigProfilerExtractor/nndsvd.py` with standalone NNDSVD implementation supporting all variants (nndsvd, nndsvda, nndsvdar, nndsvd_min).
58
+
59
+ ## [1.2.6] - 2026-01-06
60
+
61
+ ### Changed
62
+ - Updated default COSMIC version from 3.4 to 3.5. Added support for COSMIC v3.5 signatures in the `cosmic_version` parameter.
63
+ - Updated SigProfilerAssignment dependency requirement from >=1.0.1 to >=1.1.0 to support COSMIC v3.5 signatures.
64
+
65
+ ## [1.2.5] - 2025-10-28
66
+
67
+ ### Added
68
+ - Implemented a CI/CD pipeline with Travis CI to automate the building and publishing of Docker images to Docker Hub.
69
+ - Added a Dockerfile to the repository for containerization. Documentation on how to use the Dockerfile needs to be added to the README.
70
+
71
+ ## [1.2.4] - 2025-10-20
72
+
73
+ ### Added
74
+ - Added the `assignment_cpu` parameter to independently control the number of CPU cores used for the signature assignment step. This change enables full support for the parallel processing enhancements in **SigProfilerAssignment v1.0.0**, allowing for significant performance improvements and more granular resource control.
75
+
76
+ ## [1.2.3] - 2025-09-19
77
+
78
+ ### Added
79
+ - Added support for rn7 and mm39 genomes in SigProfilerExtractor.
80
+
81
+ ## [1.2.2] - 2025-08-11
82
+
83
+ ### Added
84
+ - Added mutation count and stability to 4608 plots in All_Solutions
85
+ - Add a stop parameter to the CLI to stop after de novo extraction.
86
+
87
+ ## [1.2.1] - 2025-05-14
88
+
89
+ ### Fixed
90
+ - Fixed an issue where the CLI was returning a non-zero exit code when the `--help` flag was passed.
91
+
92
+ ### Added
93
+ - Added `pyproject.toml` for modern Python packaging support.
94
+
95
+ ## [1.2.0] - 2025-02-11
96
+
97
+ ### Changed
98
+ - Updated dependencies: Now requires **Pandas >= 2.0.0**, **NumPy >= 2.0.0**, and **Python >= 3.9**.
99
+ - Dropped support for **Python 3.8**
100
+ - **Intel-based MacBooks are no longer supported** due to upstream changes in **PyTorch**, which has dropped support for macOS x86_64. Users with Intel-based MacBooks will need to migrate to Apple Silicon (M1/M2) or use a Linux-based development environment.
101
+
102
+ ## [1.1.25] - 2024-12-09
103
+
104
+ ### Added
105
+ - Introduced a Command-Line Interface (CLI) for SigProfilerExtractor, enabling users to interact with the tool via terminal commands.
106
+
107
+ ### Updated
108
+ - Improved the formatting of the parameter table for sigProfilerExtractor function for better readability and consistency.
109
+ - The CI/CD badge link has been fixed.
@@ -9,6 +9,9 @@ RUN apt-get update && apt-get install -y \
9
9
  python3-pip python3-dev git && \
10
10
  apt-get clean && rm -rf /var/lib/apt/lists/*
11
11
 
12
+ # Upgrade Ubuntu's packaging tools before invoking the PEP 517 build backend.
13
+ RUN python3 -m pip install --no-cache-dir --upgrade pip setuptools wheel
14
+
12
15
  WORKDIR /usr/src/app
13
16
 
14
17
  # Install GPU-enabled PyTorch wheels
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SigProfilerExtractor
3
- Version: 1.4.0
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
@@ -15,7 +15,7 @@ Requires-Dist: numpy>=2.0.0
15
15
  Requires-Dist: pandas>=2.0.0
16
16
  Requires-Dist: sigProfilerPlotting>=1.4.3
17
17
  Requires-Dist: SigProfilerMatrixGenerator>=1.3.5
18
- Requires-Dist: SigProfilerAssignment>=1.1.3
18
+ Requires-Dist: SigProfilerAssignment>=1.1.5
19
19
  Requires-Dist: statsmodels>=0.9.0
20
20
  Requires-Dist: scikit-learn>=0.24.2
21
21
  Requires-Dist: psutil>=5.6.1
@@ -120,8 +120,8 @@ help(sig.importdata)
120
120
  Extracts mutational signatures from an array of samples.
121
121
 
122
122
  ```python
123
- sigProfilerExtractor(input_type, out_put, input_data, reference_genome="GRCh37", opportunity_genome = "GRCh37", context_type = "default", exome = False,
124
- minimum_signatures=1, maximum_signatures=10, nmf_replicates=100, resample = True, batch_size=1, cpu=-1, gpu=False,
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=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,14 +154,14 @@ 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. |
161
161
  | | **combined_stability** | Float | The cutoff thresh-hold of the combined stability (sum of average and minimum stability) (default: `1.0`). Solutions with combined stabilities below this thresh-hold will not be considered. |
162
162
  | | **allow_stability_drop** | Boolean | Defines if solutions with a drop in stability with respect to the highest stable number of signatures will be considered (default: `False`). |
163
163
  | **Decomposition** | | | |
164
- | | **cosmic_version** | Float | Defines the version of the COSMIC reference signatures (default: `3.5`). Takes a positive float among `1`, `2`, `3`, `3.1`, `3.2`, `3.3`, `3.4`, and `3.5`.|
164
+ | | **cosmic_version** | Float | Defines the version of the COSMIC reference signatures (default: `3.6`). Takes a positive float among `1`, `2`, `3`, `3.1`, `3.2`, `3.3`, `3.4`, `3.5`, and `3.6`.|
165
165
  | | **make_decomposition_plots** | Boolean | Generate de novo to COSMIC signature decomposition plots as part of the results (default: `True`). Set to `False` to skip generating these plots. |
166
166
  | | **collapse_to_SBS96** | Boolean | If `True`, SBS288 and SBS1536 de novo signatures will be mapped to SBS96 reference signatures (default: `True`). If `False`, those will be mapped to reference signatures of the same context.
167
167
  | **Others** | | | |
@@ -88,8 +88,8 @@ help(sig.importdata)
88
88
  Extracts mutational signatures from an array of samples.
89
89
 
90
90
  ```python
91
- sigProfilerExtractor(input_type, out_put, input_data, reference_genome="GRCh37", opportunity_genome = "GRCh37", context_type = "default", exome = False,
92
- minimum_signatures=1, maximum_signatures=10, nmf_replicates=100, resample = True, batch_size=1, cpu=-1, gpu=False,
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=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,14 +122,14 @@ 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. |
129
129
  | | **combined_stability** | Float | The cutoff thresh-hold of the combined stability (sum of average and minimum stability) (default: `1.0`). Solutions with combined stabilities below this thresh-hold will not be considered. |
130
130
  | | **allow_stability_drop** | Boolean | Defines if solutions with a drop in stability with respect to the highest stable number of signatures will be considered (default: `False`). |
131
131
  | **Decomposition** | | | |
132
- | | **cosmic_version** | Float | Defines the version of the COSMIC reference signatures (default: `3.5`). Takes a positive float among `1`, `2`, `3`, `3.1`, `3.2`, `3.3`, `3.4`, and `3.5`.|
132
+ | | **cosmic_version** | Float | Defines the version of the COSMIC reference signatures (default: `3.6`). Takes a positive float among `1`, `2`, `3`, `3.1`, `3.2`, `3.3`, `3.4`, `3.5`, and `3.6`.|
133
133
  | | **make_decomposition_plots** | Boolean | Generate de novo to COSMIC signature decomposition plots as part of the results (default: `True`). Set to `False` to skip generating these plots. |
134
134
  | | **collapse_to_SBS96** | Boolean | If `True`, SBS288 and SBS1536 de novo signatures will be mapped to SBS96 reference signatures (default: `True`). If `False`, those will be mapped to reference signatures of the same context.
135
135
  | **Others** | | | |
@@ -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",
@@ -209,8 +220,12 @@ def parse_arguments_extractor(args: List[str], description: str) -> argparse.Nam
209
220
  parser.add_argument(
210
221
  "--cosmic_version",
211
222
  type=float,
212
- default=3.5,
213
- help="COSMIC version for reference signatures. Valid values are 1, 2, 3, 3.1, 3.2, 3.3, 3.4, and 3.5 (default: 3.5).",
223
+ default=sigpro.DEFAULT_COSMIC_VERSION,
224
+ help=(
225
+ "COSMIC version for reference signatures. Valid values are "
226
+ f"{', '.join(map(str, sigpro.SUPPORTED_COSMIC_VERSIONS))} "
227
+ f"(default: {sigpro.DEFAULT_COSMIC_VERSION})."
228
+ ),
214
229
  )
215
230
  parser.add_argument(
216
231
  "--make_decomposition_plots",
@@ -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):