From 725614c1729e912e8de1b7c91df3709c9294e256 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Wed, 21 Aug 2024 09:09:04 +0200 Subject: [PATCH 001/161] Use datadir to discover testfiles --- GenderCheck/test_calculate_gender.py | 26 ++++++++++-------- .../{ => test_calculate_gender}/test_bam.bam | Bin .../test_bam.bam.bai | Bin .../1.0.0/test_get_gender_from_bam_chrx.py | 22 ++++++++------- .../test_bam.bam | Bin .../test_bam.bam.bai | Bin 6 files changed, 27 insertions(+), 21 deletions(-) rename GenderCheck/{ => test_calculate_gender}/test_bam.bam (100%) rename GenderCheck/{ => test_calculate_gender}/test_bam.bam.bai (100%) rename MosaicHunter/1.0.0/{ => test_get_gender_from_bam_chrx}/test_bam.bam (100%) rename MosaicHunter/1.0.0/{ => test_get_gender_from_bam_chrx}/test_bam.bam.bai (100%) diff --git a/GenderCheck/test_calculate_gender.py b/GenderCheck/test_calculate_gender.py index 500e9c82..27a0bb37 100644 --- a/GenderCheck/test_calculate_gender.py +++ b/GenderCheck/test_calculate_gender.py @@ -1,7 +1,11 @@ -import calculate_gender - +#!/usr/bin/env python +# Import statements, alphabetic order of main package. +# Third party libraries alphabetic order of main package. import pytest +# Custom libraries alphabetic order of main package. +import calculate_gender + class TestIsValidRead(): @@ -12,11 +16,11 @@ def __init__(self, qual, start, end): self.reference_end = end @pytest.mark.parametrize("read,mapping_qual,expected", [ - (MyObject(19, True, True), 20, False), # mapping quality is below the threshold - (MyObject(20, True, True), 20, True), # mapping quality is equal to the threshold - (MyObject(20, True, True), 19, True), # mapping quality is higher than the threshold - (MyObject(20, False, True), 20, False), # reference_end is false - (MyObject(20, True, False), 20, False), # reference_start is false + (MyObject(19, True, True), 20, False), # Mapping quality is below the threshold + (MyObject(20, True, True), 20, True), # Mapping quality is equal to the threshold + (MyObject(20, True, True), 19, True), # Mapping quality is higher than the threshold + (MyObject(20, False, True), 20, False), # Reference_end is false + (MyObject(20, True, False), 20, False), # Reference_start is false ]) def test_is_valid_read(self, read, mapping_qual, expected): assert expected == calculate_gender.is_valid_read(read, mapping_qual) @@ -24,11 +28,11 @@ def test_is_valid_read(self, read, mapping_qual, expected): class TestGetGenderFromBam(): @pytest.mark.parametrize("bam,mapping_qual,locus_y,ratio_y,expected", [ - ("./test_bam.bam", 20, "Y:2649520-59034050", 0.02, "male"), # output male below - ("./test_bam.bam", 20, "Y:2649520-59034050", 0.22, "female"), # output female + ("test_bam.bam", 20, "Y:2649520-59034050", 0.02, "male"), # Output male below + ("test_bam.bam", 20, "Y:2649520-59034050", 0.22, "female"), # Output female ]) - def test_get_gender_from_bam(self, bam, mapping_qual, locus_y, ratio_y, expected): - assert expected == calculate_gender.get_gender_from_bam(bam, mapping_qual, locus_y, ratio_y) + def test_get_gender_from_bam(self, bam, mapping_qual, locus_y, ratio_y, expected, datadir): + assert expected == calculate_gender.get_gender_from_bam(f"{datadir}/{bam}", mapping_qual, locus_y, ratio_y) class TestCompareGender(): diff --git a/GenderCheck/test_bam.bam b/GenderCheck/test_calculate_gender/test_bam.bam similarity index 100% rename from GenderCheck/test_bam.bam rename to GenderCheck/test_calculate_gender/test_bam.bam diff --git a/GenderCheck/test_bam.bam.bai b/GenderCheck/test_calculate_gender/test_bam.bam.bai similarity index 100% rename from GenderCheck/test_bam.bam.bai rename to GenderCheck/test_calculate_gender/test_bam.bam.bai diff --git a/MosaicHunter/1.0.0/test_get_gender_from_bam_chrx.py b/MosaicHunter/1.0.0/test_get_gender_from_bam_chrx.py index 0a494ea9..a3fa47a4 100644 --- a/MosaicHunter/1.0.0/test_get_gender_from_bam_chrx.py +++ b/MosaicHunter/1.0.0/test_get_gender_from_bam_chrx.py @@ -10,11 +10,11 @@ def __init__(self, qual, start, end): self.reference_end = end @pytest.mark.parametrize("read,mapping_qual,expected", [ - (ValidReadObject(19, True, True), 20, False), # mapping quality is below the threshold - (ValidReadObject(20, True, True), 20, True), # mapping quality is equal to the threshold - (ValidReadObject(20, True, True), 19, True), # mapping quality is higher than the threshold - (ValidReadObject(20, False, True), 20, False), # reference_end is false - (ValidReadObject(20, True, False), 20, False), # reference_start is false + (ValidReadObject(19, True, True), 20, False), # Mapping quality is below the threshold + (ValidReadObject(20, True, True), 20, True), # Mapping quality is equal to the threshold + (ValidReadObject(20, True, True), 19, True), # Mapping quality is higher than the threshold + (ValidReadObject(20, False, True), 20, False), # Reference_end is false + (ValidReadObject(20, True, False), 20, False), # Reference_start is false ]) def test_is_valid_read(self, read, mapping_qual, expected): assert expected == get_gender_from_bam_chrx.is_valid_read(read, mapping_qual) @@ -22,13 +22,15 @@ def test_is_valid_read(self, read, mapping_qual, expected): class TestGetGenderFromBam: @pytest.mark.parametrize("bam,mapping_qual,locus_x,ratio_x_threshold_male,ratio_x_threshold_female,expected_outcome", [ - ("./test_bam.bam", 20, "X:2699520-154931044", 3.5, 4.5, ("F", False)), - ("./test_bam.bam", 20, "X:2699520-154931044", 5.5, 7.5, ("M", False)), - ("./test_bam.bam", 20, "X:2699520-154931044", 4.5, 6.5, ("F", True)), + ("test_bam.bam", 20, "X:2699520-154931044", 3.5, 4.5, ("F", False)), + ("test_bam.bam", 20, "X:2699520-154931044", 5.5, 7.5, ("M", False)), + ("test_bam.bam", 20, "X:2699520-154931044", 4.5, 6.5, ("F", True)), ]) - def test_get_gender_from_bam(self, bam, mapping_qual, locus_x, ratio_x_threshold_male, ratio_x_threshold_female, expected_outcome): + def test_get_gender_from_bam( + self, bam, mapping_qual, locus_x, ratio_x_threshold_male, ratio_x_threshold_female, expected_outcome, datadir + ): assert expected_outcome == get_gender_from_bam_chrx.get_gender_from_bam_chrx( - bam, mapping_qual, locus_x, ratio_x_threshold_male, ratio_x_threshold_female) + f"{datadir}/{bam}", mapping_qual, locus_x, ratio_x_threshold_male, ratio_x_threshold_female) class TestWriteGenderDataToFile: diff --git a/MosaicHunter/1.0.0/test_bam.bam b/MosaicHunter/1.0.0/test_get_gender_from_bam_chrx/test_bam.bam similarity index 100% rename from MosaicHunter/1.0.0/test_bam.bam rename to MosaicHunter/1.0.0/test_get_gender_from_bam_chrx/test_bam.bam diff --git a/MosaicHunter/1.0.0/test_bam.bam.bai b/MosaicHunter/1.0.0/test_get_gender_from_bam_chrx/test_bam.bam.bai similarity index 100% rename from MosaicHunter/1.0.0/test_bam.bam.bai rename to MosaicHunter/1.0.0/test_get_gender_from_bam_chrx/test_bam.bam.bai From b7e73dd2d52fcff288fe5bc98b52810ddbb7803f Mon Sep 17 00:00:00 2001 From: ellendejong Date: Wed, 21 Aug 2024 09:10:52 +0200 Subject: [PATCH 002/161] Resolve flake8 warnings and errors --- Utils/create_hsmetrics_summary.py | 2 +- Utils/get_stats_from_flagstat.py | 26 ++++++++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/Utils/create_hsmetrics_summary.py b/Utils/create_hsmetrics_summary.py index 405e1ce1..4256fc33 100644 --- a/Utils/create_hsmetrics_summary.py +++ b/Utils/create_hsmetrics_summary.py @@ -8,7 +8,7 @@ parser.add_argument('hsmetrics_files', type=argparse.FileType('r'), nargs='*', help='HSMetric file') arguments = parser.parse_args() - interval_files_pattern = re.compile("BAIT_INTERVALS=\[(\S*)\].TARGET_INTERVALS=\[(\S*)\]") + interval_files_pattern = re.compile("BAIT_INTERVALS=\[(\S*)\].TARGET_INTERVALS=\[(\S*)\]") # noqa: W605 summary_header = [] summary_data = {} for hsmetrics_file in arguments.hsmetrics_files: diff --git a/Utils/get_stats_from_flagstat.py b/Utils/get_stats_from_flagstat.py index 2f46abbe..75a145a7 100644 --- a/Utils/get_stats_from_flagstat.py +++ b/Utils/get_stats_from_flagstat.py @@ -43,12 +43,26 @@ print("\n\t{0} %duplication\n".format(100*sample_dups/sample_mapped)) - print("Total raw reads: {total:,} reads (Total throughput, 75bp={total_75bp:,} bp, 100bp={total_100bp:,} bp, 150bp={total_150bp:,} bp)".format( - total=counts['total'], total_75bp=counts['total']*75, total_100bp=counts['total']*100, total_150bp=counts['total']*150 - )) - print("Total mapped reads: {total:,} reads (Total throughput, 75bp={total_75bp:,} bp, 100bp={total_100bp:,} bp, 150bp={total_150bp:,} bp)".format( - total=counts['mapped'], total_75bp=counts['mapped']*75, total_100bp=counts['mapped']*100, total_150bp=counts['mapped']*150 - )) + print( + "Total raw reads: {total:,} reads " + "(Total throughput, 75bp={total_75bp:,} bp, 100bp={total_100bp:,} bp, 150bp={total_150bp:,} bp)".format( + total=counts['total'], + total_75bp=counts['total']*75, + total_100bp=counts['total']*100, + total_150bp=counts['total']*150 + ) + ) + + print( + "Total mapped reads: {total:,} reads " + "(Total throughput, 75bp={total_75bp:,} bp, 100bp={total_100bp:,} bp, 150bp={total_150bp:,} bp)" + .format( + total=counts['mapped'], + total_75bp=counts['mapped']*75, + total_100bp=counts['mapped']*100, + total_150bp=counts['mapped']*150 + ) + ) print("Average mapped per lib: {:,} reads".format(int(round(float(counts['mapped'])/float(counts['files']))))) print("Average dups per lib: {:,} reads".format(int(round(float(counts['dups'])/float(counts['files']))))) print("Average dups % per lib: {:.2f} %".format(100*float(counts['dups'])/float(counts['mapped']))) From 07bc221cfdcc2cab5e7654b21442214ac62e1c78 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Tue, 17 Sep 2024 13:39:10 +0200 Subject: [PATCH 003/161] add checkqc github action --- .github/workflows/checkqc_lint.yml | 8 + .github/workflows/checkqc_test.yml | 54 +++ CheckQC/poetry.lock | 686 +++++++++++++++++++++++++++++ CheckQC/pyproject.toml | 32 ++ 4 files changed, 780 insertions(+) create mode 100644 .github/workflows/checkqc_lint.yml create mode 100644 .github/workflows/checkqc_test.yml create mode 100644 CheckQC/poetry.lock create mode 100644 CheckQC/pyproject.toml diff --git a/.github/workflows/checkqc_lint.yml b/.github/workflows/checkqc_lint.yml new file mode 100644 index 00000000..690db146 --- /dev/null +++ b/.github/workflows/checkqc_lint.yml @@ -0,0 +1,8 @@ +name: CheckQC Lint +on: pull_request +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: chartboost/ruff-action@v1 diff --git a/.github/workflows/checkqc_test.yml b/.github/workflows/checkqc_test.yml new file mode 100644 index 00000000..48081c9d --- /dev/null +++ b/.github/workflows/checkqc_test.yml @@ -0,0 +1,54 @@ +# Source: https://github.com/marketplace/actions/install-poetry-action +name: CheckQC Test +on: pull_request +jobs: + pytest: + runs-on: ubuntu-latest + steps: + #---------------------------------------------- + # check-out repo and set-up python + #---------------------------------------------- + - name: Check out repository + uses: actions/checkout@v4 + - name: Set up python + id: setup-python + uses: actions/setup-python@v5 + with: + python-version: '3.11.5' + #---------------------------------------------- + # install & configure poetry + #---------------------------------------------- + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + + #---------------------------------------------- + # load cached venv if cache exists + #---------------------------------------------- + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v4 + with: + path: .venv_checkqc + key: venv_checkqc-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + #---------------------------------------------- + # install dependencies if cache does not exist + #---------------------------------------------- + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + #---------------------------------------------- + # install root project + #---------------------------------------------- + - name: Install project + run: poetry install --no-interaction + #---------------------------------------------- + # run pytest + #---------------------------------------------- + - name: Run tests + run: | + source .venv_checkqc/bin/activate + pytest tests diff --git a/CheckQC/poetry.lock b/CheckQC/poetry.lock new file mode 100644 index 00000000..32eebbf3 --- /dev/null +++ b/CheckQC/poetry.lock @@ -0,0 +1,686 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "atomicwrites" +version = "1.4.1" +description = "Atomic file writes." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, +] + +[[package]] +name = "attrs" +version = "24.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.6.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, + {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, + {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, + {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, + {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "flake8" +version = "7.1.1" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"}, + {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.12.0,<2.13.0" +pyflakes = ">=3.2.0,<3.3.0" + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pandas" +version = "2.1.4" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pandas-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bdec823dc6ec53f7a6339a0e34c68b144a7a1fd28d80c260534c39c62c5bf8c9"}, + {file = "pandas-2.1.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:294d96cfaf28d688f30c918a765ea2ae2e0e71d3536754f4b6de0ea4a496d034"}, + {file = "pandas-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b728fb8deba8905b319f96447a27033969f3ea1fea09d07d296c9030ab2ed1d"}, + {file = "pandas-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00028e6737c594feac3c2df15636d73ace46b8314d236100b57ed7e4b9ebe8d9"}, + {file = "pandas-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:426dc0f1b187523c4db06f96fb5c8d1a845e259c99bda74f7de97bd8a3bb3139"}, + {file = "pandas-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:f237e6ca6421265643608813ce9793610ad09b40154a3344a088159590469e46"}, + {file = "pandas-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b7d852d16c270e4331f6f59b3e9aa23f935f5c4b0ed2d0bc77637a8890a5d092"}, + {file = "pandas-2.1.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd7d5f2f54f78164b3d7a40f33bf79a74cdee72c31affec86bfcabe7e0789821"}, + {file = "pandas-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0aa6e92e639da0d6e2017d9ccff563222f4eb31e4b2c3cf32a2a392fc3103c0d"}, + {file = "pandas-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d797591b6846b9db79e65dc2d0d48e61f7db8d10b2a9480b4e3faaddc421a171"}, + {file = "pandas-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2d3e7b00f703aea3945995ee63375c61b2e6aa5aa7871c5d622870e5e137623"}, + {file = "pandas-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:dc9bf7ade01143cddc0074aa6995edd05323974e6e40d9dbde081021ded8510e"}, + {file = "pandas-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:482d5076e1791777e1571f2e2d789e940dedd927325cc3cb6d0800c6304082f6"}, + {file = "pandas-2.1.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8a706cfe7955c4ca59af8c7a0517370eafbd98593155b48f10f9811da440248b"}, + {file = "pandas-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0513a132a15977b4a5b89aabd304647919bc2169eac4c8536afb29c07c23540"}, + {file = "pandas-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9f17f2b6fc076b2a0078862547595d66244db0f41bf79fc5f64a5c4d635bead"}, + {file = "pandas-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:45d63d2a9b1b37fa6c84a68ba2422dc9ed018bdaa668c7f47566a01188ceeec1"}, + {file = "pandas-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:f69b0c9bb174a2342818d3e2778584e18c740d56857fc5cdb944ec8bbe4082cf"}, + {file = "pandas-2.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3f06bda01a143020bad20f7a85dd5f4a1600112145f126bc9e3e42077c24ef34"}, + {file = "pandas-2.1.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab5796839eb1fd62a39eec2916d3e979ec3130509930fea17fe6f81e18108f6a"}, + {file = "pandas-2.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edbaf9e8d3a63a9276d707b4d25930a262341bca9874fcb22eff5e3da5394732"}, + {file = "pandas-2.1.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ebfd771110b50055712b3b711b51bee5d50135429364d0498e1213a7adc2be8"}, + {file = "pandas-2.1.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8ea107e0be2aba1da619cc6ba3f999b2bfc9669a83554b1904ce3dd9507f0860"}, + {file = "pandas-2.1.4-cp39-cp39-win_amd64.whl", hash = "sha256:d65148b14788b3758daf57bf42725caa536575da2b64df9964c563b015230984"}, + {file = "pandas-2.1.4.tar.gz", hash = "sha256:fcb68203c833cc735321512e13861358079a96c174a61f5116a1de89c58c0ef7"}, +] + +[package.dependencies] +numpy = {version = ">=1.22.4,<2", markers = "python_version < \"3.11\""} +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.1" + +[package.extras] +all = ["PyQt5 (>=5.15.6)", "SQLAlchemy (>=1.4.36)", "beautifulsoup4 (>=4.11.1)", "bottleneck (>=1.3.4)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=0.8.1)", "fsspec (>=2022.05.0)", "gcsfs (>=2022.05.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.8.0)", "matplotlib (>=3.6.1)", "numba (>=0.55.2)", "numexpr (>=2.8.0)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pandas-gbq (>=0.17.5)", "psycopg2 (>=2.9.3)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.5)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "pyxlsb (>=1.0.9)", "qtpy (>=2.2.0)", "s3fs (>=2022.05.0)", "scipy (>=1.8.1)", "tables (>=3.7.0)", "tabulate (>=0.8.10)", "xarray (>=2022.03.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)", "zstandard (>=0.17.0)"] +aws = ["s3fs (>=2022.05.0)"] +clipboard = ["PyQt5 (>=5.15.6)", "qtpy (>=2.2.0)"] +compression = ["zstandard (>=0.17.0)"] +computation = ["scipy (>=1.8.1)", "xarray (>=2022.03.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pyxlsb (>=1.0.9)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)"] +feather = ["pyarrow (>=7.0.0)"] +fss = ["fsspec (>=2022.05.0)"] +gcp = ["gcsfs (>=2022.05.0)", "pandas-gbq (>=0.17.5)"] +hdf5 = ["tables (>=3.7.0)"] +html = ["beautifulsoup4 (>=4.11.1)", "html5lib (>=1.1)", "lxml (>=4.8.0)"] +mysql = ["SQLAlchemy (>=1.4.36)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.8.10)"] +parquet = ["pyarrow (>=7.0.0)"] +performance = ["bottleneck (>=1.3.4)", "numba (>=0.55.2)", "numexpr (>=2.8.0)"] +plot = ["matplotlib (>=3.6.1)"] +postgresql = ["SQLAlchemy (>=1.4.36)", "psycopg2 (>=2.9.3)"] +spss = ["pyreadstat (>=1.1.5)"] +sql-other = ["SQLAlchemy (>=1.4.36)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.8.0)"] + +[[package]] +name = "pip" +version = "24.2" +description = "The PyPA recommended tool for installing Python packages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pip-24.2-py3-none-any.whl", hash = "sha256:2cd581cf58ab7fcfca4ce8efa6dcacd0de5bf8d0a3eb9ec927e07405f4d9e2a2"}, + {file = "pip-24.2.tar.gz", hash = "sha256:5b5e490b5e9cb275c879595064adce9ebd31b854e3e803740b72f9ccf34a45b8"}, +] + +[[package]] +name = "pip-api" +version = "0.0.34" +description = "An unofficial, importable pip API" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pip_api-0.0.34-py3-none-any.whl", hash = "sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb"}, + {file = "pip_api-0.0.34.tar.gz", hash = "sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625"}, +] + +[package.dependencies] +pip = "*" + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] + +[[package]] +name = "pyaml" +version = "24.7.0" +description = "PyYAML-based module to produce a bit more pretty and readable YAML-serialized data" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyaml-24.7.0-py3-none-any.whl", hash = "sha256:6b06596cb5ac438a3fad1e1bf5775088c4d3afb927e2b03a29305d334835deb2"}, + {file = "pyaml-24.7.0.tar.gz", hash = "sha256:5d0fdf9e681036fb263a783d0298fc3af580a6e2a6cf1a3314ffc48dc3d91ccb"}, +] + +[package.dependencies] +PyYAML = "*" + +[package.extras] +anchors = ["unidecode"] + +[[package]] +name = "pycodestyle" +version = "2.12.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, + {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, +] + +[[package]] +name = "pyflakes" +version = "3.2.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, +] + +[[package]] +name = "pyjson" +version = "1.4.1" +description = "Compare the similarities between two JSONs." +optional = false +python-versions = "*" +files = [ + {file = "pyjson-1.4.1-py3-none-any.whl", hash = "sha256:625ee332ca09056216595e232b562a8d42e1cba8b6695fc169b0a0c61587d56d"}, + {file = "pyjson-1.4.1.tar.gz", hash = "sha256:79ebe55cccb6224302baca9f7119927c73dcb1c18c3fed193db80cfb320d0ca6"}, +] + +[[package]] +name = "pytest" +version = "6.2.5" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, +] + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "3.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.6" +files = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-datadir" +version = "1.5.0" +description = "pytest plugin for test data directories and files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-datadir-1.5.0.tar.gz", hash = "sha256:1617ed92f9afda0c877e4eac91904b5f779d24ba8f5e438752e3ae39d8d2ee3f"}, + {file = "pytest_datadir-1.5.0-py3-none-any.whl", hash = "sha256:34adf361bcc7b37961bbc1dfa8d25a4829e778bab461703c38a5c50ca9c36dc8"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[[package]] +name = "pytest-datafiles" +version = "2.0" +description = "py.test plugin to create a 'tmpdir' containing predefined files/directories." +optional = false +python-versions = "*" +files = [ + {file = "pytest-datafiles-2.0.tar.gz", hash = "sha256:143329cbb1dbbb07af24f88fa4668e2f59ce233696cf12c49fd1c98d1756dbf9"}, + {file = "pytest_datafiles-2.0-py2.py3-none-any.whl", hash = "sha256:e349b6ad7bcca111f3677b7201d3ca81f93b5e09dcfae8ee2be2c3cae9f55bc7"}, +] + +[package.dependencies] +py = "*" +pytest = ">=3.6" + +[[package]] +name = "pytest-dataset" +version = "0.3.2" +description = "Plugin for loading different datasets for pytest by prefix from json or yaml files" +optional = false +python-versions = "*" +files = [ + {file = "pytest-dataset-0.3.2.tar.gz", hash = "sha256:15d018f589b38f690408936fa29bce84a9a16bd9f4cea39e384470ff70920cb6"}, +] + +[package.dependencies] +pyaml = "*" +pyjson = "*" +pytest = "*" + +[[package]] +name = "pytest-flake8" +version = "1.0.7" +description = "pytest plugin to check FLAKE8 requirements" +optional = false +python-versions = "*" +files = [ + {file = "pytest-flake8-1.0.7.tar.gz", hash = "sha256:f0259761a903563f33d6f099914afef339c085085e643bee8343eb323b32dd6b"}, + {file = "pytest_flake8-1.0.7-py2.py3-none-any.whl", hash = "sha256:c28cf23e7d359753c896745fd4ba859495d02e16c84bac36caa8b1eec58f5bc1"}, +] + +[package.dependencies] +flake8 = ">=3.5" +pytest = ">=3.5" + +[[package]] +name = "pytest-mock" +version = "3.8.2" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-mock-3.8.2.tar.gz", hash = "sha256:77f03f4554392558700295e05aed0b1096a20d4a60a4f3ddcde58b0c31c8fca2"}, + {file = "pytest_mock-3.8.2-py3-none-any.whl", hash = "sha256:8a9e226d6c0ef09fcf20c94eb3405c388af438a90f3e39687f84166da82d5948"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "pytest-raises" +version = "0.11" +description = "An implementation of pytest.raises as a pytest.mark fixture" +optional = false +python-versions = "*" +files = [ + {file = "pytest-raises-0.11.tar.gz", hash = "sha256:f64a4dbcb5f89c100670fe83d87a5cd9d956586db461c5c628f7eb94b749c90b"}, + {file = "pytest_raises-0.11-py2.py3-none-any.whl", hash = "sha256:33a1351f2debb9f74ca6ef70e374899f608a1217bf13ca4a0767f37b49e9cdda"}, +] + +[package.dependencies] +pytest = ">=3.2.2" + +[package.extras] +develop = ["pylint", "pytest-cov"] + +[[package]] +name = "pytest-reqs" +version = "0.2.1" +description = "pytest plugin to check pinned requirements" +optional = false +python-versions = "*" +files = [ + {file = "pytest-reqs-0.2.1.tar.gz", hash = "sha256:a844458b1e65ca7038be5201c814472725ddcc881ab33125c86b952232a7cfd8"}, + {file = "pytest_reqs-0.2.1-py3-none-any.whl", hash = "sha256:e87fcc2ea23fea9edb9e2ed877b4e839a4aa44df430f9a38f7469a70fdb0edfc"}, +] + +[package.dependencies] +packaging = ">=17.1" +pip-api = ">=0.0.2" +pytest = ">=2.4.2" + +[[package]] +name = "pytest-unordered" +version = "0.5.2" +description = "Test equality of unordered collections in pytest" +optional = false +python-versions = "*" +files = [ + {file = "pytest-unordered-0.5.2.tar.gz", hash = "sha256:8187e6d68a7d54e5447e88c229cbeafa38205e55baf7da7ae57cc965c1ecdbb3"}, + {file = "pytest_unordered-0.5.2-py3-none-any.whl", hash = "sha256:b01bb0e8ba80db6dd8c840fe24ad1804c8672919303dc9302688221390a7dc29"}, +] + +[package.dependencies] +pytest = ">=6.0.0" + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2024.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, + {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tzdata" +version = "2024.1" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "3.9.2" +content-hash = "64302a36da3450260a740f2ba7b94069fb90da4a5fe77cf3a8727c46f8c99d97" diff --git a/CheckQC/pyproject.toml b/CheckQC/pyproject.toml new file mode 100644 index 00000000..7652a75a --- /dev/null +++ b/CheckQC/pyproject.toml @@ -0,0 +1,32 @@ +[tool.poetry] +name = "checkqc" +version = "0.1.0" +description = "Check and judge qc metrics (files)" +authors = ["Your Name "] +license = 'MIT' +package-mode = false + +[tool.poetry.dependencies] +python = "3.9.2" +pandas = "2.1.4" +pyyaml = "6.0.1" + +[tool.poetry.group.dev.dependencies] +pytest = "6.2.5" +pytest-cov = "3.0.0" +pytest-datadir = "1.5.0" +pytest-datafiles = "2.0" +pytest-dataset = "0.3.2" +pytest-flake8 = "1.0.7" +pytest-mock = "3.8.2" +pytest-raises = "0.11" +pytest-reqs = "0.2.1" +pytest-unordered = "0.5.2" + +[tool.ruff] +line-length = 127 +indent-width = 4 + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" From 3a2ed7167786bc3bee69fc0d09fb4a520cdb1685 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Wed, 18 Sep 2024 13:20:55 +0200 Subject: [PATCH 004/161] change poetry venv, and use working directory --- .github/workflows/checkqc_test.yml | 7 ++- CheckQC/poetry.lock | 79 +++++------------------------- CheckQC/pyproject.toml | 5 +- 3 files changed, 20 insertions(+), 71 deletions(-) diff --git a/.github/workflows/checkqc_test.yml b/.github/workflows/checkqc_test.yml index 48081c9d..b2a1b588 100644 --- a/.github/workflows/checkqc_test.yml +++ b/.github/workflows/checkqc_test.yml @@ -4,6 +4,9 @@ on: pull_request jobs: pytest: runs-on: ubuntu-latest + defaults: + run: + working-directory: ./CheckQC/ steps: #---------------------------------------------- # check-out repo and set-up python @@ -32,7 +35,7 @@ jobs: id: cached-poetry-dependencies uses: actions/cache@v4 with: - path: .venv_checkqc + path: .venv key: venv_checkqc-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} #---------------------------------------------- # install dependencies if cache does not exist @@ -50,5 +53,5 @@ jobs: #---------------------------------------------- - name: Run tests run: | - source .venv_checkqc/bin/activate + source .venv/bin/activate pytest tests diff --git a/CheckQC/poetry.lock b/CheckQC/poetry.lock index 32eebbf3..8b109204 100644 --- a/CheckQC/poetry.lock +++ b/CheckQC/poetry.lock @@ -1,34 +1,5 @@ # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. -[[package]] -name = "atomicwrites" -version = "1.4.1" -description = "Atomic file writes." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, -] - -[[package]] -name = "attrs" -version = "24.2.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.7" -files = [ - {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, - {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, -] - -[package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] - [[package]] name = "colorama" version = "0.4.6" @@ -121,9 +92,6 @@ files = [ {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, ] -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - [package.extras] toml = ["tomli"] @@ -256,7 +224,10 @@ files = [ ] [package.dependencies] -numpy = {version = ">=1.22.4,<2", markers = "python_version < \"3.11\""} +numpy = [ + {version = ">=1.23.2,<2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0,<2", markers = "python_version >= \"3.12\""}, +] python-dateutil = ">=2.8.2" pytz = ">=2020.1" tzdata = ">=2022.1" @@ -388,27 +359,23 @@ files = [ [[package]] name = "pytest" -version = "6.2.5" +version = "8.3.3" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, - {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, ] [package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -toml = "*" +pluggy = ">=1.5,<2" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" @@ -647,28 +614,6 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - [[package]] name = "tzdata" version = "2024.1" @@ -682,5 +627,5 @@ files = [ [metadata] lock-version = "2.0" -python-versions = "3.9.2" -content-hash = "64302a36da3450260a740f2ba7b94069fb90da4a5fe77cf3a8727c46f8c99d97" +python-versions = "^3.11.4" +content-hash = "b9545131c195e37f949ffff6f6a065464f501b7922094a291b51a93cfc521886" diff --git a/CheckQC/pyproject.toml b/CheckQC/pyproject.toml index 7652a75a..c7e7591e 100644 --- a/CheckQC/pyproject.toml +++ b/CheckQC/pyproject.toml @@ -7,12 +7,13 @@ license = 'MIT' package-mode = false [tool.poetry.dependencies] -python = "3.9.2" +python = "^3.11.4" pandas = "2.1.4" pyyaml = "6.0.1" +pytest = "^8.3.3" [tool.poetry.group.dev.dependencies] -pytest = "6.2.5" +pytest = "^8.3.3" pytest-cov = "3.0.0" pytest-datadir = "1.5.0" pytest-datafiles = "2.0" From b070824e6d8babe639fc3a7e405fd715d4213762 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Wed, 18 Sep 2024 13:33:55 +0200 Subject: [PATCH 005/161] add pyproject custommodules --- poetry.lock | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 19 +++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..d886a2be --- /dev/null +++ b/poetry.lock @@ -0,0 +1,74 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "8.3.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11.4" +content-hash = "0e9c7de1a14ab89ba216b00deb52b83bda978915d965f459b3bfce8f2b6f5d77" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..a70546a1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "custommodules" +version = "0.1.0" +description = "custom nextflow processes and their linked files" +authors = ["Your Name "] +license = "MIT" +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11.4" +pytest = "^8.3.3" + +[tool.ruff] +line-length = 127 +indent-width = 4 + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" From 856b46e5fa6d2b4732ce11777de806f13ef93e99 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Fri, 20 Sep 2024 08:57:29 +0200 Subject: [PATCH 006/161] run pytest in working dir --- .github/workflows/checkqc_test.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/checkqc_test.yml b/.github/workflows/checkqc_test.yml index b2a1b588..6e48cc6d 100644 --- a/.github/workflows/checkqc_test.yml +++ b/.github/workflows/checkqc_test.yml @@ -1,12 +1,15 @@ # Source: https://github.com/marketplace/actions/install-poetry-action name: CheckQC Test -on: pull_request +on: + pull_request: + paths: CheckQC/** + jobs: pytest: runs-on: ubuntu-latest defaults: run: - working-directory: ./CheckQC/ + working-directory: CheckQC/ steps: #---------------------------------------------- # check-out repo and set-up python @@ -54,4 +57,4 @@ jobs: - name: Run tests run: | source .venv/bin/activate - pytest tests + pytest . From 52dc431e578730e0dd425c643fc0bb53a4370836 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Fri, 20 Sep 2024 09:06:49 +0200 Subject: [PATCH 007/161] Restrict github action checkqc lint to changes in path --- .github/workflows/checkqc_lint.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/checkqc_lint.yml b/.github/workflows/checkqc_lint.yml index 690db146..94d80dc9 100644 --- a/.github/workflows/checkqc_lint.yml +++ b/.github/workflows/checkqc_lint.yml @@ -1,5 +1,8 @@ name: CheckQC Lint -on: pull_request +on: + pull_request: + paths: CheckQC/** + jobs: ruff: runs-on: ubuntu-latest From 64c138101654908bf3b02a20efc6f7397165fb4a Mon Sep 17 00:00:00 2001 From: ellendejong Date: Fri, 20 Sep 2024 09:06:57 +0200 Subject: [PATCH 008/161] Add github workflow gendercheck test and lint --- .github/workflows/gendercheck_lint.yml | 11 +++++ .github/workflows/gendercheck_test.yml | 60 ++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 .github/workflows/gendercheck_lint.yml create mode 100644 .github/workflows/gendercheck_test.yml diff --git a/.github/workflows/gendercheck_lint.yml b/.github/workflows/gendercheck_lint.yml new file mode 100644 index 00000000..b1c7d3bf --- /dev/null +++ b/.github/workflows/gendercheck_lint.yml @@ -0,0 +1,11 @@ +name: GenderCheck Lint +on: + pull_request: + paths: GenderCheck/** + +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: chartboost/ruff-action@v1 diff --git a/.github/workflows/gendercheck_test.yml b/.github/workflows/gendercheck_test.yml new file mode 100644 index 00000000..374644fa --- /dev/null +++ b/.github/workflows/gendercheck_test.yml @@ -0,0 +1,60 @@ +# Source: https://github.com/marketplace/actions/install-poetry-action +name: GenderCheck Test +on: + pull_request: + paths: GenderCheck/** + +jobs: + pytest: + runs-on: ubuntu-latest + defaults: + run: + working-directory: GenderCheck/ + steps: + #---------------------------------------------- + # check-out repo and set-up python + #---------------------------------------------- + - name: Check out repository + uses: actions/checkout@v4 + - name: Set up python + id: setup-python + uses: actions/setup-python@v5 + with: + python-version: '3.11.5' + #---------------------------------------------- + # install & configure poetry + #---------------------------------------------- + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + + #---------------------------------------------- + # load cached venv if cache exists + #---------------------------------------------- + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v4 + with: + path: .venv + key: venv_gendercheck-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + #---------------------------------------------- + # install dependencies if cache does not exist + #---------------------------------------------- + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + #---------------------------------------------- + # install root project + #---------------------------------------------- + - name: Install project + run: poetry install --no-interaction + #---------------------------------------------- + # run pytest + #---------------------------------------------- + - name: Run tests + run: | + source .venv/bin/activate + pytest . From 6b836e8dbcfa623d64a6b099b17cbb59dabc9c32 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Fri, 20 Sep 2024 09:42:16 +0200 Subject: [PATCH 009/161] add pyproject.toml and poetry.lock --- GenderCheck/poetry.lock | 124 +++++++++++++++++++++++++++++++++++++ GenderCheck/pyproject.toml | 24 +++++++ 2 files changed, 148 insertions(+) create mode 100644 GenderCheck/poetry.lock create mode 100644 GenderCheck/pyproject.toml diff --git a/GenderCheck/poetry.lock b/GenderCheck/poetry.lock new file mode 100644 index 00000000..35868e09 --- /dev/null +++ b/GenderCheck/poetry.lock @@ -0,0 +1,124 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pysam" +version = "0.22.0" +description = "Package for reading, manipulating, and writing genomic data" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pysam-0.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:116278a7caa122b2b8acc56d13b3599be9b1236f27a12488bffc306858ff0d57"}, + {file = "pysam-0.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da2f1af461e44d5c2c7210d458ee216f8ab98486adf1eea6c88eea5c1058a62f"}, + {file = "pysam-0.22.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:021fbf6874ad998aba19be33828ad9d23d52273643793488ac4b12917d714c68"}, + {file = "pysam-0.22.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:26199e403855b9da45341d25682e0df27013687d9cb1b4fd328136fbd506292b"}, + {file = "pysam-0.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9bfebf89b1dc2ff6f88d64b5f05d8630deb89562b22764f8ee7f6fa9e677bb91"}, + {file = "pysam-0.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:942dd4a2263996bc2daa21200886e9fde027f32ce8820e7832b20bbdb97eb393"}, + {file = "pysam-0.22.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:83776ba587eb9575a209efed1cedb49d69c5fa6cc520dd722a0a09d0bb4e9b87"}, + {file = "pysam-0.22.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:4779a99d1ece17a98724d87a5c10c455cf212b3baa3a8399d3d072e4d0ae5ba0"}, + {file = "pysam-0.22.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bb61bf30c15f6767403b423b04c293e96fd7635457b506c849aafcf48fc13242"}, + {file = "pysam-0.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:32042e0bf3c5dd8554769442c2e1f7b6ada902c33ee44c616d0403e7acd12ee3"}, + {file = "pysam-0.22.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f23b2f47528b94e8abe3b700103fb1214c623ae1c1b8125ecf22d4d33d76720f"}, + {file = "pysam-0.22.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cfd2b858c7405cf38c730cba779ddf9f8cff28b4842c6440e64781650dcb9a52"}, + {file = "pysam-0.22.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:87dbf72f3e61fd6d3f92b1b683d9a9e797b6cc213ffcd971899f24a16f9f6e8f"}, + {file = "pysam-0.22.0-cp36-cp36m-manylinux_2_28_aarch64.whl", hash = "sha256:9af1cd3d07fd4c84e9b3d8a46c65b25f95278185bc6d44c4a48951679d5189ac"}, + {file = "pysam-0.22.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:f73d7923c89618fb7024875ed8eddc5fb0c911f430e3495de482fcee48143e45"}, + {file = "pysam-0.22.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6ffe5c98725fea54b1b2aa8f14a60ee9ceaed32c04460d1b861a62603dcd7153"}, + {file = "pysam-0.22.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:34f5653a82138d28a8e86205785a0398eb6c89f776b4145ff42783168757323c"}, + {file = "pysam-0.22.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:9d3ebb1515c2fd9b11823469e5b211ca3cc89e976c00c284a2190804c9f11726"}, + {file = "pysam-0.22.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b8e18520e7a79bad91b44cf9199c7fa42cec5c3020024d7ef9a7161d0099bf8"}, + {file = "pysam-0.22.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a98d1ddca64943f3ead507721e52466aea2f7303e549d4960a2eb1d9fff8e3d7"}, + {file = "pysam-0.22.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:6d6aa2346b11ad35e88c65eb0067321318c25c7f35f75c98061173eabefcf8b0"}, + {file = "pysam-0.22.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:4f6657a09c81333adb5545cf9a20d4c2ca1686acf8609ad58f13b3ec1b52a9cf"}, + {file = "pysam-0.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:93eb12be3822fb387e5438811f62a0f5e56c1edd5c830aaa316fb50d3d0bc181"}, + {file = "pysam-0.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9ba53f9b0b2c5cb57908855cdb35a31b34c5211d215aa01bdb3e9b3d05c659cc"}, + {file = "pysam-0.22.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:1b84f99aa04e30bd1cc35c01bd41c2b7680131f56c71a740805aff8086f24b56"}, + {file = "pysam-0.22.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:481e4efbfbc07b6b92194a005cb9a98006c8378024f41c7b66c58b14f6e77f9c"}, + {file = "pysam-0.22.0.tar.gz", hash = "sha256:ab7a46973cf0ab8c6ac327f4c3fb67698d7ccbeef8631a716898c6ba01ef3e45"}, +] + +[[package]] +name = "pytest" +version = "8.0.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, + {file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.3.0,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-datadir" +version = "1.5.0" +description = "pytest plugin for test data directories and files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-datadir-1.5.0.tar.gz", hash = "sha256:1617ed92f9afda0c877e4eac91904b5f779d24ba8f5e438752e3ae39d8d2ee3f"}, + {file = "pytest_datadir-1.5.0-py3-none-any.whl", hash = "sha256:34adf361bcc7b37961bbc1dfa8d25a4829e778bab461703c38a5c50ca9c36dc8"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "f25c942350acba89051a081c7b7a93c1d8ba2e970ee295290d94a6e7b9285398" diff --git a/GenderCheck/pyproject.toml b/GenderCheck/pyproject.toml new file mode 100644 index 00000000..0ef8d2f8 --- /dev/null +++ b/GenderCheck/pyproject.toml @@ -0,0 +1,24 @@ +[tool.poetry] +name = "gendercheck" +version = "0.1.0" +description = "" +authors = ["Your Name "] +license = "MIT" +package-mode = false + +[tool.poetry.dependencies] +python = "^3.11" +iniconfig = "2.0.0" +packaging = "23.2" +pluggy = "1.4.0" +pysam = "0.22.0" +pytest = "8.0.2" +pytest-datadir = "^1.5.0" + +[tool.ruff] +line-length = 127 +indent-width = 4 + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" From b412262c2c6292d97eff4d1d2b9d328750da90d8 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Fri, 20 Sep 2024 09:46:48 +0200 Subject: [PATCH 010/161] Add mosaichunter github actions workflow --- .github/workflows/moisaichunter_lint.yml | 11 ++ .github/workflows/moisaichunter_test.yml | 60 +++++++++++ MosaicHunter/1.0.0/poetry.lock | 124 +++++++++++++++++++++++ MosaicHunter/1.0.0/pyproject.toml | 24 +++++ 4 files changed, 219 insertions(+) create mode 100644 .github/workflows/moisaichunter_lint.yml create mode 100644 .github/workflows/moisaichunter_test.yml create mode 100644 MosaicHunter/1.0.0/poetry.lock create mode 100644 MosaicHunter/1.0.0/pyproject.toml diff --git a/.github/workflows/moisaichunter_lint.yml b/.github/workflows/moisaichunter_lint.yml new file mode 100644 index 00000000..f12ea7d9 --- /dev/null +++ b/.github/workflows/moisaichunter_lint.yml @@ -0,0 +1,11 @@ +name: MosaicHunter Lint +on: + pull_request: + paths: MosaicHunter/** + +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: chartboost/ruff-action@v1 diff --git a/.github/workflows/moisaichunter_test.yml b/.github/workflows/moisaichunter_test.yml new file mode 100644 index 00000000..d94fd59c --- /dev/null +++ b/.github/workflows/moisaichunter_test.yml @@ -0,0 +1,60 @@ +# Source: https://github.com/marketplace/actions/install-poetry-action +name: MosaicHunter Test +on: + pull_request: + paths: MosaicHunter/** + +jobs: + pytest: + runs-on: ubuntu-latest + defaults: + run: + working-directory: MosaicHunter/1.0.0/ + steps: + #---------------------------------------------- + # check-out repo and set-up python + #---------------------------------------------- + - name: Check out repository + uses: actions/checkout@v4 + - name: Set up python + id: setup-python + uses: actions/setup-python@v5 + with: + python-version: '3.11.5' + #---------------------------------------------- + # install & configure poetry + #---------------------------------------------- + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + + #---------------------------------------------- + # load cached venv if cache exists + #---------------------------------------------- + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v4 + with: + path: .venv + key: venv_mosaichunter-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + #---------------------------------------------- + # install dependencies if cache does not exist + #---------------------------------------------- + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + #---------------------------------------------- + # install root project + #---------------------------------------------- + - name: Install project + run: poetry install --no-interaction + #---------------------------------------------- + # run pytest + #---------------------------------------------- + - name: Run tests + run: | + source .venv/bin/activate + pytest . diff --git a/MosaicHunter/1.0.0/poetry.lock b/MosaicHunter/1.0.0/poetry.lock new file mode 100644 index 00000000..35868e09 --- /dev/null +++ b/MosaicHunter/1.0.0/poetry.lock @@ -0,0 +1,124 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pysam" +version = "0.22.0" +description = "Package for reading, manipulating, and writing genomic data" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pysam-0.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:116278a7caa122b2b8acc56d13b3599be9b1236f27a12488bffc306858ff0d57"}, + {file = "pysam-0.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da2f1af461e44d5c2c7210d458ee216f8ab98486adf1eea6c88eea5c1058a62f"}, + {file = "pysam-0.22.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:021fbf6874ad998aba19be33828ad9d23d52273643793488ac4b12917d714c68"}, + {file = "pysam-0.22.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:26199e403855b9da45341d25682e0df27013687d9cb1b4fd328136fbd506292b"}, + {file = "pysam-0.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9bfebf89b1dc2ff6f88d64b5f05d8630deb89562b22764f8ee7f6fa9e677bb91"}, + {file = "pysam-0.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:942dd4a2263996bc2daa21200886e9fde027f32ce8820e7832b20bbdb97eb393"}, + {file = "pysam-0.22.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:83776ba587eb9575a209efed1cedb49d69c5fa6cc520dd722a0a09d0bb4e9b87"}, + {file = "pysam-0.22.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:4779a99d1ece17a98724d87a5c10c455cf212b3baa3a8399d3d072e4d0ae5ba0"}, + {file = "pysam-0.22.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bb61bf30c15f6767403b423b04c293e96fd7635457b506c849aafcf48fc13242"}, + {file = "pysam-0.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:32042e0bf3c5dd8554769442c2e1f7b6ada902c33ee44c616d0403e7acd12ee3"}, + {file = "pysam-0.22.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f23b2f47528b94e8abe3b700103fb1214c623ae1c1b8125ecf22d4d33d76720f"}, + {file = "pysam-0.22.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cfd2b858c7405cf38c730cba779ddf9f8cff28b4842c6440e64781650dcb9a52"}, + {file = "pysam-0.22.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:87dbf72f3e61fd6d3f92b1b683d9a9e797b6cc213ffcd971899f24a16f9f6e8f"}, + {file = "pysam-0.22.0-cp36-cp36m-manylinux_2_28_aarch64.whl", hash = "sha256:9af1cd3d07fd4c84e9b3d8a46c65b25f95278185bc6d44c4a48951679d5189ac"}, + {file = "pysam-0.22.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:f73d7923c89618fb7024875ed8eddc5fb0c911f430e3495de482fcee48143e45"}, + {file = "pysam-0.22.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6ffe5c98725fea54b1b2aa8f14a60ee9ceaed32c04460d1b861a62603dcd7153"}, + {file = "pysam-0.22.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:34f5653a82138d28a8e86205785a0398eb6c89f776b4145ff42783168757323c"}, + {file = "pysam-0.22.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:9d3ebb1515c2fd9b11823469e5b211ca3cc89e976c00c284a2190804c9f11726"}, + {file = "pysam-0.22.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b8e18520e7a79bad91b44cf9199c7fa42cec5c3020024d7ef9a7161d0099bf8"}, + {file = "pysam-0.22.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a98d1ddca64943f3ead507721e52466aea2f7303e549d4960a2eb1d9fff8e3d7"}, + {file = "pysam-0.22.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:6d6aa2346b11ad35e88c65eb0067321318c25c7f35f75c98061173eabefcf8b0"}, + {file = "pysam-0.22.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:4f6657a09c81333adb5545cf9a20d4c2ca1686acf8609ad58f13b3ec1b52a9cf"}, + {file = "pysam-0.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:93eb12be3822fb387e5438811f62a0f5e56c1edd5c830aaa316fb50d3d0bc181"}, + {file = "pysam-0.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9ba53f9b0b2c5cb57908855cdb35a31b34c5211d215aa01bdb3e9b3d05c659cc"}, + {file = "pysam-0.22.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:1b84f99aa04e30bd1cc35c01bd41c2b7680131f56c71a740805aff8086f24b56"}, + {file = "pysam-0.22.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:481e4efbfbc07b6b92194a005cb9a98006c8378024f41c7b66c58b14f6e77f9c"}, + {file = "pysam-0.22.0.tar.gz", hash = "sha256:ab7a46973cf0ab8c6ac327f4c3fb67698d7ccbeef8631a716898c6ba01ef3e45"}, +] + +[[package]] +name = "pytest" +version = "8.0.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, + {file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.3.0,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-datadir" +version = "1.5.0" +description = "pytest plugin for test data directories and files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-datadir-1.5.0.tar.gz", hash = "sha256:1617ed92f9afda0c877e4eac91904b5f779d24ba8f5e438752e3ae39d8d2ee3f"}, + {file = "pytest_datadir-1.5.0-py3-none-any.whl", hash = "sha256:34adf361bcc7b37961bbc1dfa8d25a4829e778bab461703c38a5c50ca9c36dc8"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "f25c942350acba89051a081c7b7a93c1d8ba2e970ee295290d94a6e7b9285398" diff --git a/MosaicHunter/1.0.0/pyproject.toml b/MosaicHunter/1.0.0/pyproject.toml new file mode 100644 index 00000000..7cda6f92 --- /dev/null +++ b/MosaicHunter/1.0.0/pyproject.toml @@ -0,0 +1,24 @@ +[tool.poetry] +name = "mosaichunter" +version = "1.0.0" +description = "" +authors = ["Your Name "] +license = "MIT" +package-mode = false + +[tool.poetry.dependencies] +python = "^3.11" +iniconfig = "2.0.0" +packaging = "23.2" +pluggy = "1.4.0" +pysam = "0.22.0" +pytest = "8.0.2" +pytest-datadir = "^1.5.0" + +[tool.ruff] +line-length = 127 +indent-width = 4 + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" From 496bf9fe9caad3a7ac3b4d76e5393ca16ef2d961 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Fri, 27 Sep 2024 10:50:34 +0200 Subject: [PATCH 011/161] github workflows add on push --- .github/workflows/checkqc_lint.yml | 2 ++ .github/workflows/checkqc_test.yml | 2 ++ .github/workflows/gendercheck_lint.yml | 2 ++ .github/workflows/moisaichunter_lint.yml | 2 ++ .github/workflows/moisaichunter_test.yml | 2 ++ 5 files changed, 10 insertions(+) diff --git a/.github/workflows/checkqc_lint.yml b/.github/workflows/checkqc_lint.yml index 94d80dc9..95ed78a1 100644 --- a/.github/workflows/checkqc_lint.yml +++ b/.github/workflows/checkqc_lint.yml @@ -2,6 +2,8 @@ name: CheckQC Lint on: pull_request: paths: CheckQC/** + push: + branches: [master, develop] jobs: ruff: diff --git a/.github/workflows/checkqc_test.yml b/.github/workflows/checkqc_test.yml index 6e48cc6d..928029b9 100644 --- a/.github/workflows/checkqc_test.yml +++ b/.github/workflows/checkqc_test.yml @@ -3,6 +3,8 @@ name: CheckQC Test on: pull_request: paths: CheckQC/** + push: + branches: [master, develop] jobs: pytest: diff --git a/.github/workflows/gendercheck_lint.yml b/.github/workflows/gendercheck_lint.yml index b1c7d3bf..6beaabf9 100644 --- a/.github/workflows/gendercheck_lint.yml +++ b/.github/workflows/gendercheck_lint.yml @@ -2,6 +2,8 @@ name: GenderCheck Lint on: pull_request: paths: GenderCheck/** + push: + branches: [master, develop] jobs: ruff: diff --git a/.github/workflows/moisaichunter_lint.yml b/.github/workflows/moisaichunter_lint.yml index f12ea7d9..3a014ad2 100644 --- a/.github/workflows/moisaichunter_lint.yml +++ b/.github/workflows/moisaichunter_lint.yml @@ -2,6 +2,8 @@ name: MosaicHunter Lint on: pull_request: paths: MosaicHunter/** + push: + branches: [master, develop] jobs: ruff: diff --git a/.github/workflows/moisaichunter_test.yml b/.github/workflows/moisaichunter_test.yml index d94fd59c..00f2548d 100644 --- a/.github/workflows/moisaichunter_test.yml +++ b/.github/workflows/moisaichunter_test.yml @@ -3,6 +3,8 @@ name: MosaicHunter Test on: pull_request: paths: MosaicHunter/** + push: + branches: [master, develop] jobs: pytest: From 4c678bba1828b54dcd37dcf62e8f1b9e96bbd989 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Fri, 27 Sep 2024 11:12:21 +0200 Subject: [PATCH 012/161] Add poetry group dev dependencies and update conflicting package pluggy --- CheckQC/poetry.lock | 8 ++++---- CheckQC/pyproject.toml | 1 - GenderCheck/poetry.lock | 18 +++++++++--------- GenderCheck/pyproject.toml | 6 ++++-- MosaicHunter/1.0.0/poetry.lock | 8 ++++---- MosaicHunter/1.0.0/pyproject.toml | 4 +++- 6 files changed, 24 insertions(+), 21 deletions(-) diff --git a/CheckQC/poetry.lock b/CheckQC/poetry.lock index 8b109204..02c3676f 100644 --- a/CheckQC/poetry.lock +++ b/CheckQC/poetry.lock @@ -616,16 +616,16 @@ files = [ [[package]] name = "tzdata" -version = "2024.1" +version = "2024.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, - {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, ] [metadata] lock-version = "2.0" python-versions = "^3.11.4" -content-hash = "b9545131c195e37f949ffff6f6a065464f501b7922094a291b51a93cfc521886" +content-hash = "77b250569def1ef2cca5b0540b0860096cf89b8a35737341f75c9a00f3162b9f" diff --git a/CheckQC/pyproject.toml b/CheckQC/pyproject.toml index c7e7591e..ea711c42 100644 --- a/CheckQC/pyproject.toml +++ b/CheckQC/pyproject.toml @@ -10,7 +10,6 @@ package-mode = false python = "^3.11.4" pandas = "2.1.4" pyyaml = "6.0.1" -pytest = "^8.3.3" [tool.poetry.group.dev.dependencies] pytest = "^8.3.3" diff --git a/GenderCheck/poetry.lock b/GenderCheck/poetry.lock index 35868e09..7220270b 100644 --- a/GenderCheck/poetry.lock +++ b/GenderCheck/poetry.lock @@ -35,13 +35,13 @@ files = [ [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] @@ -86,23 +86,23 @@ files = [ [[package]] name = "pytest" -version = "8.0.2" +version = "8.3.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, - {file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.3.0,<2.0" +pluggy = ">=1.5,<2" [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-datadir" @@ -121,4 +121,4 @@ pytest = ">=5.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "f25c942350acba89051a081c7b7a93c1d8ba2e970ee295290d94a6e7b9285398" +content-hash = "9d44fccb4bb8753f5ac3dff1ee21c05e4475867600ce65d0c6809bc7d3628302" diff --git a/GenderCheck/pyproject.toml b/GenderCheck/pyproject.toml index 0ef8d2f8..3c878383 100644 --- a/GenderCheck/pyproject.toml +++ b/GenderCheck/pyproject.toml @@ -10,9 +10,11 @@ package-mode = false python = "^3.11" iniconfig = "2.0.0" packaging = "23.2" -pluggy = "1.4.0" +pluggy = "^1.5.0" pysam = "0.22.0" -pytest = "8.0.2" + +[tool.poetry.group.dev.dependencies] +pytest = "^8.3.3" pytest-datadir = "^1.5.0" [tool.ruff] diff --git a/MosaicHunter/1.0.0/poetry.lock b/MosaicHunter/1.0.0/poetry.lock index 35868e09..cfb23f6a 100644 --- a/MosaicHunter/1.0.0/poetry.lock +++ b/MosaicHunter/1.0.0/poetry.lock @@ -35,13 +35,13 @@ files = [ [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] @@ -121,4 +121,4 @@ pytest = ">=5.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "f25c942350acba89051a081c7b7a93c1d8ba2e970ee295290d94a6e7b9285398" +content-hash = "d2dc5a03871e9ede54e5fc47a1cb8cc3f1adefd93e3f4b354b4871cbdc60e08d" diff --git a/MosaicHunter/1.0.0/pyproject.toml b/MosaicHunter/1.0.0/pyproject.toml index 7cda6f92..f98b9a77 100644 --- a/MosaicHunter/1.0.0/pyproject.toml +++ b/MosaicHunter/1.0.0/pyproject.toml @@ -10,8 +10,10 @@ package-mode = false python = "^3.11" iniconfig = "2.0.0" packaging = "23.2" -pluggy = "1.4.0" +pluggy = "1.5.0" pysam = "0.22.0" + +[tool.poetry.group.dev.dependencies] pytest = "8.0.2" pytest-datadir = "^1.5.0" From 1705db193a5717c7e7727b8eb76b22cfd23030e8 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Fri, 27 Sep 2024 11:16:26 +0200 Subject: [PATCH 013/161] add working dir ruff --- .github/workflows/checkqc_lint.yml | 3 +++ .github/workflows/gendercheck_lint.yml | 3 +++ .github/workflows/moisaichunter_lint.yml | 3 +++ 3 files changed, 9 insertions(+) diff --git a/.github/workflows/checkqc_lint.yml b/.github/workflows/checkqc_lint.yml index 95ed78a1..3391620f 100644 --- a/.github/workflows/checkqc_lint.yml +++ b/.github/workflows/checkqc_lint.yml @@ -8,6 +8,9 @@ on: jobs: ruff: runs-on: ubuntu-latest + defaults: + run: + working-directory: CheckQC/ steps: - uses: actions/checkout@v4 - uses: chartboost/ruff-action@v1 diff --git a/.github/workflows/gendercheck_lint.yml b/.github/workflows/gendercheck_lint.yml index 6beaabf9..7586f843 100644 --- a/.github/workflows/gendercheck_lint.yml +++ b/.github/workflows/gendercheck_lint.yml @@ -8,6 +8,9 @@ on: jobs: ruff: runs-on: ubuntu-latest + defaults: + run: + working-directory: GenderCheck/ steps: - uses: actions/checkout@v4 - uses: chartboost/ruff-action@v1 diff --git a/.github/workflows/moisaichunter_lint.yml b/.github/workflows/moisaichunter_lint.yml index 3a014ad2..fb6c0f97 100644 --- a/.github/workflows/moisaichunter_lint.yml +++ b/.github/workflows/moisaichunter_lint.yml @@ -8,6 +8,9 @@ on: jobs: ruff: runs-on: ubuntu-latest + defaults: + run: + working-directory: MosaicHunter/1.0.0// steps: - uses: actions/checkout@v4 - uses: chartboost/ruff-action@v1 From cef72724f2cdcc3c619576c7f1fa86444dd84b32 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Fri, 27 Sep 2024 11:17:19 +0200 Subject: [PATCH 014/161] fix incorrect type to array in filter paths --- .github/workflows/checkqc_lint.yml | 2 +- .github/workflows/checkqc_test.yml | 2 +- .github/workflows/gendercheck_lint.yml | 2 +- .github/workflows/gendercheck_test.yml | 3 ++- .github/workflows/moisaichunter_lint.yml | 2 +- .github/workflows/moisaichunter_test.yml | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/checkqc_lint.yml b/.github/workflows/checkqc_lint.yml index 3391620f..fcd49e90 100644 --- a/.github/workflows/checkqc_lint.yml +++ b/.github/workflows/checkqc_lint.yml @@ -1,7 +1,7 @@ name: CheckQC Lint on: pull_request: - paths: CheckQC/** + paths: [CheckQC/**] push: branches: [master, develop] diff --git a/.github/workflows/checkqc_test.yml b/.github/workflows/checkqc_test.yml index 928029b9..2ab91335 100644 --- a/.github/workflows/checkqc_test.yml +++ b/.github/workflows/checkqc_test.yml @@ -2,7 +2,7 @@ name: CheckQC Test on: pull_request: - paths: CheckQC/** + paths: [CheckQC/**] push: branches: [master, develop] diff --git a/.github/workflows/gendercheck_lint.yml b/.github/workflows/gendercheck_lint.yml index 7586f843..cece6085 100644 --- a/.github/workflows/gendercheck_lint.yml +++ b/.github/workflows/gendercheck_lint.yml @@ -1,7 +1,7 @@ name: GenderCheck Lint on: pull_request: - paths: GenderCheck/** + paths: [GenderCheck/**] push: branches: [master, develop] diff --git a/.github/workflows/gendercheck_test.yml b/.github/workflows/gendercheck_test.yml index 374644fa..4f3c700c 100644 --- a/.github/workflows/gendercheck_test.yml +++ b/.github/workflows/gendercheck_test.yml @@ -2,7 +2,8 @@ name: GenderCheck Test on: pull_request: - paths: GenderCheck/** + paths: [GenderCheck/**] + push: jobs: pytest: diff --git a/.github/workflows/moisaichunter_lint.yml b/.github/workflows/moisaichunter_lint.yml index fb6c0f97..5742c9d1 100644 --- a/.github/workflows/moisaichunter_lint.yml +++ b/.github/workflows/moisaichunter_lint.yml @@ -1,7 +1,7 @@ name: MosaicHunter Lint on: pull_request: - paths: MosaicHunter/** + paths: [MosaicHunter/**] push: branches: [master, develop] diff --git a/.github/workflows/moisaichunter_test.yml b/.github/workflows/moisaichunter_test.yml index 00f2548d..c88aab02 100644 --- a/.github/workflows/moisaichunter_test.yml +++ b/.github/workflows/moisaichunter_test.yml @@ -2,7 +2,7 @@ name: MosaicHunter Test on: pull_request: - paths: MosaicHunter/** + paths: [MosaicHunter/**] push: branches: [master, develop] From 56162853a72aab2a02fe91831587b0ac4be22c9f Mon Sep 17 00:00:00 2001 From: ellendejong Date: Fri, 27 Sep 2024 11:17:29 +0200 Subject: [PATCH 015/161] add on push --- .github/workflows/gendercheck_test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/gendercheck_test.yml b/.github/workflows/gendercheck_test.yml index 4f3c700c..f92821a0 100644 --- a/.github/workflows/gendercheck_test.yml +++ b/.github/workflows/gendercheck_test.yml @@ -4,6 +4,7 @@ on: pull_request: paths: [GenderCheck/**] push: + branches: [master, develop] jobs: pytest: From 32b0a232fd942e48e6efe9c03208b81ec42151e6 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Fri, 27 Sep 2024 11:17:43 +0200 Subject: [PATCH 016/161] fix regex string --- Utils/create_hsmetrics_summary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Utils/create_hsmetrics_summary.py b/Utils/create_hsmetrics_summary.py index 4256fc33..582b61e4 100644 --- a/Utils/create_hsmetrics_summary.py +++ b/Utils/create_hsmetrics_summary.py @@ -8,7 +8,7 @@ parser.add_argument('hsmetrics_files', type=argparse.FileType('r'), nargs='*', help='HSMetric file') arguments = parser.parse_args() - interval_files_pattern = re.compile("BAIT_INTERVALS=\[(\S*)\].TARGET_INTERVALS=\[(\S*)\]") # noqa: W605 + interval_files_pattern = re.compile(r"BAIT_INTERVALS=\[(\S*)\].TARGET_INTERVALS=\[(\S*)\]") summary_header = [] summary_data = {} for hsmetrics_file in arguments.hsmetrics_files: From 27b913146d9d162402f7e61725c4e14f2075fbba Mon Sep 17 00:00:00 2001 From: ellendejong Date: Fri, 27 Sep 2024 11:46:19 +0200 Subject: [PATCH 017/161] add pre-commit ruff --- .pre-commit-config.yaml | 11 +++ poetry.lock | 181 +++++++++++++++++++++++++++++++++++++++- pyproject.toml | 3 + 3 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..8f4b8b59 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.5.1 + hooks: + # Run the linter. + - id: ruff + # Run the formatter. + - id: ruff-format diff --git a/poetry.lock b/poetry.lock index d886a2be..0e005192 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,16 @@ # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + [[package]] name = "colorama" version = "0.4.6" @@ -11,6 +22,47 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "filelock" +version = "3.16.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +typing = ["typing-extensions (>=4.12.2)"] + +[[package]] +name = "identify" +version = "2.6.1" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, + {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, +] + +[package.extras] +license = ["ukkonen"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -22,6 +74,17 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + [[package]] name = "packaging" version = "24.1" @@ -33,6 +96,22 @@ files = [ {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + [[package]] name = "pluggy" version = "1.5.0" @@ -48,6 +127,24 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pre-commit" +version = "3.8.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, + {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + [[package]] name = "pytest" version = "8.3.3" @@ -68,7 +165,89 @@ pluggy = ">=1.5,<2" [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "virtualenv" +version = "20.26.5" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.26.5-py3-none-any.whl", hash = "sha256:4f3ac17b81fba3ce3bd6f4ead2749a72da5929c01774948e243db9ba41df4ff6"}, + {file = "virtualenv-20.26.5.tar.gz", hash = "sha256:ce489cac131aa58f4b25e321d6d186171f78e6cb13fafbf32a840cee67733ff4"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + [metadata] lock-version = "2.0" python-versions = "^3.11.4" -content-hash = "0e9c7de1a14ab89ba216b00deb52b83bda978915d965f459b3bfce8f2b6f5d77" +content-hash = "aa9362db1f4605a8d183a481eed9b3763b2a3382b38f9ed4a3989ee82a70c1c6" diff --git a/pyproject.toml b/pyproject.toml index a70546a1..a2943cf7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,9 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.11.4" + +[tool.poetry.group.dev.dependencies] +pre-commit = "^3.8.0" pytest = "^8.3.3" [tool.ruff] From 6e4c3dc34b4e8504b4504f3176134b552076f372 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Fri, 27 Sep 2024 11:49:03 +0200 Subject: [PATCH 018/161] Add authors --- CheckQC/pyproject.toml | 2 +- GenderCheck/pyproject.toml | 2 +- MosaicHunter/1.0.0/pyproject.toml | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CheckQC/pyproject.toml b/CheckQC/pyproject.toml index ea711c42..402d9e6a 100644 --- a/CheckQC/pyproject.toml +++ b/CheckQC/pyproject.toml @@ -2,7 +2,7 @@ name = "checkqc" version = "0.1.0" description = "Check and judge qc metrics (files)" -authors = ["Your Name "] +authors = ["Bioinformatica Genetica "] license = 'MIT' package-mode = false diff --git a/GenderCheck/pyproject.toml b/GenderCheck/pyproject.toml index 3c878383..9a4d458b 100644 --- a/GenderCheck/pyproject.toml +++ b/GenderCheck/pyproject.toml @@ -2,7 +2,7 @@ name = "gendercheck" version = "0.1.0" description = "" -authors = ["Your Name "] +authors = ["Bioinformatica Genetica "] license = "MIT" package-mode = false diff --git a/MosaicHunter/1.0.0/pyproject.toml b/MosaicHunter/1.0.0/pyproject.toml index f98b9a77..7b74836a 100644 --- a/MosaicHunter/1.0.0/pyproject.toml +++ b/MosaicHunter/1.0.0/pyproject.toml @@ -2,7 +2,7 @@ name = "mosaichunter" version = "1.0.0" description = "" -authors = ["Your Name "] +authors = ["Bioinformatica Genetica "] license = "MIT" package-mode = false diff --git a/pyproject.toml b/pyproject.toml index a2943cf7..67a3fbde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "custommodules" version = "0.1.0" description = "custom nextflow processes and their linked files" -authors = ["Your Name "] +authors = ["Bioinformatica Genetica "] license = "MIT" readme = "README.md" From 940044c15725f56b270e8b9bc53d813836db1164 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Mon, 30 Sep 2024 08:12:38 +0200 Subject: [PATCH 019/161] filter paths on push --- .github/workflows/checkqc_lint.yml | 1 + .github/workflows/checkqc_test.yml | 1 + .github/workflows/gendercheck_lint.yml | 1 + .github/workflows/gendercheck_test.yml | 1 + .github/workflows/moisaichunter_lint.yml | 1 + .github/workflows/moisaichunter_test.yml | 1 + 6 files changed, 6 insertions(+) diff --git a/.github/workflows/checkqc_lint.yml b/.github/workflows/checkqc_lint.yml index fcd49e90..26629c52 100644 --- a/.github/workflows/checkqc_lint.yml +++ b/.github/workflows/checkqc_lint.yml @@ -3,6 +3,7 @@ on: pull_request: paths: [CheckQC/**] push: + paths: [CheckQC/**] branches: [master, develop] jobs: diff --git a/.github/workflows/checkqc_test.yml b/.github/workflows/checkqc_test.yml index 2ab91335..7073f490 100644 --- a/.github/workflows/checkqc_test.yml +++ b/.github/workflows/checkqc_test.yml @@ -4,6 +4,7 @@ on: pull_request: paths: [CheckQC/**] push: + paths: [CheckQC/**] branches: [master, develop] jobs: diff --git a/.github/workflows/gendercheck_lint.yml b/.github/workflows/gendercheck_lint.yml index cece6085..e8480614 100644 --- a/.github/workflows/gendercheck_lint.yml +++ b/.github/workflows/gendercheck_lint.yml @@ -3,6 +3,7 @@ on: pull_request: paths: [GenderCheck/**] push: + paths: [GenderCheck/**] branches: [master, develop] jobs: diff --git a/.github/workflows/gendercheck_test.yml b/.github/workflows/gendercheck_test.yml index f92821a0..4ff1d829 100644 --- a/.github/workflows/gendercheck_test.yml +++ b/.github/workflows/gendercheck_test.yml @@ -4,6 +4,7 @@ on: pull_request: paths: [GenderCheck/**] push: + paths: [GenderCheck/**] branches: [master, develop] jobs: diff --git a/.github/workflows/moisaichunter_lint.yml b/.github/workflows/moisaichunter_lint.yml index 5742c9d1..96d6c074 100644 --- a/.github/workflows/moisaichunter_lint.yml +++ b/.github/workflows/moisaichunter_lint.yml @@ -3,6 +3,7 @@ on: pull_request: paths: [MosaicHunter/**] push: + paths: [MosaicHunter/**] branches: [master, develop] jobs: diff --git a/.github/workflows/moisaichunter_test.yml b/.github/workflows/moisaichunter_test.yml index c88aab02..44471f32 100644 --- a/.github/workflows/moisaichunter_test.yml +++ b/.github/workflows/moisaichunter_test.yml @@ -4,6 +4,7 @@ on: pull_request: paths: [MosaicHunter/**] push: + paths: [MosaicHunter/**] branches: [master, develop] jobs: From da9f31df4e3d34c5214ce79d8accd162e13bb1a6 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Mon, 30 Sep 2024 08:14:24 +0200 Subject: [PATCH 020/161] move filter paths on push one row down. --- .github/workflows/checkqc_lint.yml | 2 +- .github/workflows/checkqc_test.yml | 2 +- .github/workflows/gendercheck_lint.yml | 2 +- .github/workflows/gendercheck_test.yml | 2 +- .github/workflows/moisaichunter_lint.yml | 2 +- .github/workflows/moisaichunter_test.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/checkqc_lint.yml b/.github/workflows/checkqc_lint.yml index 26629c52..6c153c07 100644 --- a/.github/workflows/checkqc_lint.yml +++ b/.github/workflows/checkqc_lint.yml @@ -3,8 +3,8 @@ on: pull_request: paths: [CheckQC/**] push: - paths: [CheckQC/**] branches: [master, develop] + paths: [CheckQC/**] jobs: ruff: diff --git a/.github/workflows/checkqc_test.yml b/.github/workflows/checkqc_test.yml index 7073f490..fd9b320f 100644 --- a/.github/workflows/checkqc_test.yml +++ b/.github/workflows/checkqc_test.yml @@ -4,8 +4,8 @@ on: pull_request: paths: [CheckQC/**] push: - paths: [CheckQC/**] branches: [master, develop] + paths: [CheckQC/**] jobs: pytest: diff --git a/.github/workflows/gendercheck_lint.yml b/.github/workflows/gendercheck_lint.yml index e8480614..79a11506 100644 --- a/.github/workflows/gendercheck_lint.yml +++ b/.github/workflows/gendercheck_lint.yml @@ -3,8 +3,8 @@ on: pull_request: paths: [GenderCheck/**] push: - paths: [GenderCheck/**] branches: [master, develop] + paths: [GenderCheck/**] jobs: ruff: diff --git a/.github/workflows/gendercheck_test.yml b/.github/workflows/gendercheck_test.yml index 4ff1d829..b5083d45 100644 --- a/.github/workflows/gendercheck_test.yml +++ b/.github/workflows/gendercheck_test.yml @@ -4,8 +4,8 @@ on: pull_request: paths: [GenderCheck/**] push: - paths: [GenderCheck/**] branches: [master, develop] + paths: [GenderCheck/**] jobs: pytest: diff --git a/.github/workflows/moisaichunter_lint.yml b/.github/workflows/moisaichunter_lint.yml index 96d6c074..dd00c088 100644 --- a/.github/workflows/moisaichunter_lint.yml +++ b/.github/workflows/moisaichunter_lint.yml @@ -3,8 +3,8 @@ on: pull_request: paths: [MosaicHunter/**] push: - paths: [MosaicHunter/**] branches: [master, develop] + paths: [MosaicHunter/**] jobs: ruff: diff --git a/.github/workflows/moisaichunter_test.yml b/.github/workflows/moisaichunter_test.yml index 44471f32..f1906d04 100644 --- a/.github/workflows/moisaichunter_test.yml +++ b/.github/workflows/moisaichunter_test.yml @@ -4,8 +4,8 @@ on: pull_request: paths: [MosaicHunter/**] push: - paths: [MosaicHunter/**] branches: [master, develop] + paths: [MosaicHunter/**] jobs: pytest: From 3005cb3e1c73171bbbbcec0be11df7ecb4fdefbd Mon Sep 17 00:00:00 2001 From: ellendejong Date: Fri, 11 Oct 2024 10:25:22 +0200 Subject: [PATCH 021/161] Use poetry in dockerfiles checkqc and gendercheck. --- CheckQC/CheckQC.nf | 4 ++-- CheckQC/Dockerfile | 35 ++++++++++++++++++++++++++++++++--- CheckQC/requirements.txt | 12 ------------ GenderCheck/CompareGender.nf | 2 +- GenderCheck/Dockerfile | 35 ++++++++++++++++++++++++++++++++--- GenderCheck/requirements.txt | 5 ----- 6 files changed, 67 insertions(+), 26 deletions(-) delete mode 100644 CheckQC/requirements.txt delete mode 100644 GenderCheck/requirements.txt diff --git a/CheckQC/CheckQC.nf b/CheckQC/CheckQC.nf index 0dbf4d79..d0b2cddd 100644 --- a/CheckQC/CheckQC.nf +++ b/CheckQC/CheckQC.nf @@ -1,13 +1,13 @@ process CheckQC { tag {"CheckQC ${identifier}"} label 'CheckQC' - container = 'docker.io/umcugenbioinf/checkqc:1.0.0' + container = 'docker.io/umcugenbioinf/checkqc:1.0.1' shell = ['/bin/bash', '-euo', 'pipefail'] input: val(identifier) path(input_files) - + output: path("${identifier}_summary.csv", emit: qc_output) diff --git a/CheckQC/Dockerfile b/CheckQC/Dockerfile index c7763aeb..41b0d18c 100644 --- a/CheckQC/Dockerfile +++ b/CheckQC/Dockerfile @@ -3,9 +3,38 @@ FROM --platform=linux/amd64 python:3.9 ################## METADATA ###################### LABEL base_image="python:3.9" -LABEL version="1.0.0" +LABEL version="1.0.1" LABEL extra.binaries="pandas,PyYAML,pytest" ################## INSTALLATIONS ###################### -COPY requirements.txt requirements.txt -RUN pip install -r requirements.txt \ No newline at end of file +# Use poetry to install virtualenv. +ENV POETRY_VERSION=1.8.3 +ENV POETRY_HOME=/opt/poetry +ENV POETRY_VENV=/opt/poetry-venv + +# Tell Poetry where to place its cache and virtual environment +ENV POETRY_CACHE_DIR=/tmp/poetry_cache + +# Do not ask any interactive question +ENV POETRY_NO_INTERACTION=1 + +# Make poetry create the virtual environment in the project's root +# it gets named `.venv` +ENV POETRY_VIRTUALENVS_IN_PROJECT=1 +ENV POETRY_VIRTUALENVS_CREATE=1 + +# Set virtual_env variable +ENV VIRTUAL_ENV=/.venv +# Prepend virtual environments path +ENV PATH="${VIRTUAL_ENV}/bin:${POETRY_VENV}/bin:${PATH}" + +# Creating a virtual environment just for poetry and install it with pip +RUN python3 -m venv $POETRY_VENV \ + && $POETRY_VENV/bin/pip3 install -U pip3 setuptools \ + && $POETRY_VENV/bin/pip3 install poetry==${POETRY_VERSION} + +# Copy project requirement files here to ensure they will be cached. +COPY pyproject.toml poetry.lock ./ + +# Install dependencies. +RUN --mount=type=cache,target=$POETRY_CACHE_DIR poetry install --without dev --no-interaction --no-cache diff --git a/CheckQC/requirements.txt b/CheckQC/requirements.txt deleted file mode 100644 index 66b6acda..00000000 --- a/CheckQC/requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -pandas==2.1.4 -pytest==6.2.5 -pytest-cov==3.0.0 -pytest-datadir==1.5.0 -pytest-datafiles==2.0 -pytest-dataset==0.3.2 -pytest-flake8==1.0.7 -pytest-mock==3.8.2 -pytest-raises==0.11 -pytest-reqs==0.2.1 -pytest-unordered==0.5.2 -PyYAML==6.0.1 \ No newline at end of file diff --git a/GenderCheck/CompareGender.nf b/GenderCheck/CompareGender.nf index fdfc495e..6bed01e7 100644 --- a/GenderCheck/CompareGender.nf +++ b/GenderCheck/CompareGender.nf @@ -3,7 +3,7 @@ process CompareGender { tag {"CompareGender ${sample_id}"} label 'CompareGender' label 'CompareGender_Pysam' - container = 'ghcr.io/umcugenetics/custommodules_gendercheck:1.0.0' + container = 'ghcr.io/umcugenetics/custommodules_gendercheck:1.0.1' shell = ['/bin/bash', '-eo', 'pipefail'] input: diff --git a/GenderCheck/Dockerfile b/GenderCheck/Dockerfile index 6d4895c8..09b195bc 100644 --- a/GenderCheck/Dockerfile +++ b/GenderCheck/Dockerfile @@ -3,9 +3,38 @@ FROM --platform=linux/amd64 python:3.11 ################## METADATA ###################### LABEL base_image="python:3.11" -LABEL version="1.0.0" +LABEL version="1.0.1" LABEL extra.binaries="pysam,pytest" ################## INSTALLATIONS ###################### -COPY requirements.txt requirements.txt -RUN pip install -r requirements.txt \ No newline at end of file +# Use poetry to install virtualenv. +ENV POETRY_VERSION=1.8.3 +ENV POETRY_HOME=/opt/poetry +ENV POETRY_VENV=/opt/poetry-venv + +# Tell Poetry where to place its cache and virtual environment +ENV POETRY_CACHE_DIR=/tmp/poetry_cache + +# Do not ask any interactive question +ENV POETRY_NO_INTERACTION=1 + +# Make poetry create the virtual environment in the project's root +# it gets named `.venv` +ENV POETRY_VIRTUALENVS_IN_PROJECT=1 +ENV POETRY_VIRTUALENVS_CREATE=1 + +# Set virtual_env variable +ENV VIRTUAL_ENV=/.venv +# Prepend virtual environments path +ENV PATH="${VIRTUAL_ENV}/bin:${POETRY_VENV}/bin:${PATH}" + +# Creating a virtual environment just for poetry and install it with pip +RUN python3 -m venv $POETRY_VENV \ + && $POETRY_VENV/bin/pip3 install -U pip3 setuptools \ + && $POETRY_VENV/bin/pip3 install poetry==${POETRY_VERSION} + +# Copy project requirement files here to ensure they will be cached. +COPY pyproject.toml poetry.lock ./ + +# Install dependencies. +RUN --mount=type=cache,target=$POETRY_CACHE_DIR poetry install --without dev --no-interaction --no-cache diff --git a/GenderCheck/requirements.txt b/GenderCheck/requirements.txt deleted file mode 100644 index 7664a681..00000000 --- a/GenderCheck/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -iniconfig==2.0.0 -packaging==23.2 -pluggy==1.4.0 -pysam==0.22.0 -pytest==8.0.2 \ No newline at end of file From f50de79e6e8cdcc0d965bc0f2e7ab4405415f3fc Mon Sep 17 00:00:00 2001 From: ellendejong Date: Fri, 11 Oct 2024 10:25:54 +0200 Subject: [PATCH 022/161] Remove requirements of MosaicHunter --- MosaicHunter/1.0.0/requirements.txt | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 MosaicHunter/1.0.0/requirements.txt diff --git a/MosaicHunter/1.0.0/requirements.txt b/MosaicHunter/1.0.0/requirements.txt deleted file mode 100644 index 7664a681..00000000 --- a/MosaicHunter/1.0.0/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -iniconfig==2.0.0 -packaging==23.2 -pluggy==1.4.0 -pysam==0.22.0 -pytest==8.0.2 \ No newline at end of file From c5d878dc9b0bcdf862d96d0aca2de328107a2f67 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Fri, 11 Oct 2024 10:32:11 +0200 Subject: [PATCH 023/161] Update container version MosaicHunterGetGender. --- MosaicHunter/1.0.0/MosaicHunter.nf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MosaicHunter/1.0.0/MosaicHunter.nf b/MosaicHunter/1.0.0/MosaicHunter.nf index 3f04dae9..e60c9ff1 100644 --- a/MosaicHunter/1.0.0/MosaicHunter.nf +++ b/MosaicHunter/1.0.0/MosaicHunter.nf @@ -3,7 +3,7 @@ process MosaicHunterGetGender { tag {"MosaicHunterGetGender ${sample_id}"} label 'MosaicHunterGetGender' - container = 'ghcr.io/umcugenetics/custommodules_gendercheck:1.0.0' + container = 'ghcr.io/umcugenetics/custommodules_gendercheck:1.0.1' shell = ['/bin/bash', '-eo', 'pipefail'] /* From 740e504ae22d43dd2178f70522a4fcc4c2102000 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Fri, 11 Oct 2024 10:36:08 +0200 Subject: [PATCH 024/161] Use poetry to run pytest --- .github/workflows/gendercheck_test.yml | 3 +- .github/workflows/moisaichunter_test.yml | 3 +- CheckQC/checkqc_giabeval_settings.yaml | 64 ++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 CheckQC/checkqc_giabeval_settings.yaml diff --git a/.github/workflows/gendercheck_test.yml b/.github/workflows/gendercheck_test.yml index b5083d45..dc3d2794 100644 --- a/.github/workflows/gendercheck_test.yml +++ b/.github/workflows/gendercheck_test.yml @@ -59,5 +59,4 @@ jobs: #---------------------------------------------- - name: Run tests run: | - source .venv/bin/activate - pytest . + poetry run pytest . diff --git a/.github/workflows/moisaichunter_test.yml b/.github/workflows/moisaichunter_test.yml index f1906d04..6cd34679 100644 --- a/.github/workflows/moisaichunter_test.yml +++ b/.github/workflows/moisaichunter_test.yml @@ -59,5 +59,4 @@ jobs: #---------------------------------------------- - name: Run tests run: | - source .venv/bin/activate - pytest . + poetry run pytest . diff --git a/CheckQC/checkqc_giabeval_settings.yaml b/CheckQC/checkqc_giabeval_settings.yaml new file mode 100644 index 00000000..39bf9868 --- /dev/null +++ b/CheckQC/checkqc_giabeval_settings.yaml @@ -0,0 +1,64 @@ +metrics: + - filename: "summary_snp.*truth.txt" + qc_col: "Sensitivity" + threshold: 0.98 + operator: "<=" + report_cols: ["sample_1", "Sensitivity"] + sample_cols: ["sample_1"] + title: "PK_C_SNPs" + - filename: "summary_indel.*truth.txt" + qc_col: "Sensitivity" + threshold: 0.90 + operator: "<=" + report_cols: ["sample_1", "Sensitivity"] + sample_cols: ["sample_1"] + title: "PK_C_INDELs" + - filename: "summary_snp.*truth.txt" + qc_col: "Precision" + threshold: 0.98 + operator: "<=" + report_cols: ["sample_1", "Precision"] + sample_cols: ["sample_1"] + title: "PK_D_SNPs" + - filename: "summary_indel.*truth.txt" + qc_col: "Precision" + threshold: 0.85 + operator: "<=" + report_cols: ["sample_1", "Precision"] + sample_cols: ["sample_1"] + title: "PK_D_INDELs" + - filename: "summary_snp_.*_((?!truth).)*$" + qc_col: "Sensitivity" + threshold: 0.98 + operator: "<=" + report_cols: ["sample_1", "sample_2", "Sensitivity"] + sample_cols: ["sample_1", "sample_2"] + title: "PK_E_SNPs" + - filename: "summary_indel_.*_((?!truth).)*$" + qc_col: "Sensitivity" + threshold: 0.90 + operator: "<=" + report_cols: ["sample_1", "sample_2", "Sensitivity"] + sample_cols: ["sample_1", "sample_2"] + title: "PK_E_INDELs" + - filename: "summary_high_conf_snp.*truth.txt" + qc_col: "Sensitivity" + threshold: 0.98 + operator: "<=" + report_cols: ["sample_1", "Sensitivity"] + sample_cols: ["sample_1"] + title: "PK_C_SNPs_high_conf" + - filename: "summary_high_conf_snp.*truth.txt" + qc_col: "Precision" + threshold: 0.98 + operator: "<=" + report_cols: ["sample_1", "Precision"] + sample_cols: ["sample_1"] + title: "PK_D_SNPs_high_conf" + - filename: "summary_high_conf_snp_.*_((?!truth).)*$" + qc_col: "Sensitivity" + threshold: 0.98 + operator: "<=" + report_cols: ["sample_1", "sample_2", "Sensitivity"] + sample_cols: ["sample_1", "sample_2"] + title: "PK_E_SNPs_high_conf" \ No newline at end of file From 95b42b3bafd064e5f88dcef2a98bd8bc53007c3b Mon Sep 17 00:00:00 2001 From: ellendejong Date: Fri, 11 Oct 2024 11:12:59 +0200 Subject: [PATCH 025/161] Remove setuptools. --- CheckQC/Dockerfile | 1 - GenderCheck/Dockerfile | 1 - 2 files changed, 2 deletions(-) diff --git a/CheckQC/Dockerfile b/CheckQC/Dockerfile index 41b0d18c..1b510b50 100644 --- a/CheckQC/Dockerfile +++ b/CheckQC/Dockerfile @@ -30,7 +30,6 @@ ENV PATH="${VIRTUAL_ENV}/bin:${POETRY_VENV}/bin:${PATH}" # Creating a virtual environment just for poetry and install it with pip RUN python3 -m venv $POETRY_VENV \ - && $POETRY_VENV/bin/pip3 install -U pip3 setuptools \ && $POETRY_VENV/bin/pip3 install poetry==${POETRY_VERSION} # Copy project requirement files here to ensure they will be cached. diff --git a/GenderCheck/Dockerfile b/GenderCheck/Dockerfile index 09b195bc..69bdb4db 100644 --- a/GenderCheck/Dockerfile +++ b/GenderCheck/Dockerfile @@ -30,7 +30,6 @@ ENV PATH="${VIRTUAL_ENV}/bin:${POETRY_VENV}/bin:${PATH}" # Creating a virtual environment just for poetry and install it with pip RUN python3 -m venv $POETRY_VENV \ - && $POETRY_VENV/bin/pip3 install -U pip3 setuptools \ && $POETRY_VENV/bin/pip3 install poetry==${POETRY_VERSION} # Copy project requirement files here to ensure they will be cached. From d1206fbc6f8a3fe8dd990b34f9bded650b6e0029 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Wed, 23 Oct 2024 10:32:58 +0200 Subject: [PATCH 026/161] Revert: checkqc settings yaml should not be stored in github --- CheckQC/checkqc_giabeval_settings.yaml | 64 -------------------------- 1 file changed, 64 deletions(-) delete mode 100644 CheckQC/checkqc_giabeval_settings.yaml diff --git a/CheckQC/checkqc_giabeval_settings.yaml b/CheckQC/checkqc_giabeval_settings.yaml deleted file mode 100644 index 39bf9868..00000000 --- a/CheckQC/checkqc_giabeval_settings.yaml +++ /dev/null @@ -1,64 +0,0 @@ -metrics: - - filename: "summary_snp.*truth.txt" - qc_col: "Sensitivity" - threshold: 0.98 - operator: "<=" - report_cols: ["sample_1", "Sensitivity"] - sample_cols: ["sample_1"] - title: "PK_C_SNPs" - - filename: "summary_indel.*truth.txt" - qc_col: "Sensitivity" - threshold: 0.90 - operator: "<=" - report_cols: ["sample_1", "Sensitivity"] - sample_cols: ["sample_1"] - title: "PK_C_INDELs" - - filename: "summary_snp.*truth.txt" - qc_col: "Precision" - threshold: 0.98 - operator: "<=" - report_cols: ["sample_1", "Precision"] - sample_cols: ["sample_1"] - title: "PK_D_SNPs" - - filename: "summary_indel.*truth.txt" - qc_col: "Precision" - threshold: 0.85 - operator: "<=" - report_cols: ["sample_1", "Precision"] - sample_cols: ["sample_1"] - title: "PK_D_INDELs" - - filename: "summary_snp_.*_((?!truth).)*$" - qc_col: "Sensitivity" - threshold: 0.98 - operator: "<=" - report_cols: ["sample_1", "sample_2", "Sensitivity"] - sample_cols: ["sample_1", "sample_2"] - title: "PK_E_SNPs" - - filename: "summary_indel_.*_((?!truth).)*$" - qc_col: "Sensitivity" - threshold: 0.90 - operator: "<=" - report_cols: ["sample_1", "sample_2", "Sensitivity"] - sample_cols: ["sample_1", "sample_2"] - title: "PK_E_INDELs" - - filename: "summary_high_conf_snp.*truth.txt" - qc_col: "Sensitivity" - threshold: 0.98 - operator: "<=" - report_cols: ["sample_1", "Sensitivity"] - sample_cols: ["sample_1"] - title: "PK_C_SNPs_high_conf" - - filename: "summary_high_conf_snp.*truth.txt" - qc_col: "Precision" - threshold: 0.98 - operator: "<=" - report_cols: ["sample_1", "Precision"] - sample_cols: ["sample_1"] - title: "PK_D_SNPs_high_conf" - - filename: "summary_high_conf_snp_.*_((?!truth).)*$" - qc_col: "Sensitivity" - threshold: 0.98 - operator: "<=" - report_cols: ["sample_1", "sample_2", "Sensitivity"] - sample_cols: ["sample_1", "sample_2"] - title: "PK_E_SNPs_high_conf" \ No newline at end of file From 1c25a908d3632d88c7cefebd8db9f13a44f46216 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Wed, 23 Oct 2024 11:17:34 +0200 Subject: [PATCH 027/161] Remove pytest-reqs. --- CheckQC/poetry.lock | 187 +++++++++++++++-------------------------- CheckQC/pyproject.toml | 1 - 2 files changed, 68 insertions(+), 120 deletions(-) diff --git a/CheckQC/poetry.lock b/CheckQC/poetry.lock index 02c3676f..5222856b 100644 --- a/CheckQC/poetry.lock +++ b/CheckQC/poetry.lock @@ -13,83 +13,73 @@ files = [ [[package]] name = "coverage" -version = "7.6.1" +version = "7.6.4" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, - {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, - {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, - {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, - {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, - {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, - {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, - {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, - {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, - {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, - {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, - {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, - {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, - {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, - {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, - {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, - {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, - {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, + {file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"}, + {file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a"}, + {file = "coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa"}, + {file = "coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172"}, + {file = "coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b"}, + {file = "coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522"}, + {file = "coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf"}, + {file = "coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5"}, + {file = "coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17"}, + {file = "coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a"}, + {file = "coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e"}, + {file = "coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef"}, + {file = "coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e"}, + {file = "coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1"}, + {file = "coverage-7.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3"}, + {file = "coverage-7.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901"}, + {file = "coverage-7.6.4-cp39-cp39-win32.whl", hash = "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09"}, + {file = "coverage-7.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f"}, + {file = "coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e"}, + {file = "coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73"}, ] [package.extras] @@ -256,31 +246,6 @@ sql-other = ["SQLAlchemy (>=1.4.36)"] test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] xml = ["lxml (>=4.8.0)"] -[[package]] -name = "pip" -version = "24.2" -description = "The PyPA recommended tool for installing Python packages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pip-24.2-py3-none-any.whl", hash = "sha256:2cd581cf58ab7fcfca4ce8efa6dcacd0de5bf8d0a3eb9ec927e07405f4d9e2a2"}, - {file = "pip-24.2.tar.gz", hash = "sha256:5b5e490b5e9cb275c879595064adce9ebd31b854e3e803740b72f9ccf34a45b8"}, -] - -[[package]] -name = "pip-api" -version = "0.0.34" -description = "An unofficial, importable pip API" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pip_api-0.0.34-py3-none-any.whl", hash = "sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb"}, - {file = "pip_api-0.0.34.tar.gz", hash = "sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625"}, -] - -[package.dependencies] -pip = "*" - [[package]] name = "pluggy" version = "1.5.0" @@ -309,13 +274,13 @@ files = [ [[package]] name = "pyaml" -version = "24.7.0" +version = "24.9.0" description = "PyYAML-based module to produce a bit more pretty and readable YAML-serialized data" optional = false python-versions = ">=3.8" files = [ - {file = "pyaml-24.7.0-py3-none-any.whl", hash = "sha256:6b06596cb5ac438a3fad1e1bf5775088c4d3afb927e2b03a29305d334835deb2"}, - {file = "pyaml-24.7.0.tar.gz", hash = "sha256:5d0fdf9e681036fb263a783d0298fc3af580a6e2a6cf1a3314ffc48dc3d91ccb"}, + {file = "pyaml-24.9.0-py3-none-any.whl", hash = "sha256:31080551502f1014852b3c966a96c796adc79b4cf86e165f28ed83455bf19c62"}, + {file = "pyaml-24.9.0.tar.gz", hash = "sha256:e78dee8b0d4fed56bb9fa11a8a7858e6fade1ec70a9a122cee6736efac3e69b5"}, ] [package.dependencies] @@ -488,22 +453,6 @@ pytest = ">=3.2.2" [package.extras] develop = ["pylint", "pytest-cov"] -[[package]] -name = "pytest-reqs" -version = "0.2.1" -description = "pytest plugin to check pinned requirements" -optional = false -python-versions = "*" -files = [ - {file = "pytest-reqs-0.2.1.tar.gz", hash = "sha256:a844458b1e65ca7038be5201c814472725ddcc881ab33125c86b952232a7cfd8"}, - {file = "pytest_reqs-0.2.1-py3-none-any.whl", hash = "sha256:e87fcc2ea23fea9edb9e2ed877b4e839a4aa44df430f9a38f7469a70fdb0edfc"}, -] - -[package.dependencies] -packaging = ">=17.1" -pip-api = ">=0.0.2" -pytest = ">=2.4.2" - [[package]] name = "pytest-unordered" version = "0.5.2" @@ -628,4 +577,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11.4" -content-hash = "77b250569def1ef2cca5b0540b0860096cf89b8a35737341f75c9a00f3162b9f" +content-hash = "8f17402f1b27d1ca0810b83fbb3b1dc42b5e5a1d98607f32c3d38aa20e989b51" diff --git a/CheckQC/pyproject.toml b/CheckQC/pyproject.toml index 402d9e6a..cfb9f284 100644 --- a/CheckQC/pyproject.toml +++ b/CheckQC/pyproject.toml @@ -20,7 +20,6 @@ pytest-dataset = "0.3.2" pytest-flake8 = "1.0.7" pytest-mock = "3.8.2" pytest-raises = "0.11" -pytest-reqs = "0.2.1" pytest-unordered = "0.5.2" [tool.ruff] From 730a6eb6a7b4ad2a3d047138f71602623a572fec Mon Sep 17 00:00:00 2001 From: ellendejong Date: Wed, 23 Oct 2024 11:18:02 +0200 Subject: [PATCH 028/161] fix checkqc test by creating a valid qc metric (all required columns) --- CheckQC/test_check_qc.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/CheckQC/test_check_qc.py b/CheckQC/test_check_qc.py index d6ef6ae0..a56f3100 100644 --- a/CheckQC/test_check_qc.py +++ b/CheckQC/test_check_qc.py @@ -210,9 +210,16 @@ def test_add_failed_samples_multi_sample_col(self): assert passed_sample in list(qc_metric[["sample_col1", "sample_col2"]].values.ravel()) def test_only_passed_rows(self): - fake_qc_metric = DataFrame({"sample": ["sample1"], "fake_qc_col": ["0.1"]}) - failed_rows = DataFrame().index - qc_metric, qc_metric_out = check_qc.add_failed_samples_metric(fake_qc_metric, failed_rows, None, None) + fake_qc_metric = DataFrame({ + "sample_col": ["sample1"], + "qc_check": [None], + "qc_status": ["PASS"], + "qc_msg": [None], + "qc_value": [0.1], + }) + qc_metric, qc_metric_out = check_qc.add_failed_samples_metric( + fake_qc_metric, DataFrame().index, fake_qc_metric.columns.to_list(), ["sample"] + ) assert qc_metric_out.empty assert fake_qc_metric.equals(qc_metric) From 59e1f59d72b6a634925cd5d2d9421bf9cfaf3b59 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Mon, 4 Nov 2024 09:49:05 +0100 Subject: [PATCH 029/161] move defaults outside jobs gendercheck_test --- .github/workflows/gendercheck_test.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gendercheck_test.yml b/.github/workflows/gendercheck_test.yml index dc3d2794..772ab44b 100644 --- a/.github/workflows/gendercheck_test.yml +++ b/.github/workflows/gendercheck_test.yml @@ -7,12 +7,14 @@ on: branches: [master, develop] paths: [GenderCheck/**] +defaults: + run: + working-directory: GenderCheck/ + jobs: pytest: runs-on: ubuntu-latest - defaults: - run: - working-directory: GenderCheck/ + steps: #---------------------------------------------- # check-out repo and set-up python From 0740cead8fb320f08597d688423b5ac61732c6bc Mon Sep 17 00:00:00 2001 From: ellendejong Date: Mon, 4 Nov 2024 10:05:26 +0100 Subject: [PATCH 030/161] add working-directory to each step that includes a 'with'. --- .github/workflows/gendercheck_test.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/gendercheck_test.yml b/.github/workflows/gendercheck_test.yml index 772ab44b..a441b219 100644 --- a/.github/workflows/gendercheck_test.yml +++ b/.github/workflows/gendercheck_test.yml @@ -7,14 +7,15 @@ on: branches: [master, develop] paths: [GenderCheck/**] -defaults: - run: - working-directory: GenderCheck/ jobs: pytest: runs-on: ubuntu-latest - + env: + working-directory: ./GenderCheck/ + defaults: + run: + working-directory: ${{ env.working-directory }} steps: #---------------------------------------------- # check-out repo and set-up python @@ -25,6 +26,7 @@ jobs: id: setup-python uses: actions/setup-python@v5 with: + working-directory: ${{ env.working-directory }} python-version: '3.11.5' #---------------------------------------------- # install & configure poetry @@ -32,6 +34,7 @@ jobs: - name: Install Poetry uses: snok/install-poetry@v1 with: + working-directory: ${{ env.working-directory }} virtualenvs-create: true virtualenvs-in-project: true installer-parallel: true @@ -43,6 +46,7 @@ jobs: id: cached-poetry-dependencies uses: actions/cache@v4 with: + working-directory: ${{ env.working-directory }} path: .venv key: venv_gendercheck-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} #---------------------------------------------- From 1920f36afa8583611844c7e413a234f9d10aff07 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Mon, 4 Nov 2024 10:35:21 +0100 Subject: [PATCH 031/161] Add working directory as environment var and use to load cache. --- .github/workflows/checkqc_test.yml | 6 ++++-- .github/workflows/gendercheck_test.yml | 7 ++----- .github/workflows/kinship_test.yml | 6 ++++-- .github/workflows/moisaichunter_test.yml | 6 ++++-- .github/workflows/utils_test.yml | 6 ++++-- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/.github/workflows/checkqc_test.yml b/.github/workflows/checkqc_test.yml index fd9b320f..b5951a43 100644 --- a/.github/workflows/checkqc_test.yml +++ b/.github/workflows/checkqc_test.yml @@ -10,9 +10,11 @@ on: jobs: pytest: runs-on: ubuntu-latest + env: + working-directory: CheckQC/ defaults: run: - working-directory: CheckQC/ + working-directory: ${{ env.working-directory }} steps: #---------------------------------------------- # check-out repo and set-up python @@ -41,7 +43,7 @@ jobs: id: cached-poetry-dependencies uses: actions/cache@v4 with: - path: .venv + path: ${{ env.working-directory }}/.venv key: venv_checkqc-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} #---------------------------------------------- # install dependencies if cache does not exist diff --git a/.github/workflows/gendercheck_test.yml b/.github/workflows/gendercheck_test.yml index a441b219..e8b0058c 100644 --- a/.github/workflows/gendercheck_test.yml +++ b/.github/workflows/gendercheck_test.yml @@ -12,7 +12,7 @@ jobs: pytest: runs-on: ubuntu-latest env: - working-directory: ./GenderCheck/ + working-directory: GenderCheck/ defaults: run: working-directory: ${{ env.working-directory }} @@ -26,7 +26,6 @@ jobs: id: setup-python uses: actions/setup-python@v5 with: - working-directory: ${{ env.working-directory }} python-version: '3.11.5' #---------------------------------------------- # install & configure poetry @@ -34,7 +33,6 @@ jobs: - name: Install Poetry uses: snok/install-poetry@v1 with: - working-directory: ${{ env.working-directory }} virtualenvs-create: true virtualenvs-in-project: true installer-parallel: true @@ -46,8 +44,7 @@ jobs: id: cached-poetry-dependencies uses: actions/cache@v4 with: - working-directory: ${{ env.working-directory }} - path: .venv + path: ${{ env.working-directory }}/.venv key: venv_gendercheck-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} #---------------------------------------------- # install dependencies if cache does not exist diff --git a/.github/workflows/kinship_test.yml b/.github/workflows/kinship_test.yml index 25acb4e7..bb7725dc 100644 --- a/.github/workflows/kinship_test.yml +++ b/.github/workflows/kinship_test.yml @@ -10,9 +10,11 @@ on: jobs: pytest: runs-on: ubuntu-latest + env: + working-directory: Kinship/ defaults: run: - working-directory: Kinship/ + working-directory: ${{ env.working-directory }} steps: #---------------------------------------------- # check-out repo and set-up python @@ -42,7 +44,7 @@ jobs: id: cached-poetry-dependencies uses: actions/cache@v4 with: - path: .venv + path: ${{ env.working-directory }}/.venv key: venv_kinship-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} #---------------------------------------------- # install dependencies if cache does not exist diff --git a/.github/workflows/moisaichunter_test.yml b/.github/workflows/moisaichunter_test.yml index 6cd34679..c38b2fab 100644 --- a/.github/workflows/moisaichunter_test.yml +++ b/.github/workflows/moisaichunter_test.yml @@ -10,9 +10,11 @@ on: jobs: pytest: runs-on: ubuntu-latest + env: + working-directory: MosaicHunter/1.0.0/ defaults: run: - working-directory: MosaicHunter/1.0.0/ + working-directory: ${{ env.working-directory }} steps: #---------------------------------------------- # check-out repo and set-up python @@ -41,7 +43,7 @@ jobs: id: cached-poetry-dependencies uses: actions/cache@v4 with: - path: .venv + path: ${{ env.working-directory }}/.venv key: venv_mosaichunter-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} #---------------------------------------------- # install dependencies if cache does not exist diff --git a/.github/workflows/utils_test.yml b/.github/workflows/utils_test.yml index 8fa18c2b..47bc24d1 100644 --- a/.github/workflows/utils_test.yml +++ b/.github/workflows/utils_test.yml @@ -10,9 +10,11 @@ on: jobs: pytest: runs-on: ubuntu-latest + env: + working-directory: Utils/ defaults: run: - working-directory: Utils/ + working-directory: ${{ env.working-directory }} steps: #---------------------------------------------- # check-out repo and set-up python @@ -41,7 +43,7 @@ jobs: id: cached-poetry-dependencies uses: actions/cache@v4 with: - path: .venv + path: ${{ env.working-directory }}/.venv key: venv_utils-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} #---------------------------------------------- # install dependencies if cache does not exist From 72f98549d83e2edfdf67838ef1e5d97408440628 Mon Sep 17 00:00:00 2001 From: ellendejong Date: Mon, 11 Nov 2024 11:47:32 +0100 Subject: [PATCH 032/161] replace source and pytest with poetry run --- .github/workflows/checkqc_test.yml | 3 +-- .github/workflows/utils_test.yml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/checkqc_test.yml b/.github/workflows/checkqc_test.yml index b5951a43..032de651 100644 --- a/.github/workflows/checkqc_test.yml +++ b/.github/workflows/checkqc_test.yml @@ -61,5 +61,4 @@ jobs: #---------------------------------------------- - name: Run tests run: | - source .venv/bin/activate - pytest . + poetry run pytest . diff --git a/.github/workflows/utils_test.yml b/.github/workflows/utils_test.yml index 47bc24d1..9490573f 100644 --- a/.github/workflows/utils_test.yml +++ b/.github/workflows/utils_test.yml @@ -61,5 +61,4 @@ jobs: #---------------------------------------------- - name: Run tests run: | - source .venv/bin/activate - pytest . + poetry run pytest . From 0f724c4ccbe7fee6fa1e69d65c5566393d813530 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 31 Jan 2025 17:22:42 +0100 Subject: [PATCH 033/161] GenerateBreaks output split into 2 files --- DIMS/AssignToBins.R | 10 +++++----- DIMS/AssignToBins.nf | 4 ++-- DIMS/GenerateBreaks.R | 3 ++- DIMS/GenerateBreaks.nf | 1 + 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/DIMS/AssignToBins.R b/DIMS/AssignToBins.R index e49d6c66..a25a5e70 100644 --- a/DIMS/AssignToBins.R +++ b/DIMS/AssignToBins.R @@ -1,5 +1,3 @@ -## adapted from 2-DIMS.R - # load required packages suppressPackageStartupMessages(library("xcms")) @@ -8,13 +6,15 @@ cmd_args <- commandArgs(trailingOnly = TRUE) mzml_filepath <- cmd_args[1] breaks_filepath <- cmd_args[2] -resol <- as.numeric(cmd_args[3]) +trimparams_filepath <- cmd_args[3] +resol <- as.numeric(cmd_args[4]) trim <- 0.1 dims_thresh <- 100 -# load breaks_file: contains breaks_fwhm, breaks_fwhm_avg, -# trim_left_neg, trim_left_pos, trim_right_neg & trim_right_pos +# load breaks_file: contains breaks_fwhm, breaks_fwhm_avg load(breaks_filepath) +# load trim paramters: trim_left_neg, trim_left_pos, trim_right_neg & trim_right_pos +load(trimparams_filepath) # get sample name sample_name <- sub("\\..*$", "", basename(mzml_filepath)) diff --git a/DIMS/AssignToBins.nf b/DIMS/AssignToBins.nf index d0a7098b..d3bc79ae 100644 --- a/DIMS/AssignToBins.nf +++ b/DIMS/AssignToBins.nf @@ -5,7 +5,7 @@ process AssignToBins { shell = ['/bin/bash', '-euo', 'pipefail'] input: - tuple(val(file_id), path(mzML_file), path(breaks_file)) + tuple(val(file_id), path(mzML_file), path(breaks_file), path(trim_params_file)) output: path("${file_id}.RData"), emit: rdata_file @@ -13,7 +13,7 @@ process AssignToBins { script: """ - Rscript ${baseDir}/CustomModules/DIMS/AssignToBins.R $mzML_file $breaks_file $params.resolution + Rscript ${baseDir}/CustomModules/DIMS/AssignToBins.R $mzML_file $breaks_file $trim_params_file $params.resolution """ } diff --git a/DIMS/GenerateBreaks.R b/DIMS/GenerateBreaks.R index d07fd203..ddb68c56 100644 --- a/DIMS/GenerateBreaks.R +++ b/DIMS/GenerateBreaks.R @@ -57,5 +57,6 @@ for (i in 1:nr_segments) { } # generate output file -save(breaks_fwhm, breaks_fwhm_avg, trim_left_pos, trim_right_pos, trim_left_neg, trim_right_neg, file = "breaks.fwhm.RData") +save(breaks_fwhm, breaks_fwhm_avg, file = "breaks.fwhm.RData") +save(trim_left_pos, trim_right_pos, trim_left_neg, trim_right_neg, file = "trim_params.RData") save(high_mz, file = "highest_mz.RData") diff --git a/DIMS/GenerateBreaks.nf b/DIMS/GenerateBreaks.nf index af617a8d..c486010d 100644 --- a/DIMS/GenerateBreaks.nf +++ b/DIMS/GenerateBreaks.nf @@ -10,6 +10,7 @@ process GenerateBreaks { output: path('breaks.fwhm.RData'), emit: breaks + path('trim_params.RData'), emit: trim_params path('highest_mz.RData'), emit: highest_mz script: From de495e3cd07216be24fd993abdfe7c75054da6be Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Mon, 3 Feb 2025 10:21:36 +0100 Subject: [PATCH 034/161] AverageTechReplicates replaced by EvaluateTics --- DIMS/EvaluateTics.R | 176 +++++++++++++++++++++++++++++++++++++++++++ DIMS/EvaluateTics.nf | 34 +++++++++ 2 files changed, 210 insertions(+) create mode 100644 DIMS/EvaluateTics.R create mode 100644 DIMS/EvaluateTics.nf diff --git a/DIMS/EvaluateTics.R b/DIMS/EvaluateTics.R new file mode 100644 index 00000000..5b8c1e30 --- /dev/null +++ b/DIMS/EvaluateTics.R @@ -0,0 +1,176 @@ +# load packages +library("ggplot2") +library("gridExtra") + +# define parameters +cmd_args <- commandArgs(trailingOnly = TRUE) + +init_file <- cmd_args[1] +nr_replicates <- as.numeric(cmd_args[2]) +run_name <- cmd_args[3] +dims_matrix <- cmd_args[4] +highest_mz_file <- cmd_args[5] +highest_mz <- get(load(highest_mz_file)) +trim_params_filepath <- cmd_args[6] +thresh2remove <- 1000000000 +dims_thresh <- 100 + +remove_from_repl_pattern <- function(bad_samples, repl_pattern, nr_replicates) { + # collect list of samples to remove from replication pattern + remove_from_group <- NULL + for (sample_nr in 1:length(repl_pattern)){ + repl_pattern_1sample <- repl_pattern[[sample_nr]] + remove <- NULL + for (file_nr in 1:length(repl_pattern_1sample)) { + if (repl_pattern_1sample[file_nr] %in% bad_samples) { + remove <- c(remove, file_nr) + } + } + if (length(remove) == nr_replicates) { + remove_from_group <- c(remove_from_group, sample_nr) + } + if (!is.null(remove)) { + repl_pattern[[sample_nr]] <- repl_pattern[[sample_nr]][-remove] + } + } + if (length(remove_from_group) != 0) { + repl_pattern <- repl_pattern[-remove_from_group] + } + return(list("pattern" = repl_pattern)) +} + +# load init_file: contains repl_pattern +load(init_file) + +# load trim_params_file: contains trim_left_neg, trim_left_pos, trim_right_neg & trim_right_pos +load(trim_params_filepath) + +# lower the threshold below which a sample will be removed for DBS and for high m/z +if (dims_matrix == "DBS") { + thresh2remove <- 500000000 +} +if (highest_mz > 700) { + thresh2remove <- 1000000 +} + +# remove technical replicates which are below the threshold and average intensity over time +remove_neg <- NULL +remove_pos <- NULL +cat("Pklist sum threshold to remove technical replicate:", thresh2remove, "\n") +for (sample_nr in 1:length(repl_pattern)) { + tech_reps <- as.vector(unlist(repl_pattern[sample_nr])) + tech_reps_array_pos <- NULL + tech_reps_array_neg <- NULL + for (file_nr in 1:length(tech_reps)) { + load(paste(tech_reps[file_nr], ".RData", sep = "")) + cat("\n\nParsing", tech_reps[file_nr]) + # positive scan mode: determine whether sum of intensities is above threshold + cat("\n\tPositive peak_list sum", sum(peak_list$pos[, 1])) + if (sum(peak_list$pos[, 1]) < thresh2remove) { + cat(" ... Removed") + remove_pos <- c(remove_pos, tech_reps[file_nr]) + } + tech_reps_array_pos <- cbind(tech_reps_array_pos, peak_list$pos) + # negative scan mode: determine whether sum of intensities is above threshold + cat("\n\tNegative peak_list sum", sum(peak_list$neg[, 1])) + if (sum(peak_list$neg[, 1]) < thresh2remove) { + cat(" ... Removed") + remove_neg <- c(remove_neg, tech_reps[file_nr]) + } + tech_reps_array_neg <- cbind(tech_reps_array_neg, peak_list$neg) + } +} + +pattern_list <- remove_from_repl_pattern(remove_neg, repl_pattern, nr_replicates) +repl_pattern_filtered <- pattern_list$pattern +save(repl_pattern_filtered, file = "negative_repl_pattern.RData") +write.table( + remove_neg, + file = "miss_infusions_negative.txt", + row.names = FALSE, + col.names = FALSE, + sep = "\t" +) + +pattern_list <- remove_from_repl_pattern(remove_pos, repl_pattern, nr_replicates) +repl_pattern_filtered <- pattern_list$pattern +save(repl_pattern_filtered, file = "positive_repl_pattern.RData") +write.table( + remove_pos, + file = "miss_infusions_positive.txt", + row.names = FALSE, + col.names = FALSE, + sep = "\t" +) + +## generate TIC plots +# get all txt files +tic_files <- list.files("./", full.names = TRUE, pattern = "*TIC.txt") +all_samps <- sub("_TIC\\..*$", "", basename(tic_files)) + +# determine maximum intensity +highest_tic_max <- 0 +for (file in tic_files) { + tic <- read.table(file) + this_tic_max <- max(tic$tic_intensity) + if (this_tic_max > highest_tic_max) { + highest_tic_max <- this_tic_max + max_sample <- sub("_TIC\\..*$", "", basename(file)) + } +} + +# create a list with information for all TIC plots +tic_plot_list <- list() +plot_nr <- 0 +for (sample_nr in c(1:length(repl_pattern))) { + tech_reps <- as.vector(unlist(repl_pattern[sample_nr])) + sample_name <- names(repl_pattern)[sample_nr] + for (file_nr in 1:length(tech_reps)) { + plot_nr <- plot_nr + 1 + # repl1_nr <- read.table(paste(paste(outdir, "2-pklist/", sep = "/"), tech_reps[file_nr], "_TIC.txt", sep = "")) + repl1_nr <- read.table(paste0(tech_reps[file_nr], "_TIC.txt")) + bad_color_pos <- tech_reps[file_nr] %in% remove_pos + bad_color_neg <- tech_reps[file_nr] %in% remove_neg + if (bad_color_neg & bad_color_pos) { + plot_color <- "#F8766D" + } else if (bad_color_pos) { + plot_color <- "#ED8141" + } else if (bad_color_neg) { + plot_color <- "#BF80FF" + } else { + plot_color <- "white" + } + tic_plot <- ggplot(repl1_nr, aes(retention_time, tic_intensity)) + + geom_line(linewidth = 0.3) + + geom_hline(yintercept = highest_tic_max, col = "grey", linetype = 2, linewidth = 0.3) + + geom_vline(xintercept = trim_left_pos, col = "red", linetype = 2, linewidth = 0.3) + + geom_vline(xintercept = trim_right_pos, col = "red", linetype = 2, linewidth = 0.3) + + geom_vline(xintercept = trim_left_neg, col = "red", linetype = 2, linewidth = 0.3) + + geom_vline(xintercept = trim_right_neg, col = "red", linetype = 2, linewidth = 0.3) + + labs(x = "t (s)", y = "tic_intensity", title = paste0(tech_reps[file_nr], " || ", sample_name)) + + theme(plot.background = element_rect(fill = plot_color), + axis.text = element_text(size = 4), + axis.title = element_text(size = 4), + plot.title = element_text(size = 6)) + tic_plot_list[[plot_nr]] <- tic_plot + } +} + +# create a layout matrix dependent on number of replicates +layout <- matrix(1:(10 * nr_replicates), 10, nr_replicates, TRUE) +# put TIC plots in matrix +tic_plot_pdf <- marrangeGrob( + grobs = tic_plot_list, + nrow = 10, ncol = nr_replicates, + layout_matrix = layout, + top = quote(paste( + "TICs of run", run_name, + " \n colors: red = both modes misinjection, orange = pos mode misinjection, purple = neg mode misinjection \n ", + g, "/", npages + )) +) + +# save to file +ggsave(filename = paste0(run_name, "_TICplots.pdf"), + tic_plot_pdf, width = 21, height = 29.7, units = "cm") + diff --git a/DIMS/EvaluateTics.nf b/DIMS/EvaluateTics.nf new file mode 100644 index 00000000..7ce58684 --- /dev/null +++ b/DIMS/EvaluateTics.nf @@ -0,0 +1,34 @@ +process EvaluateTics { + tag "DIMS EvaluateTics" + label 'EvaluateTics' + container = 'docker://umcugenbioinf/dims:1.3' + shell = ['/bin/bash', '-euo', 'pipefail'] + + input: + path(rdata_file) + path(tic_txt_files) + path(init_file) + val(nr_replicates) + val(analysis_id) + val(matrix) + path(highest_mz_file) + path(trim_params_file) + + output: + path('*_repl_pattern.RData'), emit: pattern_files + path('miss_infusions_negative.txt') + path('miss_infusions_positive.txt') + path('*_TICplots.pdf') + + script: + """ + Rscript ${baseDir}/CustomModules/DIMS/EvaluateTics.R $init_file \ + $params.nr_replicates \ + $analysis_id \ + $matrix \ + $highest_mz_file \ + $trim_params_file + """ +} + + From 434a41f7aca65177e226405058e30330207efed3 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Mon, 3 Feb 2025 10:22:30 +0100 Subject: [PATCH 035/161] refactored PeakFinding, peak finding funtions moved to folder preprocessing --- DIMS/PeakFinding.R | 87 ++- DIMS/PeakFinding.nf | 2 +- DIMS/preprocessing/peak_finding_functions.R | 779 ++++++++++++++++++++ 3 files changed, 831 insertions(+), 37 deletions(-) create mode 100644 DIMS/preprocessing/peak_finding_functions.R diff --git a/DIMS/PeakFinding.R b/DIMS/PeakFinding.R index 697f8f7e..738a0bc7 100644 --- a/DIMS/PeakFinding.R +++ b/DIMS/PeakFinding.R @@ -1,53 +1,68 @@ -## adapted from 4-peakFinding.R - # define parameters cmd_args <- commandArgs(trailingOnly = TRUE) -sample_file <- cmd_args[1] +replicate_mzmlfile <- cmd_args[1] breaks_file <- cmd_args[2] resol <- as.numeric(cmd_args[3]) scripts_dir <- cmd_args[4] -thresh <- 2000 -outdir <- "./" +peak_thresh <- 2000 # load in function scripts -source(paste0(scripts_dir, "do_peakfinding.R")) -source(paste0(scripts_dir, "check_overlap.R")) -source(paste0(scripts_dir, "search_mzrange.R")) -source(paste0(scripts_dir, "fit_optim.R")) -source(paste0(scripts_dir, "fit_gaussian.R")) -source(paste0(scripts_dir, "fit_init.R")) -source(paste0(scripts_dir, "get_fwhm.R")) -source(paste0(scripts_dir, "get_stdev.R")) -source(paste0(scripts_dir, "optimize_gaussfit.R")) -source(paste0(scripts_dir, "fit_peaks.R")) -source(paste0(scripts_dir, "fit_gaussians.R")) -source(paste0(scripts_dir, "estimate_area.R")) -source(paste0(scripts_dir, "get_fit_quality.R")) -source(paste0(scripts_dir, "check_overlap.R")) -source(paste0(scripts_dir, "sum_curves.R")) -source(paste0(scripts_dir, "within_ppm.R")) +preprocessing_scripts_dir <- gsub("Utils", "preprocessing", scripts_dir) +source(paste0(preprocessing_scripts_dir, "peak_finding_functions.R")) load(breaks_file) -# Load output of AverageTechReplicates for a sample -sample_avgtechrepl <- get(load(sample_file)) -if (grepl("_pos", sample_file)) { - scanmode <- "positive" -} else if (grepl("_neg", sample_file)) { - scanmode <- "negative" -} +# Load output of AssignToBins for a sample +sample_techrepl <- get(load(replicate_mzmlfile)) +scanmodes <- c("positive", "negative") +ints_pos <- peak_list$pos +ints_neg <- peak_list$neg # Initialize options(digits = 16) -# Number used to calculate area under Gaussian curve -int_factor <- 1 * 10^5 -# Initial value used to estimate scaling parameter -scale <- 2 -width <- 1024 -height <- 768 # run the findPeaks function +techrepl_name <- colnames(ints_pos)[1] + +for (scanmode in scanmodes) { + # turn dataframe with intensities into a named list + if (scanmode == "positive") { + ints_perscanmode <- peak_list$pos + } else if (scanmode == "negative") { + ints_perscanmode <- peak_list$neg + } + + ints_fullrange <- as.vector(ints_perscanmode) + names(ints_fullrange) <- rownames(ints_perscanmode) + + start.time <- Sys.time() + + # look for m/z range for all peaks + allpeaks_values <- search_mzrange(ints_fullrange, resol, techrepl_name, scanmode, peak_thresh) + + end.time <- Sys.time() + time.taken <- end.time - start.time + time.taken + + # turn the list into a dataframe + outlist_persample <- NULL + outlist_persample <- cbind("samplenr" = allpeaks_values$nr, + "mzmed.pkt" = allpeaks_values$mean, + "fq" = allpeaks_values$qual, + "mzmin.pkt" = allpeaks_values$min, + "mzmax.pkt" = allpeaks_values$max, + "height.pkt" = allpeaks_values$area) + + # remove peaks with height = 0 + outlist_persample <- outlist_persample[outlist_persample[, "height.pkt"] != 0, ] + + # save output to file + save(outlist_persample, file = paste0(techrepl_name, "_", scanmode, ".RData")) + + # generate text output to log file on number of spikes for this sample + # spikes are peaks that are too narrow, e.g. 1 data point + cat(paste("There were", allpeaks_values$spikes, "spikes")) + +} -# do_peakfinding(sample_avgtechrepl, breaks_fwhm, int_factor, scale, resol, outdir, scanmode, FALSE, thresh, width, height) -do_peakfinding(sample_avgtechrepl, int_factor, scale, resol, outdir, scanmode, FALSE, thresh, width, height) diff --git a/DIMS/PeakFinding.nf b/DIMS/PeakFinding.nf index 0097d128..dad2a497 100644 --- a/DIMS/PeakFinding.nf +++ b/DIMS/PeakFinding.nf @@ -12,6 +12,6 @@ process PeakFinding { script: """ - Rscript ${baseDir}/CustomModules/DIMS/PeakFinding.R $rdata_file $breaks_file $params.resolution $params.scripts_dir + Rscript ${baseDir}/CustomModules/DIMS/PeakFinding.R $rdata_file $breaks_file $params.resolution $params.preprocessing_scripts_dir """ } diff --git a/DIMS/preprocessing/peak_finding_functions.R b/DIMS/preprocessing/peak_finding_functions.R new file mode 100644 index 00000000..331d563b --- /dev/null +++ b/DIMS/preprocessing/peak_finding_functions.R @@ -0,0 +1,779 @@ +# functions for peak finding +search_mzrange <- function(ints_fullrange, resol, sample_name, scanmode, peak_thresh) { + #' Divide the full m/z range into regions of interest with min, max and mean m/z + #' + #' @param ints_fullrange: Named list of intensities (float) + #' @param resol: Value for resolution (integer) + #' @param sample_name: Sample name (string) + #' @param scanmode: Scan mode, positive or negative (string) + #' @param peak_thresh: Value for noise level threshold (integer) + #' + #' @return allpeaks_values: list of m/z regions of interest + + # Number used to calculate area under Gaussian curve + int_factor <- 1 * 10^5 + + # initialize list to store results for all peaks + allpeaks_values <- list("mean" = NULL, "area" = NULL, "nr" = NULL, + "min" = NULL, "max" = NULL, "qual" = NULL, "spikes" = 0) + + # find indices where intensity is not equal to zero + nonzero_positions <- as.vector(which(ints_fullrange != 0)) + + # initialize + # start position of the first peak + start_index <- nonzero_positions[1] + # maximum length of region of interest + max_roi_length <- 15 + + # find regions of interest + for (running_index in 1:length(nonzero_positions)) { + # find position of the end of a peak. + if (running_index < length(nonzero_positions) && (nonzero_positions[running_index + 1] - nonzero_positions[running_index]) > 1) { + end_index <- nonzero_positions[running_index] + # get m/z values and intensities for this region of interest + mass_vector <- as.numeric(names(ints_fullrange)[c(start_index:end_index)]) + int_vector <- as.vector(ints_fullrange[c(start_index:end_index)]) + # check if intensity is above threshold or the maximum intensity is NaN + if (max(int_vector) < peak_thresh || is.nan(max(int_vector))) { + # go to next region of interest + start_index <- nonzero_positions[running_index + 1] + next + } + # check if there are more intensities than maximum for region of interest + if (length(int_vector) > max_roi_length) { + print(length(int_vector)) + print(running_index) + # trim lowest intensities to zero + #int_vector[which(int_vector < min(int_vector) * 1.1)] <- 0 + # split the range into multiple sub ranges + #sub_range <- int_vector + #names(sub_range) <- mass_vector + #allpeaks_values <- search_mzrange(sub_range, allpeaks_values, resol, + # sample_name, scanmode, peak_thresh) + # A proper peak needs to have at least 3 intensities above threshold + } else if (length(int_vector) > 3) { + # check if the sum of intensities is above zero. Why is this necessary? + #if (sum(int_vector) == 0) next + # define mass_diff as difference between last and first value of mass_vector + mass_diff <- mass_vector[length(mass_vector)] - mass_vector[1] + # generate a second mass_vector with equally spaced m/z values + mass_vector2 <- seq(mass_vector[1], mass_vector[length(mass_vector)], + length = mass_diff * int_factor) + + # Find the index in int_vector with the highest intensity + # max_index <- which(int_vector == max(int_vector)) + # get initial fit values + roi_values <- fit_gaussian(mass_vector2, mass_vector, int_vector, # max_index, + resol, force = length(max_index), + use_bounds = FALSE, scanmode) + + if (roi_values$qual[1] == 1) { + # get optimized fit values + roi_values <- fit_optim(mass_vector, int_vector, resol, scanmode) + # add region of interest to list of all peaks + allpeaks_values$mean <- c(allpeaks_values$mean, roi_values$mean) + allpeaks_values$area <- c(allpeaks_values$area, roi_values$area) + allpeaks_values$nr <- c(allpeaks_values$nr, sample_name) + allpeaks_values$min <- c(allpeaks_values$min, roi_values$min) + allpeaks_values$max <- c(allpeaks_values$max, roi_values$max) + allpeaks_values$qual <- c(allpeaks_values$qual, 0) + allpeaks_values$spikes <- allpeaks_values$spikes + 1 + + } else { + for (j in 1:length(roi_values$mean)){ + allpeaks_values$mean <- c(allpeaks_values$mean, roi_values$mean[j]) + allpeaks_values$area <- c(allpeaks_values$area, roi_values$area[j]) + allpeaks_values$nr <- c(allpeaks_values$nr, sample_name) + allpeaks_values$min <- c(allpeaks_values$min, roi_values$min[1]) + allpeaks_values$max <- c(allpeaks_values$max, roi_values$max[1]) + allpeaks_values$qual <- c(allpeaks_values$qual, roi_values$qual[1]) + } + } + + } else { + + roi_values <- fit_optim(mass_vector, int_vector, resol, scanmode) + allpeaks_values$mean <- c(allpeaks_values$mean, roi_values$mean) + allpeaks_values$area <- c(allpeaks_values$area, roi_values$area) + allpeaks_values$nr <- c(allpeaks_values$nr, sample_name) + allpeaks_values$min <- c(allpeaks_values$min, roi_values$min) + allpeaks_values$max <- c(allpeaks_values$max, roi_values$max) + allpeaks_values$qual <- c(allpeaks_values$qual, 0) + allpeaks_values$spikes <- allpeaks_values$spikes + 1 + } + } + start_index <- nonzero_positions[running_index + 1] + } + + return(allpeaks_values) +} + +# remove plot sections (commented out) +fit_gaussian <- function(mass_vector2, mass_vector, int_vector, + resol, force, use_bounds, scanmode) { + #' Fit 1, 2, 3 or 4 Gaussian peaks in small region of m/z + #' + #' @param mass_vector2: Vector of equally spaced m/z values (float) + #' @param mass_vector: Vector of m/z values for a region of interest (float) + #' @param int_vector: Value used to calculate area under Gaussian curve (integer) + #' @param resol: Value for resolution (integer) + #' @param force: Number of local maxima in int_vector (integer) + #' @param use_bounds: Boolean to indicate whether boundaries are to be used + #' @param scanmode: Scan mode, positive or negative (string) + #' + #' @return roi_value_list: list of fit values for region of interest (list) + + # Number used to calculate area under Gaussian curve + int_factor <- 1 * 10^5 + + # Find the index in int_vector with the highest intensity + max_index <- which(int_vector == max(int_vector)) + + # Initialise + peak_mean <- NULL + peak_area <- NULL + peak_qual <- NULL + peak_min <- NULL + peak_max <- NULL + fit_quality1 <- 0.15 + fit_quality <- 0.2 + + # One local maximum: + if (force == 1) { + # determine fit values for 1 Gaussian peak (mean, scale, sigma, qual) + fit_values <- fit_1peak(mass_vector2, mass_vector, int_vector, max_index, resol, + fit_quality1, use_bounds) + + # set initial value for scale factor + scale <- 2 + # test if the mean is outside the m/z range + if (fit_values$mean[1] < mass_vector[1] || fit_values$mean[1] > mass_vector[length(mass_vector)]) { + # run this function again with fixed boundaries + return(fit_gaussian(mass_vector2, mass_vector, int_vector, resol, + force = 1, use_bounds = TRUE, scanmode)) + } else { + # test if the fit is bad + if (fit_values$qual > fit_quality1) { + # Try to fit two curves; find two local maxima. NB: max_index (now new_index) removed from fit_gaussian + new_index <- which(diff(sign(diff(int_vector))) == -2) + 1 + # test if there are two indices in new_index + if (length(new_index) != 2) { + new_index <- round(length(mass_vector) / 3) + new_index <- c(new_index, 2 * new_index) + } + # run this function again with two local maxima + return(fit_gaussian(mass_vector2, mass_vector, int_vector, + resol, force = 2, use_bounds = FALSE, scanmode)) + # good fit + } else { + peak_mean <- c(peak_mean, fit_values$mean) + peak_area <- c(peak_area, estimate_area(fit_values$mean, resol, fit_values$scale, + fit_values$sigma)) + peak_qual <- fit_values$qual + peak_min <- mass_vector[1] + peak_max <- mass_vector[length(mass_vector)] + } + } + + #### Two local maxima; need at least 6 data points for this #### + } else if (force == 2 && (length(mass_vector) > 6)) { + # determine fit values for 2 Gaussian peaks (mean, scale, sigma, qual) + fit_values <- fit_2peaks(mass_vector2, mass_vector, int_vector, max_index, resol, + use_bounds, fit_quality) + # test if one of the means is outside the m/z range + if (fit_values$mean[1] < mass_vector[1] || fit_values$mean[1] > mass_vector[length(mass_vector)] || + fit_values$mean[2] < mass_vector[1] || fit_values$mean[2] > mass_vector[length(mass_vector)]) { + # check if fit quality is bad + if (fit_values$qual > fit_quality) { + # run this function again with fixed boundaries + return(fit_gaussian(mass_vector2, mass_vector, int_vector, resol, + force = 2, use_bounds = TRUE, scanmode)) + } else { + # check which mean is outside range and remove it from the list of means + # NB: peak_mean and other variables have not been given values from 2-peak fit yet! + for (i in 1:length(fit_values$mean)){ + if (fit_values$mean[i] < mass_vector[1] || fit_values$mean[i] > mass_vector[length(mass_vector)]) { + peak_mean <- c(peak_mean, -i) + peak_area <- c(peak_area, -i) + } else { + peak_mean <- c(peak_mean, fit_values$mean[i]) + peak_area <- c(peak_area, fit_values$area[i]) + } + } + peak_qual <- fit_values$qual + peak_min <- mass_vector[1] + peak_max <- mass_vector[length(mass_vector)] + } + # if all means are within range + } else { + # check for bad fit + if (fit_values$qual > fit_quality) { + # Try to fit three curves; find three local maxima + new_index <- which(diff(sign(diff(int_vector))) == -2) + 1 + # test if there are three indices in new_index + if (length(new_index) != 3) { + new_index <- round(length(mass_vector) / 4) + new_index <- c(new_index, 2 * new_index, 3 * new_index) + } + # run this function again with three local maxima + return(fit_gaussian(mass_vector2, mass_vector, int_vector, + resol, force = 3, use_bounds = FALSE, scanmode)) + # good fit, all means are within m/z range + } else { + # check if means are within 3 ppm and sum if so + tmp <- fit_values$qual + nr_means_new <- -1 + nr_means <- length(fit_values$mean) + while (nr_means != nr_means_new) { + nr_means <- length(fit_values$mean) + fit_values <- within_ppm(fit_values$mean, fit_values$scale, fit_values$sigma, fit_values$area, + mass_vector2, mass_vector, ppm = 4, resol) + nr_means_new <- length(fit_values$mean) + } + # restore original quality score + fit_values$qual <- tmp + + for (i in 1:length(fit_values$mean)){ + peak_mean <- c(peak_mean, fit_values$mean[i]) + peak_area <- c(peak_area, fit_values$area[i]) + } + peak_qual <- fit_values$qual + peak_min <- mass_vector[1] + peak_max <- mass_vector[length(mass_vector)] + } + } + + } else { # More than two local maxima; fit 1 peak. + scale <- 2 + fit_quality1 <- 0.40 + use_bounds <- TRUE + max_index <- which(int_vector == max(int_vector)) + fit_values <- fit_1peak(mass_vector2, mass_vector, int_vector, max_index, resol, + fit_quality1, use_bounds) + # check for bad fit + if (fit_values$qual > fit_quality1) { + # get fit values from fit_optim + fit_values <- fit_optim(mass_vector, int_vector, resol, scanmode) + peak_mean <- c(peak_mean, fit_values$mean) + peak_area <- c(peak_area, fit_values$area) + peak_min <- fit_values$min + peak_max <- fit_values$max + peak_qual <- 0 + } else { + peak_mean <- c(peak_mean, fit_values$mean) + peak_area <- c(peak_area, estimate_area(fit_values$mean, resol, fit_values$scale, fit_values$sigma)) + peak_qual <- fit_values$qual + peak_min <- mass_vector[1] + peak_max <- mass_vector[length(mass_vector)] + } + } + + # put all values for this region of interest into a list + roi_value_list <- list("mean" = peak_mean, + "area" = peak_area, + "qual" = peak_qual, + "min" = peak_min, + "max" = peak_max) + return(roi_value_list) +} + + +fit_1peak <- function(mass_vector2, mass_vector, int_vector, max_index, + resol, fit_quality, use_bounds) { + #' Fit 1 Gaussian peak in small region of m/z + #' + #' @param mass_vector2: Vector of equally spaced m/z values (float) + #' @param mass_vector: Vector of m/z values for a region of interest (float) + #' @param int_vector: Value used to calculate area under Gaussian curve (integer) + #' @param max_index: Index in int_vector with the highest intensity (integer) + #' @param resol: Value for resolution (integer) + #' @param fit_quality: Value indicating quality of fit of Gaussian curve (float) + #' @param use_bounds: Boolean to indicate whether boundaries are to be used + #' + #' @return roi_value_list: list of fit values for region of interest (list) + + # set initial value for scale + scale <- 2 + + if (length(int_vector) < 3) { + message("Range too small, no fit possible!") + } else { + if ((length(int_vector) == 4)) { + # fit 1 peak + mu <- weighted.mean(mass_vector, int_vector) + sigma <- get_stdev(mass_vector, int_vector) + fitted_peak <- fit_1gaussian(mass_vector, int_vector, sigma, mu, use_bounds) + } else { + # set range vector + if ((length(mass_vector) - length(max_index)) < 2) { + range1 <- c((length(mass_vector) - 4) : length(mass_vector)) + } else if (length(max_index) < 2) { + range1 <- c(1:5) + } else { + range1 <- c(max_index[1] - 2, max_index[1] - 1, max_index[1], max_index[1] + 1, max_index[1] + 2) + } + if (range1[1] == 0) range1 <- range1[-1] + # remove NA + if (length(which(is.na(int_vector[range1]))) != 0) { + range1 <- range1[-which(is.na(int_vector[range1]))] + } + # fit 1 peak + mu <- weighted.mean(mass_vector[range1], int_vector[range1]) + sigma <- get_stdev(mass_vector[range1], int_vector[range1]) + fitted_peak <- fit_1gaussian(mass_vector, int_vector, sigma, mu, use_bounds) + } + + p1 <- fitted_peak$par + + # get new value for fit quality and scale + fq_new <- get_fit_quality(mass_vector, int_vector, p1[1], p1[1], resol, p1[2], sigma)$fq_new + scale_new <- 1.2 * scale + + # bad fit + if (fq_new > fit_quality) { + # optimize scaling factor + fq <- 0 + scale <- 0 + if (sum(int_vector) > sum(p1[2] * dnorm(mass_vector, p1[1], sigma))) { + while ((round(fq, digits = 3) != round(fq_new, digits = 3)) && (scale_new < 10000)) { + fq <- fq_new + scale <- scale_new + # fit 1 peak + fitted_peak <- fit_1gaussian(mass_vector, int_vector, sigma, mu, use_bounds) + p1 <- fitted_peak$par + # get new value for fit quality and scale + fq_new <- get_fit_quality(mass_vector, int_vector, p1[1], p1[1], resol, p1[2], sigma)$fq_new + scale_new <- 1.2 * scale + } + } else { + while ((round(fq, digits = 3) != round(fq_new, digits = 3)) && (scale_new < 10000)) { + fq <- fq_new + scale <- scale_new + # fit 1 peak + fitted_peak <- fit_1gaussian(mass_vector, int_vector, sigma, mu, use_bounds) + p1 <- fitted_peak$par + # get new value for fit quality and scale + fq_new <- get_fit_quality(mass_vector, int_vector, p1[1], p1[1], resol, p1[2], sigma)$fq_new + scale_new <- 0.8 * scale + } + } + # use optimized scale factor to fit 1 peak + if (fq < fq_new) { + fitted_peak <- fit_1gaussian(mass_vector, int_vector, sigma, mu, use_bounds) + p1 <- fitted_peak$par + fq_new <- fq + } + } + } + + roi_value_list <- list("mean" = p1[1], "scale" = p1[2], "sigma" = sigma, "qual" = fq_new) + return(roi_value_list) +} + +fit_2peaks <- function(mass_vector2, mass_vector, int_vector, max_index, resol, use_bounds = FALSE, + fit_quality) { + #' Fit 2 Gaussian peaks in small region of m/z + #' + #' @param mass_vector2: Vector of equally spaced m/z values (float) + #' @param mass_vector: Vector of m/z values for a region of interest (float) + #' @param int_vector: Value used to calculate area under Gaussian curve (integer) + #' @param max_index: Index in int_vector with the highest intensity (integer) + #' @param resol: Value for resolution (integer) + #' @param fit_quality: Value indicating quality of fit of Gaussian curve (float) + #' @param use_bounds: Boolean to indicate whether boundaries are to be used + #' + #' @return roi_value_list: list of fit values for region of interest (list) + + peak_mean <- NULL + peak_area <- NULL + peak_scale <- NULL + peak_sigma <- NULL + + # set range vectors for 2 peaks + range1 <- c(max_index[1] - 2, max_index[1] - 1, max_index[1], max_index[1] + 1, max_index[1] + 2) + if (range1[1] == 0) range1 <- range1[-1] + range2 <- c(max_index[2] - 2, max_index[2] - 1, max_index[2], max_index[2] + 1, max_index[2] + 2) + if (length(mass_vector) < range2[length(range2)]) range2 <- range2[-length(range2)] + range1 <- check_overlap(range1, range2)[[1]] + range2 <- check_overlap(range1, range2)[[2]] + # check for negative or 0 + remove <- which(range1 < 1) + if (length(remove) > 0) range1 <- range1[-remove] + remove <- which(range2 < 1) + if (length(remove) > 0) range2 <- range2[-remove] + # remove NA + if (length(which(is.na(int_vector[range1]))) != 0) range1 <- range1[-which(is.na(int_vector[range1]))] + if (length(which(is.na(int_vector[range2]))) != 0) range2 <- range2[-which(is.na(int_vector[range2]))] + + # fit 2 peaks, first separately, then together + mu1 <- weighted.mean(mass_vector[range1], int_vector[range1]) + sigma1 <- get_stdev(mass_vector[range1], int_vector[range1]) + fitted_peak <- fit_1gaussian(mass_vector[range1], int_vector[range1], sigma1, mu1, scale, use_bounds) + p1 <- fitted_peak$par + # second peak + mu2 <- weighted.mean(mass_vector[range2], int_vector[range2]) + sigma2 <- get_stdev(mass_vector[range2], int_vector[range2]) + fitted_peak <- fit_1gaussian(mass_vector[range2], int_vector[range2], sigma2, mu2, scale, use_bounds) + p2 <- fitted_peak$par + # combined + fitted_2peaks <- fit_2gaussians(mass_vector, int_vector, sigma1, sigma2, p1[1], p1[2], p2[1], p2[2], use_bounds) + pc <- fitted_2peaks$par + + # get fit quality + if (is.null(sigma2)) sigma2 <- sigma1 + sum_fit <- (pc[2] * dnorm(mass_vector, pc[1], sigma1)) + + (pc[4] * dnorm(mass_vector, pc[3], sigma2)) + fq <- get_fit_quality(mass_vector, int_vector, sort(c(pc[1], pc[3]))[1], sort(c(pc[1], pc[3]))[2], + resol, sum_fit = sum_fit)$fq_new + + # get parameter values + area1 <- estimate_area(pc[1], resol, pc[2], sigma1, int_factor) + area2 <- estimate_area(pc[3], resol, pc[4], sigma2, int_factor) + peak_area <- c(peak_area, area1) + peak_area <- c(peak_area, area2) + peak_mean <- c(peak_mean, pc[1]) + peak_mean <- c(peak_mean, pc[3]) + peak_scale <- c(peak_scale, pc[2]) + peak_scale <- c(peak_scale, pc[4]) + peak_sigma <- c(peak_sigma, sigma1) + peak_sigma <- c(peak_sigma, sigma2) + + roi_value_list <- list("mean" = peak_mean, "scale" = peak_scale, "sigma" = peak_sigma, "area" = peak_area, "qual" = fq) + return(roi_value_list) +} + +fit_1gaussian <- function(mass_vector, int_vector, sigma, query_mass, use_bounds) { + #' Fit a Gaussian curve for a peak with given parameters + #' + #' @param mass_vector: Vector of masses (float) + #' @param int_vector: Vector of intensities (float) + #' @param sigma: Value for width of the peak (float) + #' @param query_mass: Value for mass at center of peak (float) + #' @param use_bounds: Boolean to indicate whether boundaries are to be used + #' + #' @return opt_fit: list of parameters and values describing the optimal fit + + # Initial value used to estimate scaling parameter + scale <- 2 + + # define optimization function for optim based on normal distribution + opt_f <- function(params) { + d <- params[2] * dnorm(mass_vector, mean = params[1], sd = sigma) + sum((d - int_vector) ^ 2) + } + if (use_bounds) { + # determine lower and upper boundaries + lower <- c(mass_vector[1], 0, mass_vector[1], 0) + upper <- c(mass_vector[length(mass_vector)], Inf, mass_vector[length(mass_vector)], Inf) + # get optimal value for fitted Gaussian curve + opt_fit <- optim(c(as.numeric(query_mass), as.numeric(scale)), + opt_f, control = list(maxit = 10000), method = "L-BFGS-B", + lower = lower, upper = upper) + } else { + opt_fit <- optim(c(as.numeric(query_mass), as.numeric(scale)), + opt_f, control = list(maxit = 10000)) + } + return(opt_fit) +} + +fit_2gaussians <- function(mass_vector, int_vector, sigma1, sigma2, + query_mass1, scale1, + query_mass2, scale2, use_bounds) { + #' Fit two Gaussian curves for a peak with given parameters + #' + #' @param mass_vector: Vector of masses (float) + #' @param int_vector: Vector of intensities (float) + #' @param sigma1: Value for width of the first peak (float) + #' @param sigma2: Value for width of the second peak (float) + #' @param query_mass1: Value for mass at center of first peak (float) + #' @param scale1: Value for scaling intensities for first peak (float) + #' @param query_mass2: Value for mass at center of second peak (float) + #' @param scale2: Value for scaling intensities for second peak (float) + #' @param use_bounds: Boolean to indicate whether boundaries are to be used + #' + #' @return opt_fit: list of parameters and values describing the optimal fit + + # define optimization function for optim based on normal distribution + opt_f <- function(params) { + d <- params[2] * dnorm(mass_vector, mean = params[1], sd = sigma1) + + params[4] * dnorm(mass_vector, mean = params[3], sd = sigma2) + sum((d - int_vector) ^ 2) + } + + if (use_bounds) { + # determine lower and upper boundaries + lower <- c(mass_vector[1], 0, mass_vector[1], 0) + upper <- c(mass_vector[length(mass_vector)], Inf, mass_vector[length(mass_vector)], Inf) + # get optimal value for 2 fitted Gaussian curves + if (is.null(query_mass2) && is.null(scale2) && is.null(sigma2)) { + sigma2 <- sigma1 + opt_fit <- optim(c(as.numeric(query_mass1), as.numeric(scale1), + as.numeric(query_mass1), as.numeric(scale1)), + opt_f, control = list(maxit = 10000), + method = "L-BFGS-B", lower = lower, upper = upper) + } else { + opt_fit <- optim(c(as.numeric(query_mass1), as.numeric(scale1), + as.numeric(query_mass2), as.numeric(scale2)), + opt_f, control = list(maxit = 10000), + method = "L-BFGS-B", lower = lower, upper = upper) + } + } else { + if (is.null(query_mass2) && is.null(scale2) && is.null(sigma2)) { + sigma2 <- sigma1 + opt_fit <- optim(c(as.numeric(query_mass1), as.numeric(scale1), + as.numeric(query_mass1), as.numeric(scale1)), + opt_f, control = list(maxit = 10000)) + } else { + opt_fit <- optim(c(as.numeric(query_mass1), as.numeric(scale1), + as.numeric(query_mass2), as.numeric(scale2)), + opt_f, control = list(maxit = 10000)) + } + } + return(opt_fit) +} + +get_stdev <- function(mass_vector, int_vector, resol = 140000) { + #' Calculate standard deviation to determine width of a peak + #' + #' @param mass_vector: Vector of 3 mass values (float) + #' @param int_vector: Vector of 3 intensities (float) + #' @param resol: Value for resolution (integer) + #' + #' @return stdev: Value for standard deviation + # find maximum intensity in vector + max_index <- which(int_vector == max(int_vector)) + # find corresponding mass at maximum intensity + max_mass <- mass_vector[max_index] + # calculate resolution at given m/z value + resol_mz <- resol * (1 / sqrt(2) ^ (log2(max_mass / 200))) + # calculate full width at half maximum + fwhm <- max_mass / resol_mz + # calculate standard deviation + stdev <- (fwhm / 2) * 0.85 + return(stdev) +} + +## adapted from getFitQuality.R +# parameter not used: mu_last +get_fit_quality <- function(mass_vector, int_vector, mu_first, mu_last, resol, scale = NULL, sigma = NULL, sum_fit = NULL) { + #' Fit 1 Gaussian peak in small region of m/z + #' + #' @param mass_vector: Vector of m/z values for a region of interest (float) + #' @param int_vector: Value used to calculate area under Gaussian curve (integer) + #' @param mu_first: Value for first peak (float) + #' @param scale: Initial value used to estimate scaling parameter (integer) + #' @param resol: Value for resolution (integer) + #' @param sum_fit: Value indicating quality of fit of Gaussian curve (float) + #' + #' @return list_params: list of parameters indicating quality of fit (list) + if (is.null(sum_fit)) { + mass_vector_int <- mass_vector + int_vector_int <- int_vector + # get new fit quality + fq_new <- mean(abs((scale * dnorm(mass_vector_int, mu_first, sigma)) - int_vector_int) / + rep((max(scale * dnorm(mass_vector_int, mu_first, sigma)) / 2), length(mass_vector_int))) + } else { + sum_fit_int <- sum_fit + int_vector_int <- int_vector + mass_vector_int <- mass_vector + # get new fit quality + fq_new <- mean(abs(sum_fit_int - int_vector_int) / rep(max(sum_fit_int) /2, length(sum_fit_int))) + } + + # Prevent division by 0 + if (is.nan(fq_new)) fq_new <- 1 + + list_params <- list("fq_new" = fq_new, "x_int" = mass_vector_int, "y_int" = int_vector_int) + return(list_params) +} + +estimate_area <- function(mass_max, resol, scale, sigma) { + #' Estimate area of Gaussian curve + #' + #' @param mass_max: Value for m/z at maximum intensity of a peak (float) + #' @param resol: Value for resolution (integer) + #' @param scale: Value for peak width (float) + #' @param sigma: Value for standard deviation (float) + #' @param int_factor: Value used to calculate area under Gaussian curve (integer) + #' + #' @return area_curve: Value for area under the Gaussian curve (float) + + # Number used to calculate area under Gaussian curve + int_factor <- 1 * 10^5 + + # avoid vectors that are too big (cannot allocate vector of size ...) + if (mass_max > 1200) return(0) + + # generate a mass_vector with equally spaced m/z values + fwhm <- get_fwhm(mass_max, resol) + mz_min <- mass_max - 2 * fwhm + mz_max <- mass_max + 2 * fwhm + mz_range <- mz_max - mz_min + mass_vector2 <- seq(mz_min, mz_max, length = mz_range * int_factor) + + # estimate area under the curve + area_curve <- sum(scale * dnorm(mass_vector2, mass_max, sigma)) / 100 + + return(area_curve) +} + +get_fwhm <- function(query_mass, resol) { + #' Calculate fwhm (full width at half maximum intensity) for a peak + #' + #' @param query_mass: Value for mass (float) + #' @param resol: Value for resolution (integer) + #' + #' @return fwhm: Value for full width at half maximum (float) + + # set aberrant values of query_mass to zero + if (is.nan(query_mass)) query_mass <- 0 + if (is.na(query_mass)) query_mass <- 0 + if (is.null(query_mass)) query_mass <- 0 + if (query_mass < 0) query_mass <- 0 + # calculate resolution at given m/z value + resol_mz <- resol * (1 / sqrt(2) ^ (log2(query_mass / 200))) + # calculate full width at half maximum + fwhm <- query_mass / resol_mz + return(fwhm) +} + +fit_optim <- function(mass_vector, int_vector, resol, scanmode) { + #' Determine optimized fit of Gaussian curve to small region of m/z + #' + #' @param mass_vector: Vector of m/z values for a region of interest (float) + #' @param int_vector: Vector of intensities for a region of interest (float) + #' @param resol: Value for resolution (integer) + #' @param scanmode: Scan mode, positive or negative (string) + #' + #' @return roi_value_list: list of fit values for region of interest (list) + + # Number used to calculate area under Gaussian curve + int_factor <- 1 * 10^5 + factor <- 1.5 + # Find the index in int_vector with the highest intensity + max_index <- which(int_vector == max(int_vector))[1] + mass_max <- mass_vector[max_index] + int_max <- int_vector[max_index] + # get peak width + fwhm <- get_fwhm(mass_max, resol) + # simplify the peak shape: represent it by a triangle + mass_max_simple <- c(mass_max - factor * fwhm, mass_max, mass_max + factor * fwhm) + int_max_simple <- c(0, int_max, 0) + + # define mass_diff as difference between last and first value of mass_max_simple + mass_diff <- mass_max_simple[length(mass_max_simple)] - mass_max_simple[1] + # generate a second mass_vector with equally spaced m/z values + mass_vector2 <- seq(mass_max_simple[1], mass_max_simple[length(mass_max_simple)], + length = mass_diff * int_factor) + sigma <- get_stdev(mass_vector2, int_max_simple) + # define optimization function for optim based on normal distribution + opt_f <- function(p, mass_vector, int_vector, sigma, mass_max) { + curve <- p * dnorm(mass_vector, mass_max, sigma) + return((max(curve) - max(int_vector))^2) + } + + # get optimal value for fitted Gaussian curve + opt_fit <- optimize(opt_f, c(0, 100000), tol = 0.0001, mass_vector, int_vector, sigma, mass_max) + scale <- opt_fit$minimum + + # get an estimate of the area under the peak + area <- estimate_area(mass_max, resol, scale, sigma) + # put all values for this region of interest into a list + roi_value_list <- list("mean" = mass_max, + "area" = area, + "min" = mass_vector2[1], + "max" = mass_vector2[length(mass_vector2)]) + return(roi_value_list) +} + +within_ppm <- function(mean, scale, sigma, area, mass_vector2, mass_vector, ppm = 4, resol) { + #' Test whether two mass ranges are within ppm distance of each other + #' + #' @param mean: Value for mean m/z (float) + #' @param scale: Initial value used to estimate scaling parameter (integer) + #' @param sigma: Value for standard deviation (float) + #' @param area: Value for area under the curve (float) + #' @param mass_vector2: Vector of equally spaced m/z values (float) + #' @param mass_vector: Vector of m/z values for a region of interest (float) + #' @param ppm: Value for distance between two values of mass (integer) + #' @param resol: Value for resolution (integer) + #' + #' @return list_params: list of parameters indicating quality of fit (list) + + # sort + index <- order(mean) + mean <- mean[index] + scale <- scale[index] + sigma <- sigma[index] + area <- area[index] + + summed <- NULL + remove <- NULL + + if (length(mean) > 1) { + for (i in 2:length(mean)) { + if ((abs(mean[i - 1] - mean[i]) / mean[i - 1]) * 10^6 < ppm) { + + # avoid double occurrance in sum + if ((i - 1) %in% summed) next + + result_values <- sum_curves(mean[i - 1], mean[i], scale[i - 1], scale[i], sigma[i - 1], sigma[i], + mass_vector2, mass_vector, resol) + summed <- c(summed, i - 1, i) + if (is.nan(result_values$mean)) result_values$mean <- 0 + mean[i - 1] <- result_values$mean + mean[i] <- result_values$mean + area[i - 1] <- result_values$area + area[i] <- result_values$area + scale[i - 1] <- result_values$scale + scale[i] <- result_values$scale + sigma[i - 1] <- result_values$sigma + sigma[i] <- result_values$sigma + + remove <- c(remove, i) + } + } + } + + if (length(remove) != 0) { + mean <- mean[-c(remove)] + area <- area[-c(remove)] + scale <- scale[-c(remove)] + sigma <- sigma[-c(remove)] + } + + list_params <- list("mean" = mean, "area" = area, "scale" = scale, "sigma" = sigma, "qual" = NULL) + return(list_params) +} + +sum_curves <- function(mean1, mean2, scale1, scale2, sigma1, sigma2, mass_vector2, mass_vector, resol) { + #' Sum two curves + #' + #' @param mean1: Value for mean m/z of first peak (float) + #' @param mean2: Value for mean m/z of second peak (float) + #' @param scale1: Initial value used to estimate scaling parameter for first peak (integer) + #' @param scale2: Initial value used to estimate scaling parameter for second peak (integer) + #' @param sigma1: Value for standard deviation for first peak (float) + #' @param sigma2: Value for standard deviation for second peak (float) + #' @param mass_vector2: Vector of equally spaced m/z values (float) + #' @param mass_vector: Vector of m/z values for a region of interest (float) + #' @param resol: Value for resolution (integer) + #' + #' @return list_params: list of parameters indicating quality of fit (list) + + sum_fit <- (scale1 * dnorm(mass_vector2, mean1, sigma1)) + (scale2 * dnorm(mass_vector2, mean2, sigma2)) + + mean1_plus2 <- weighted.mean(c(mean1, mean2), c(max(scale1 * dnorm(mass_vector2, mean1, sigma1)), + max(scale2 * dnorm(mass_vector2, mean2, sigma2)))) + + # get new values for parameters + fwhm <- get_fwhm(mean1_plus2, resol) + area <- max(sum_fit) + scale <- scale1 + scale2 + sigma <- (fwhm / 2) * 0.85 + + list_params <- list("mean" = mean1_plus2, "area" = area, "scale" = scale, "sigma" = sigma) + return(list_params) +} + From 9d7a69fcb2a3330bc77e05bbc645008c7793627d Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Mon, 10 Feb 2025 10:03:21 +0100 Subject: [PATCH 036/161] refactor DIMS PeakFinding, flow between scripts --- DIMS/AveragePeaks.R | 72 ++++ DIMS/AveragePeaks.nf | 18 + DIMS/PeakFinding.R | 30 +- DIMS/PeakGrouping.R | 2 +- DIMS/preprocessing/peak_finding_functions.R | 430 +++++++++++--------- 5 files changed, 334 insertions(+), 218 deletions(-) create mode 100644 DIMS/AveragePeaks.R create mode 100644 DIMS/AveragePeaks.nf diff --git a/DIMS/AveragePeaks.R b/DIMS/AveragePeaks.R new file mode 100644 index 00000000..695c08fa --- /dev/null +++ b/DIMS/AveragePeaks.R @@ -0,0 +1,72 @@ +# define parameters +# ppm as fixed value, not the same ppm as in peak grouping +ppm_peak <- 2 + +library(dplyr) + +scanmodes <- c("positive", "negative") + +for (scanmode in scanmodes){ + # get sample names + load(paste0(scanmode, "_repl_pattern.RData")) + sample_names <- names(repl_pattern_filtered) + # initialize + outlist_total <- NULL + # for each biological sample, average peaks in technical replicates + for (sample_name in sample_names) { + print(sample_name) + # Initialize per sample + peaklist_allrepl <- NULL + nr_repl_persample <- 0 + # averaged_peaks <- matrix(0, nrow = 10 ^ 7, ncol = 6) # how big does it need to be? + averaged_peaks <- matrix(0, nrow = 0, ncol = 6) # append + colnames(averaged_peaks) <- c("samplenr", "mzmed.pkt", "fq", "mzmin.pkt", "mzmax.pkt", "height.pkt") + # load RData files of technical replicates belonging to biological sample + sample_techreps_file <- repl_pattern_filtered[sample_name][[1]] + for (file_nr in 1:length(sample_techreps_file)) { + print(sample_techreps_file[file_nr]) + tech_repl_file <- paste0(sample_techreps_file[file_nr], "_positive.RData") + tech_repl <- get(load(tech_repl_file)) + # combine data for all technical replicates + peaklist_allrepl <- rbind(peaklist_allrepl, tech_repl) + # count number of replicates for each biological sample + nr_repl_persample <- nr_repl_persample + 1 + } + # sort on mass + peaklist_allrepl_df <- as.data.frame(peaklist_allrepl) + peaklist_allrepl_df$mzmed.pkt <- as.numeric(peaklist_allrepl_df$mzmed.pkt) + peaklist_allrepl_df$height.pkt <- as.numeric(peaklist_allrepl_df$height.pkt) + # peaklist_allrepl_sorted <- peaklist_allrepl_df %>% arrange(desc(height.pkt)) + peaklist_allrepl_sorted <- peaklist_allrepl_df %>% arrange(mzmed.pkt) + # average over technical replicates + while (nrow(peaklist_allrepl_sorted) > 1) { + # store row numbers + peaklist_allrepl_sorted$rownr <- 1:nrow(peaklist_allrepl_sorted) + # find the peaks in the dataset with corresponding m/z plus or minus tolerance + reference_mass <- peaklist_allrepl_sorted$mzmed.pkt[1] + mz_tolerance <- (reference_mass * ppm_peak) / 10^6 + minmz_ref <- reference_mass - mz_tolerance + maxmz_ref <- reference_mass + mz_tolerance + select_peak_indices <- which((peaklist_allrepl_sorted$mzmed.pkt > minmz_ref) & (peaklist_allrepl_sorted$mzmed.pkt < maxmz_ref)) + select_peaks <- peaklist_allrepl_sorted[select_peak_indices, ] + nrsamples <- length(select_peak_indices) + # put averaged intensities into a new row and append to averaged_peaks + averaged_1peak <- matrix(0, nrow = 1, ncol = 6) + colnames(averaged_1peak) <- c("samplenr", "mzmed.pkt", "fq", "mzmin.pkt", "mzmax.pkt", "height.pkt") + # calculate m/z values for peak group + averaged_1peak[1, "mzmed.pkt"] <- mean(select_peaks$mzmed.pkt) + averaged_1peak[1, "mzmin.pkt"] <- min(select_peaks$mzmed.pkt) + averaged_1peak[1, "mzmax.pkt"] <- max(select_peaks$mzmed.pkt) + averaged_1peak[1, "fq"] <- nrsamples + averaged_1peak[1, "height.pkt"] <- mean(select_peaks$height.pkt) + # put intensities into proper columns + peaklist_allrepl_sorted <- peaklist_allrepl_sorted[-select_peaks$rownr, ] + averaged_peaks <- rbind(averaged_peaks, averaged_1peak) + } + # add sample name to first column and append to outlist_total for all samples + averaged_peaks[ , "samplenr"] <- sample_name + outlist_total <- rbind(outlist_total, averaged_peaks) + } + save(outlist_total, file = paste0("AvgPeaks_", scanmode, ".RData")) +} + diff --git a/DIMS/AveragePeaks.nf b/DIMS/AveragePeaks.nf new file mode 100644 index 00000000..9d49f5f8 --- /dev/null +++ b/DIMS/AveragePeaks.nf @@ -0,0 +1,18 @@ +process AveragePeaks { + tag "DIMS AveragePeaks" + label 'AveragePeaks' + container = 'docker://umcugenbioinf/dims:1.3' + shell = ['/bin/bash', '-euo', 'pipefail'] + + input: + path(rdata_files) + path(replication_pattern) + + output: + path 'AvgPeaks_*.RData' + + script: + """ + Rscript ${baseDir}/CustomModules/DIMS/AveragePeaks.R + """ +} diff --git a/DIMS/PeakFinding.R b/DIMS/PeakFinding.R index 738a0bc7..c54a64cf 100644 --- a/DIMS/PeakFinding.R +++ b/DIMS/PeakFinding.R @@ -1,30 +1,26 @@ # define parameters cmd_args <- commandArgs(trailingOnly = TRUE) -replicate_mzmlfile <- cmd_args[1] +replicate_rdatafile <- cmd_args[1] breaks_file <- cmd_args[2] resol <- as.numeric(cmd_args[3]) -scripts_dir <- cmd_args[4] +preprocessing_scripts_dir <- cmd_args[4] peak_thresh <- 2000 # load in function scripts -preprocessing_scripts_dir <- gsub("Utils", "preprocessing", scripts_dir) source(paste0(preprocessing_scripts_dir, "peak_finding_functions.R")) load(breaks_file) # Load output of AssignToBins for a sample -sample_techrepl <- get(load(replicate_mzmlfile)) -scanmodes <- c("positive", "negative") -ints_pos <- peak_list$pos -ints_neg <- peak_list$neg +sample_techrepl <- get(load(replicate_rdatafile)) +techrepl_name <- colnames(peak_list$pos)[1] # Initialize options(digits = 16) # run the findPeaks function -techrepl_name <- colnames(ints_pos)[1] - +scanmodes <- c("positive", "negative") for (scanmode in scanmodes) { # turn dataframe with intensities into a named list if (scanmode == "positive") { @@ -36,18 +32,12 @@ for (scanmode in scanmodes) { ints_fullrange <- as.vector(ints_perscanmode) names(ints_fullrange) <- rownames(ints_perscanmode) - start.time <- Sys.time() - # look for m/z range for all peaks - allpeaks_values <- search_mzrange(ints_fullrange, resol, techrepl_name, scanmode, peak_thresh) - - end.time <- Sys.time() - time.taken <- end.time - start.time - time.taken + allpeaks_values <- search_mzrange(ints_fullrange, resol, techrepl_name, peak_thresh) # turn the list into a dataframe - outlist_persample <- NULL - outlist_persample <- cbind("samplenr" = allpeaks_values$nr, + outlist_techrep <- NULL + outlist_techrep <- cbind("samplenr" = allpeaks_values$nr, "mzmed.pkt" = allpeaks_values$mean, "fq" = allpeaks_values$qual, "mzmin.pkt" = allpeaks_values$min, @@ -55,10 +45,10 @@ for (scanmode in scanmodes) { "height.pkt" = allpeaks_values$area) # remove peaks with height = 0 - outlist_persample <- outlist_persample[outlist_persample[, "height.pkt"] != 0, ] + outlist_techrep <- outlist_techrep[outlist_techrep[, "height.pkt"] != 0, ] # save output to file - save(outlist_persample, file = paste0(techrepl_name, "_", scanmode, ".RData")) + save(outlist_techrep, file = paste0(techrepl_name, "_", scanmode, ".RData")) # generate text output to log file on number of spikes for this sample # spikes are peaks that are too narrow, e.g. 1 data point diff --git a/DIMS/PeakGrouping.R b/DIMS/PeakGrouping.R index 53e42de3..69fcde9c 100644 --- a/DIMS/PeakGrouping.R +++ b/DIMS/PeakGrouping.R @@ -22,7 +22,7 @@ if (grepl("negative", basename(hmdb_part_file))) { batch_number <- strsplit(basename(hmdb_part_file), ".", fixed = TRUE)[[1]][2] # load file with spectrum peaks -spec_peaks_file <- paste0("SpectrumPeaks_", scanmode, ".RData") +spec_peaks_file <- paste0("AvgPeaks_", scanmode, ".RData") load(spec_peaks_file) outlist_copy <- outlist_total rm(outlist_total) diff --git a/DIMS/preprocessing/peak_finding_functions.R b/DIMS/preprocessing/peak_finding_functions.R index 331d563b..947fea1f 100644 --- a/DIMS/preprocessing/peak_finding_functions.R +++ b/DIMS/preprocessing/peak_finding_functions.R @@ -1,18 +1,14 @@ # functions for peak finding -search_mzrange <- function(ints_fullrange, resol, sample_name, scanmode, peak_thresh) { +search_mzrange <- function(ints_fullrange, resol, sample_name, peak_thresh) { #' Divide the full m/z range into regions of interest with min, max and mean m/z #' #' @param ints_fullrange: Named list of intensities (float) #' @param resol: Value for resolution (integer) #' @param sample_name: Sample name (string) - #' @param scanmode: Scan mode, positive or negative (string) #' @param peak_thresh: Value for noise level threshold (integer) #' #' @return allpeaks_values: list of m/z regions of interest - # Number used to calculate area under Gaussian curve - int_factor <- 1 * 10^5 - # initialize list to store results for all peaks allpeaks_values <- list("mean" = NULL, "area" = NULL, "nr" = NULL, "min" = NULL, "max" = NULL, "qual" = NULL, "spikes" = 0) @@ -50,27 +46,27 @@ search_mzrange <- function(ints_fullrange, resol, sample_name, scanmode, peak_th #sub_range <- int_vector #names(sub_range) <- mass_vector #allpeaks_values <- search_mzrange(sub_range, allpeaks_values, resol, - # sample_name, scanmode, peak_thresh) + # sample_name, peak_thresh) # A proper peak needs to have at least 3 intensities above threshold } else if (length(int_vector) > 3) { # check if the sum of intensities is above zero. Why is this necessary? #if (sum(int_vector) == 0) next # define mass_diff as difference between last and first value of mass_vector - mass_diff <- mass_vector[length(mass_vector)] - mass_vector[1] + # mass_diff <- mass_vector[length(mass_vector)] - mass_vector[1] # generate a second mass_vector with equally spaced m/z values - mass_vector2 <- seq(mass_vector[1], mass_vector[length(mass_vector)], - length = mass_diff * int_factor) + mass_vector_eq <- seq(mass_vector[1], mass_vector[length(mass_vector)], + length = 10 * length(mass_vector)) # Find the index in int_vector with the highest intensity # max_index <- which(int_vector == max(int_vector)) # get initial fit values - roi_values <- fit_gaussian(mass_vector2, mass_vector, int_vector, # max_index, - resol, force = length(max_index), - use_bounds = FALSE, scanmode) + roi_values <- fit_gaussian(mass_vector_eq, mass_vector, int_vector, + resol, force_nr = length(max_index), + use_bounds = FALSE) if (roi_values$qual[1] == 1) { # get optimized fit values - roi_values <- fit_optim(mass_vector, int_vector, resol, scanmode) + roi_values <- fit_optim(mass_vector, int_vector, resol) # add region of interest to list of all peaks allpeaks_values$mean <- c(allpeaks_values$mean, roi_values$mean) allpeaks_values$area <- c(allpeaks_values$area, roi_values$area) @@ -93,7 +89,7 @@ search_mzrange <- function(ints_fullrange, resol, sample_name, scanmode, peak_th } else { - roi_values <- fit_optim(mass_vector, int_vector, resol, scanmode) + roi_values <- fit_optim(mass_vector, int_vector, resol) allpeaks_values$mean <- c(allpeaks_values$mean, roi_values$mean) allpeaks_values$area <- c(allpeaks_values$area, roi_values$area) allpeaks_values$nr <- c(allpeaks_values$nr, sample_name) @@ -109,24 +105,19 @@ search_mzrange <- function(ints_fullrange, resol, sample_name, scanmode, peak_th return(allpeaks_values) } -# remove plot sections (commented out) -fit_gaussian <- function(mass_vector2, mass_vector, int_vector, - resol, force, use_bounds, scanmode) { +fit_gaussian <- function(mass_vector_eq, mass_vector, int_vector, + resol, force_nr, use_bounds) { #' Fit 1, 2, 3 or 4 Gaussian peaks in small region of m/z #' - #' @param mass_vector2: Vector of equally spaced m/z values (float) + #' @param mass_vector_eq: Vector of equally spaced m/z values (float) #' @param mass_vector: Vector of m/z values for a region of interest (float) #' @param int_vector: Value used to calculate area under Gaussian curve (integer) #' @param resol: Value for resolution (integer) - #' @param force: Number of local maxima in int_vector (integer) + #' @param force_nr: Number of local maxima in int_vector (integer) #' @param use_bounds: Boolean to indicate whether boundaries are to be used - #' @param scanmode: Scan mode, positive or negative (string) #' #' @return roi_value_list: list of fit values for region of interest (list) - # Number used to calculate area under Gaussian curve - int_factor <- 1 * 10^5 - # Find the index in int_vector with the highest intensity max_index <- which(int_vector == max(int_vector)) @@ -138,20 +129,20 @@ fit_gaussian <- function(mass_vector2, mass_vector, int_vector, peak_max <- NULL fit_quality1 <- 0.15 fit_quality <- 0.2 + # set initial value for scale factor + scale_factor <- 2 # One local maximum: - if (force == 1) { - # determine fit values for 1 Gaussian peak (mean, scale, sigma, qual) - fit_values <- fit_1peak(mass_vector2, mass_vector, int_vector, max_index, resol, + if (force_nr == 1) { + # determine fit values for 1 Gaussian peak + fit_values <- fit_1peak(mass_vector_eq, mass_vector, int_vector, max_index, resol, fit_quality1, use_bounds) - # set initial value for scale factor - scale <- 2 # test if the mean is outside the m/z range if (fit_values$mean[1] < mass_vector[1] || fit_values$mean[1] > mass_vector[length(mass_vector)]) { # run this function again with fixed boundaries - return(fit_gaussian(mass_vector2, mass_vector, int_vector, resol, - force = 1, use_bounds = TRUE, scanmode)) + return(fit_gaussian(mass_vector_eq, mass_vector, int_vector, resol, + force_nr = 1, use_bounds = TRUE)) } else { # test if the fit is bad if (fit_values$qual > fit_quality1) { @@ -163,12 +154,12 @@ fit_gaussian <- function(mass_vector2, mass_vector, int_vector, new_index <- c(new_index, 2 * new_index) } # run this function again with two local maxima - return(fit_gaussian(mass_vector2, mass_vector, int_vector, - resol, force = 2, use_bounds = FALSE, scanmode)) + return(fit_gaussian(mass_vector_eq, mass_vector, int_vector, + resol, force_nr = 2, use_bounds = FALSE)) # good fit } else { peak_mean <- c(peak_mean, fit_values$mean) - peak_area <- c(peak_area, estimate_area(fit_values$mean, resol, fit_values$scale, + peak_area <- c(peak_area, estimate_area(fit_values$mean, resol, fit_values$scale_factor, fit_values$sigma)) peak_qual <- fit_values$qual peak_min <- mass_vector[1] @@ -177,9 +168,9 @@ fit_gaussian <- function(mass_vector2, mass_vector, int_vector, } #### Two local maxima; need at least 6 data points for this #### - } else if (force == 2 && (length(mass_vector) > 6)) { - # determine fit values for 2 Gaussian peaks (mean, scale, sigma, qual) - fit_values <- fit_2peaks(mass_vector2, mass_vector, int_vector, max_index, resol, + } else if (force_nr == 2 && (length(mass_vector) > 6)) { + # determine fit values for 2 Gaussian peaks + fit_values <- fit_2peaks(mass_vector_eq, mass_vector, int_vector, max_index, resol, use_bounds, fit_quality) # test if one of the means is outside the m/z range if (fit_values$mean[1] < mass_vector[1] || fit_values$mean[1] > mass_vector[length(mass_vector)] || @@ -187,8 +178,8 @@ fit_gaussian <- function(mass_vector2, mass_vector, int_vector, # check if fit quality is bad if (fit_values$qual > fit_quality) { # run this function again with fixed boundaries - return(fit_gaussian(mass_vector2, mass_vector, int_vector, resol, - force = 2, use_bounds = TRUE, scanmode)) + return(fit_gaussian(mass_vector_eq, mass_vector, int_vector, resol, + force_nr = 2, use_bounds = TRUE)) } else { # check which mean is outside range and remove it from the list of means # NB: peak_mean and other variables have not been given values from 2-peak fit yet! @@ -217,9 +208,9 @@ fit_gaussian <- function(mass_vector2, mass_vector, int_vector, new_index <- c(new_index, 2 * new_index, 3 * new_index) } # run this function again with three local maxima - return(fit_gaussian(mass_vector2, mass_vector, int_vector, - resol, force = 3, use_bounds = FALSE, scanmode)) - # good fit, all means are within m/z range + return(fit_gaussian(mass_vector_eq, mass_vector, int_vector, + resol, force_nr = 3, use_bounds = FALSE)) + # good fit, all means are within m/z range } else { # check if means are within 3 ppm and sum if so tmp <- fit_values$qual @@ -227,8 +218,8 @@ fit_gaussian <- function(mass_vector2, mass_vector, int_vector, nr_means <- length(fit_values$mean) while (nr_means != nr_means_new) { nr_means <- length(fit_values$mean) - fit_values <- within_ppm(fit_values$mean, fit_values$scale, fit_values$sigma, fit_values$area, - mass_vector2, mass_vector, ppm = 4, resol) + fit_values <- within_ppm(fit_values$mean, fit_values$scale_factor, fit_values$sigma, fit_values$area, + mass_vector_eq, mass_vector, ppm = 4, resol) nr_means_new <- length(fit_values$mean) } # restore original quality score @@ -245,16 +236,15 @@ fit_gaussian <- function(mass_vector2, mass_vector, int_vector, } } else { # More than two local maxima; fit 1 peak. - scale <- 2 fit_quality1 <- 0.40 use_bounds <- TRUE max_index <- which(int_vector == max(int_vector)) - fit_values <- fit_1peak(mass_vector2, mass_vector, int_vector, max_index, resol, + fit_values <- fit_1peak(mass_vector_eq, mass_vector, int_vector, max_index, resol, fit_quality1, use_bounds) # check for bad fit if (fit_values$qual > fit_quality1) { # get fit values from fit_optim - fit_values <- fit_optim(mass_vector, int_vector, resol, scanmode) + fit_values <- fit_optim(mass_vector, int_vector, resol) peak_mean <- c(peak_mean, fit_values$mean) peak_area <- c(peak_area, fit_values$area) peak_min <- fit_values$min @@ -262,7 +252,7 @@ fit_gaussian <- function(mass_vector2, mass_vector, int_vector, peak_qual <- 0 } else { peak_mean <- c(peak_mean, fit_values$mean) - peak_area <- c(peak_area, estimate_area(fit_values$mean, resol, fit_values$scale, fit_values$sigma)) + peak_area <- c(peak_area, estimate_area(fit_values$mean, resol, fit_values$scale_factor, fit_values$sigma)) peak_qual <- fit_values$qual peak_min <- mass_vector[1] peak_max <- mass_vector[length(mass_vector)] @@ -279,11 +269,11 @@ fit_gaussian <- function(mass_vector2, mass_vector, int_vector, } -fit_1peak <- function(mass_vector2, mass_vector, int_vector, max_index, +fit_1peak <- function(mass_vector_eq, mass_vector, int_vector, max_index, resol, fit_quality, use_bounds) { #' Fit 1 Gaussian peak in small region of m/z #' - #' @param mass_vector2: Vector of equally spaced m/z values (float) + #' @param mass_vector_eq: Vector of equally spaced m/z values (float) #' @param mass_vector: Vector of m/z values for a region of interest (float) #' @param int_vector: Value used to calculate area under Gaussian curve (integer) #' @param max_index: Index in int_vector with the highest intensity (integer) @@ -293,17 +283,17 @@ fit_1peak <- function(mass_vector2, mass_vector, int_vector, max_index, #' #' @return roi_value_list: list of fit values for region of interest (list) - # set initial value for scale - scale <- 2 + # set initial value for scale_factor + scale_factor <- 2 if (length(int_vector) < 3) { message("Range too small, no fit possible!") } else { - if ((length(int_vector) == 4)) { + if (length(int_vector) == 4) { # fit 1 peak - mu <- weighted.mean(mass_vector, int_vector) + weighted_mu <- weighted.mean(mass_vector, int_vector) sigma <- get_stdev(mass_vector, int_vector) - fitted_peak <- fit_1gaussian(mass_vector, int_vector, sigma, mu, use_bounds) + fitted_peak <- optimize_1gaussian(mass_vector, int_vector, sigma, weighted_mu, scale_factor, use_bounds) } else { # set range vector if ((length(mass_vector) - length(max_index)) < 2) { @@ -311,71 +301,79 @@ fit_1peak <- function(mass_vector2, mass_vector, int_vector, max_index, } else if (length(max_index) < 2) { range1 <- c(1:5) } else { - range1 <- c(max_index[1] - 2, max_index[1] - 1, max_index[1], max_index[1] + 1, max_index[1] + 2) + range1 <- seq(from = (max_index[1] - 2), to = (max_index[1] + 2)) + } + # remove zero at the beginning of range1 + if (range1[1] == 0) { + range1 <- range1[-1] } - if (range1[1] == 0) range1 <- range1[-1] # remove NA if (length(which(is.na(int_vector[range1]))) != 0) { range1 <- range1[-which(is.na(int_vector[range1]))] } # fit 1 peak - mu <- weighted.mean(mass_vector[range1], int_vector[range1]) + weighted_mu <- weighted.mean(mass_vector[range1], int_vector[range1]) sigma <- get_stdev(mass_vector[range1], int_vector[range1]) - fitted_peak <- fit_1gaussian(mass_vector, int_vector, sigma, mu, use_bounds) + fitted_peak <- optimize_1gaussian(mass_vector, int_vector, sigma, weighted_mu, scale_factor, use_bounds) } - p1 <- fitted_peak$par + fitted_mz <- fitted_peak$par[1] + fitted_nr <- fitted_peak$par[2] - # get new value for fit quality and scale - fq_new <- get_fit_quality(mass_vector, int_vector, p1[1], p1[1], resol, p1[2], sigma)$fq_new - scale_new <- 1.2 * scale + # get new value for fit quality and scale_factor + fq_new <- get_fit_quality(mass_vector, int_vector, fitted_mz, resol, fitted_nr, sigma)$fq_new + scale_factor_new <- 1.2 * scale_factor # bad fit if (fq_new > fit_quality) { # optimize scaling factor fq <- 0 - scale <- 0 - if (sum(int_vector) > sum(p1[2] * dnorm(mass_vector, p1[1], sigma))) { - while ((round(fq, digits = 3) != round(fq_new, digits = 3)) && (scale_new < 10000)) { + scale_factor <- 0 + if (sum(int_vector) > sum(params1[2] * dnorm(mass_vector, fitted_mz, sigma))) { + # increase scale_factor until convergence + while ((round(fq, digits = 3) != round(fq_new, digits = 3)) && (scale_factor_new < 10000)) { fq <- fq_new - scale <- scale_new + scale_factor <- scale_factor_new # fit 1 peak - fitted_peak <- fit_1gaussian(mass_vector, int_vector, sigma, mu, use_bounds) - p1 <- fitted_peak$par - # get new value for fit quality and scale - fq_new <- get_fit_quality(mass_vector, int_vector, p1[1], p1[1], resol, p1[2], sigma)$fq_new - scale_new <- 1.2 * scale + fitted_peak <- optimize_1gaussian(mass_vector, int_vector, sigma, weighted_mu, scale_factor, use_bounds) + params1 <- fitted_peak$par + # get new value for fit quality and scale_factor + fq_new <- get_fit_quality(mass_vector, int_vector, fitted_mz, resol, fitted_nr, sigma)$fq_new + scale_factor_new <- 1.2 * scale_factor } } else { - while ((round(fq, digits = 3) != round(fq_new, digits = 3)) && (scale_new < 10000)) { + # decrease scale_factor until convergence + while ((round(fq, digits = 3) != round(fq_new, digits = 3)) && (scale_factor_new < 10000)) { fq <- fq_new - scale <- scale_new + scale_factor <- scale_factor_new # fit 1 peak - fitted_peak <- fit_1gaussian(mass_vector, int_vector, sigma, mu, use_bounds) - p1 <- fitted_peak$par - # get new value for fit quality and scale - fq_new <- get_fit_quality(mass_vector, int_vector, p1[1], p1[1], resol, p1[2], sigma)$fq_new - scale_new <- 0.8 * scale + fitted_peak <- optimize_1gaussian(mass_vector, int_vector, sigma, weighted_mu, scale_factor, use_bounds) + params1 <- fitted_peak$par + # get new value for fit quality and scale_factor + fq_new <- get_fit_quality(mass_vector, int_vector, fitted_mz, resol, fitted_nr, sigma)$fq_new + scale_factor_new <- 0.8 * scale_factor + print(scale_factor_new) } } - # use optimized scale factor to fit 1 peak + # use optimized scale_factor factor to fit 1 peak if (fq < fq_new) { - fitted_peak <- fit_1gaussian(mass_vector, int_vector, sigma, mu, use_bounds) - p1 <- fitted_peak$par + fitted_peak <- optimize_1gaussian(mass_vector, int_vector, sigma, weighted_mu, scale_factor, use_bounds) + fitted_mz <- fitted_peak$par[1] + fitted_nr <- fitted_peak$par[2] fq_new <- fq } } } - roi_value_list <- list("mean" = p1[1], "scale" = p1[2], "sigma" = sigma, "qual" = fq_new) + roi_value_list <- list("mean" = fitted_mz, "scale_factor" = fitted_nr, "sigma" = sigma, "qual" = fq_new) return(roi_value_list) } -fit_2peaks <- function(mass_vector2, mass_vector, int_vector, max_index, resol, use_bounds = FALSE, +fit_2peaks <- function(mass_vector_eq, mass_vector, int_vector, max_index, resol, use_bounds = FALSE, fit_quality) { - #' Fit 2 Gaussian peaks in small region of m/z + #' Fit 2 Gaussian peaks in a small region of m/z #' - #' @param mass_vector2: Vector of equally spaced m/z values (float) + #' @param mass_vector_eq: Vector of equally spaced m/z values (float) #' @param mass_vector: Vector of m/z values for a region of interest (float) #' @param int_vector: Value used to calculate area under Gaussian curve (integer) #' @param max_index: Index in int_vector with the highest intensity (integer) @@ -390,73 +388,93 @@ fit_2peaks <- function(mass_vector2, mass_vector, int_vector, max_index, resol, peak_scale <- NULL peak_sigma <- NULL - # set range vectors for 2 peaks - range1 <- c(max_index[1] - 2, max_index[1] - 1, max_index[1], max_index[1] + 1, max_index[1] + 2) - if (range1[1] == 0) range1 <- range1[-1] - range2 <- c(max_index[2] - 2, max_index[2] - 1, max_index[2], max_index[2] + 1, max_index[2] + 2) - if (length(mass_vector) < range2[length(range2)]) range2 <- range2[-length(range2)] + # set range vectors for first peak + range1 <- seq(from = (max_index[1] - 2), to = (max_index[1] + 2)) + # remove zero at the beginning of range1 + if (range1[1] == 0) { + range1 <- range1[-1] + } + # set range vectors for second peak + range2 <- seq(from = (max_index[2] - 2), to = (max_index[2] + 2)) + # if range2 ends outside mass_vector, shorten it + if (length(mass_vector) < range2[length(range2)]) { + range2 <- range2[-length(range2)] + } + # check whether the two ranges overlap range1 <- check_overlap(range1, range2)[[1]] range2 <- check_overlap(range1, range2)[[2]] - # check for negative or 0 + # check for negative or 0 values in range1 or range2 remove <- which(range1 < 1) - if (length(remove) > 0) range1 <- range1[-remove] + if (length(remove) > 0) { + range1 <- range1[-remove] + } remove <- which(range2 < 1) - if (length(remove) > 0) range2 <- range2[-remove] + if (length(remove) > 0) { + range2 <- range2[-remove] + } # remove NA - if (length(which(is.na(int_vector[range1]))) != 0) range1 <- range1[-which(is.na(int_vector[range1]))] - if (length(which(is.na(int_vector[range2]))) != 0) range2 <- range2[-which(is.na(int_vector[range2]))] + if (length(which(is.na(int_vector[range1]))) != 0) { + range1 <- range1[-which(is.na(int_vector[range1]))] + } + if (length(which(is.na(int_vector[range2]))) != 0) { + range2 <- range2[-which(is.na(int_vector[range2]))] + } # fit 2 peaks, first separately, then together - mu1 <- weighted.mean(mass_vector[range1], int_vector[range1]) + weighted_mu1 <- weighted.mean(mass_vector[range1], int_vector[range1]) sigma1 <- get_stdev(mass_vector[range1], int_vector[range1]) - fitted_peak <- fit_1gaussian(mass_vector[range1], int_vector[range1], sigma1, mu1, scale, use_bounds) - p1 <- fitted_peak$par + fitted_peak1 <- optimize_1gaussian(mass_vector[range1], int_vector[range1], sigma1, weighted_mu1, scale_factor, use_bounds) + fitted_mz1 <- fitted_peak1$par[1] + fitted_nr1 <- fitted_peak1$par[2] # second peak - mu2 <- weighted.mean(mass_vector[range2], int_vector[range2]) + weighted_mu2 <- weighted.mean(mass_vector[range2], int_vector[range2]) sigma2 <- get_stdev(mass_vector[range2], int_vector[range2]) - fitted_peak <- fit_1gaussian(mass_vector[range2], int_vector[range2], sigma2, mu2, scale, use_bounds) - p2 <- fitted_peak$par + fitted_peak2 <- optimize_1gaussian(mass_vector[range2], int_vector[range2], sigma2, weighted_mu2, scale_factor, use_bounds) + fitted_mz2 <- fitted_peak2$par[1] + fitted_nr2 <- fitted_peak2$par[2] # combined - fitted_2peaks <- fit_2gaussians(mass_vector, int_vector, sigma1, sigma2, p1[1], p1[2], p2[1], p2[2], use_bounds) - pc <- fitted_2peaks$par + fitted_2peaks <- optimize_2gaussians(mass_vector, int_vector, sigma1, sigma2, + fitted_mz1, fitted_nr1, + fitted_mz2, fitted_nr2, use_bounds) + fitted_2peaks_mz1 <- fitted_2peaks$par[1] + fitted_2peaks_nr1 <- fitted_2peaks$par[2] + fitted_2peaks_mz2 <- fitted_2peaks$par[3] + fitted_2peaks_nr2 <- fitted_2peaks$par[4] # get fit quality - if (is.null(sigma2)) sigma2 <- sigma1 - sum_fit <- (pc[2] * dnorm(mass_vector, pc[1], sigma1)) + - (pc[4] * dnorm(mass_vector, pc[3], sigma2)) - fq <- get_fit_quality(mass_vector, int_vector, sort(c(pc[1], pc[3]))[1], sort(c(pc[1], pc[3]))[2], + if (is.null(sigma2)) { + sigma2 <- sigma1 + } + sum_fit <- (fitted_2peaks_nr1 * dnorm(mass_vector, fitted_2peaks_mz1, sigma1)) + + (fitted_2peaks_nr2 * dnorm(mass_vector, fitted_2peaks_mz2, sigma2)) + lowest_mz <- sort(c(fitted_2peaks_mz1, fitted_2peaks_mz2))[1] + fq_new <- get_fit_quality(mass_vector, int_vector, lowest_mz, resol, sum_fit = sum_fit)$fq_new # get parameter values - area1 <- estimate_area(pc[1], resol, pc[2], sigma1, int_factor) - area2 <- estimate_area(pc[3], resol, pc[4], sigma2, int_factor) - peak_area <- c(peak_area, area1) - peak_area <- c(peak_area, area2) - peak_mean <- c(peak_mean, pc[1]) - peak_mean <- c(peak_mean, pc[3]) - peak_scale <- c(peak_scale, pc[2]) - peak_scale <- c(peak_scale, pc[4]) - peak_sigma <- c(peak_sigma, sigma1) - peak_sigma <- c(peak_sigma, sigma2) - - roi_value_list <- list("mean" = peak_mean, "scale" = peak_scale, "sigma" = peak_sigma, "area" = peak_area, "qual" = fq) + area1 <- estimate_area(fitted_2peaks_mz1, resol, fitted_2peaks_nr1, sigma1) + area2 <- estimate_area(fitted_2peaks_mz2, resol, fitted_2peaks_nr2, sigma2) + peak_area <- c(peak_area, area1, area2) + peak_mean <- c(peak_mean, fitted_2peaks_mz1, fitted_2peaks_mz2) + peak_scale <- c(peak_scale, fitted_2peaks_nr1, fitted_2peaks_nr2) + peak_sigma <- c(peak_sigma, sigma1, sigma2) + + roi_value_list <- list("mean" = peak_mean, "scale_factor" = peak_scale, "sigma" = peak_sigma, "area" = peak_area, "qual" = fq_new) return(roi_value_list) } -fit_1gaussian <- function(mass_vector, int_vector, sigma, query_mass, use_bounds) { +optimize_1gaussian <- function(mass_vector, int_vector, sigma, query_mass, scale_factor, use_bounds) { #' Fit a Gaussian curve for a peak with given parameters #' #' @param mass_vector: Vector of masses (float) #' @param int_vector: Vector of intensities (float) #' @param sigma: Value for width of the peak (float) #' @param query_mass: Value for mass at center of peak (float) + #' @param scale_factor: Value for scaling intensities (float) #' @param use_bounds: Boolean to indicate whether boundaries are to be used #' #' @return opt_fit: list of parameters and values describing the optimal fit - # Initial value used to estimate scaling parameter - scale <- 2 - # define optimization function for optim based on normal distribution opt_f <- function(params) { d <- params[2] * dnorm(mass_vector, mean = params[1], sd = sigma) @@ -467,19 +485,19 @@ fit_1gaussian <- function(mass_vector, int_vector, sigma, query_mass, use_bounds lower <- c(mass_vector[1], 0, mass_vector[1], 0) upper <- c(mass_vector[length(mass_vector)], Inf, mass_vector[length(mass_vector)], Inf) # get optimal value for fitted Gaussian curve - opt_fit <- optim(c(as.numeric(query_mass), as.numeric(scale)), + opt_fit <- optim(c(as.numeric(query_mass), as.numeric(scale_factor)), opt_f, control = list(maxit = 10000), method = "L-BFGS-B", lower = lower, upper = upper) } else { - opt_fit <- optim(c(as.numeric(query_mass), as.numeric(scale)), + opt_fit <- optim(c(as.numeric(query_mass), as.numeric(scale_factor)), opt_f, control = list(maxit = 10000)) } return(opt_fit) } -fit_2gaussians <- function(mass_vector, int_vector, sigma1, sigma2, - query_mass1, scale1, - query_mass2, scale2, use_bounds) { +optimize_2gaussians <- function(mass_vector, int_vector, sigma1, sigma2, + query_mass1, scale_factor1, + query_mass2, scale_factor2, use_bounds) { #' Fit two Gaussian curves for a peak with given parameters #' #' @param mass_vector: Vector of masses (float) @@ -487,9 +505,9 @@ fit_2gaussians <- function(mass_vector, int_vector, sigma1, sigma2, #' @param sigma1: Value for width of the first peak (float) #' @param sigma2: Value for width of the second peak (float) #' @param query_mass1: Value for mass at center of first peak (float) - #' @param scale1: Value for scaling intensities for first peak (float) + #' @param scale_factor1: Value for scaling intensities for first peak (float) #' @param query_mass2: Value for mass at center of second peak (float) - #' @param scale2: Value for scaling intensities for second peak (float) + #' @param scale_factor2: Value for scaling intensities for second peak (float) #' @param use_bounds: Boolean to indicate whether boundaries are to be used #' #' @return opt_fit: list of parameters and values describing the optimal fit @@ -506,27 +524,27 @@ fit_2gaussians <- function(mass_vector, int_vector, sigma1, sigma2, lower <- c(mass_vector[1], 0, mass_vector[1], 0) upper <- c(mass_vector[length(mass_vector)], Inf, mass_vector[length(mass_vector)], Inf) # get optimal value for 2 fitted Gaussian curves - if (is.null(query_mass2) && is.null(scale2) && is.null(sigma2)) { + if (is.null(query_mass2) && is.null(scale_factor2) && is.null(sigma2)) { sigma2 <- sigma1 - opt_fit <- optim(c(as.numeric(query_mass1), as.numeric(scale1), - as.numeric(query_mass1), as.numeric(scale1)), + opt_fit <- optim(c(as.numeric(query_mass1), as.numeric(scale_factor1), + as.numeric(query_mass1), as.numeric(scale_factor1)), opt_f, control = list(maxit = 10000), method = "L-BFGS-B", lower = lower, upper = upper) } else { - opt_fit <- optim(c(as.numeric(query_mass1), as.numeric(scale1), - as.numeric(query_mass2), as.numeric(scale2)), + opt_fit <- optim(c(as.numeric(query_mass1), as.numeric(scale_factor1), + as.numeric(query_mass2), as.numeric(scale_factor2)), opt_f, control = list(maxit = 10000), method = "L-BFGS-B", lower = lower, upper = upper) } } else { - if (is.null(query_mass2) && is.null(scale2) && is.null(sigma2)) { + if (is.null(query_mass2) && is.null(scale_factor2) && is.null(sigma2)) { sigma2 <- sigma1 - opt_fit <- optim(c(as.numeric(query_mass1), as.numeric(scale1), - as.numeric(query_mass1), as.numeric(scale1)), + opt_fit <- optim(c(as.numeric(query_mass1), as.numeric(scale_factor1), + as.numeric(query_mass1), as.numeric(scale_factor1)), opt_f, control = list(maxit = 10000)) } else { - opt_fit <- optim(c(as.numeric(query_mass1), as.numeric(scale1), - as.numeric(query_mass2), as.numeric(scale2)), + opt_fit <- optim(c(as.numeric(query_mass1), as.numeric(scale_factor1), + as.numeric(query_mass2), as.numeric(scale_factor2)), opt_f, control = list(maxit = 10000)) } } @@ -554,15 +572,13 @@ get_stdev <- function(mass_vector, int_vector, resol = 140000) { return(stdev) } -## adapted from getFitQuality.R -# parameter not used: mu_last -get_fit_quality <- function(mass_vector, int_vector, mu_first, mu_last, resol, scale = NULL, sigma = NULL, sum_fit = NULL) { - #' Fit 1 Gaussian peak in small region of m/z +get_fit_quality <- function(mass_vector, int_vector, mu_first, resol, scale_factor = NULL, sigma = NULL, sum_fit = NULL) { + #' Get fit quality for 1 Gaussian peak in small region of m/z #' #' @param mass_vector: Vector of m/z values for a region of interest (float) #' @param int_vector: Value used to calculate area under Gaussian curve (integer) #' @param mu_first: Value for first peak (float) - #' @param scale: Initial value used to estimate scaling parameter (integer) + #' @param scale_factor: Initial value used to estimate scaling parameter (integer) #' @param resol: Value for resolution (integer) #' @param sum_fit: Value indicating quality of fit of Gaussian curve (float) #' @@ -571,8 +587,8 @@ get_fit_quality <- function(mass_vector, int_vector, mu_first, mu_last, resol, s mass_vector_int <- mass_vector int_vector_int <- int_vector # get new fit quality - fq_new <- mean(abs((scale * dnorm(mass_vector_int, mu_first, sigma)) - int_vector_int) / - rep((max(scale * dnorm(mass_vector_int, mu_first, sigma)) / 2), length(mass_vector_int))) + fq_new <- mean(abs((scale_factor * dnorm(mass_vector_int, mu_first, sigma)) - int_vector_int) / + rep((max(scale_factor * dnorm(mass_vector_int, mu_first, sigma)) / 2), length(mass_vector_int))) } else { sum_fit_int <- sum_fit int_vector_int <- int_vector @@ -588,32 +604,27 @@ get_fit_quality <- function(mass_vector, int_vector, mu_first, mu_last, resol, s return(list_params) } -estimate_area <- function(mass_max, resol, scale, sigma) { +estimate_area <- function(mass_max, resol, scale_factor, sigma) { #' Estimate area of Gaussian curve #' #' @param mass_max: Value for m/z at maximum intensity of a peak (float) #' @param resol: Value for resolution (integer) - #' @param scale: Value for peak width (float) + #' @param scale_factor: Value for peak width (float) #' @param sigma: Value for standard deviation (float) - #' @param int_factor: Value used to calculate area under Gaussian curve (integer) #' #' @return area_curve: Value for area under the Gaussian curve (float) - # Number used to calculate area under Gaussian curve - int_factor <- 1 * 10^5 - - # avoid vectors that are too big (cannot allocate vector of size ...) - if (mass_max > 1200) return(0) + # calculate width of peak at half maximum + fwhm <- get_fwhm(mass_max, resol) # generate a mass_vector with equally spaced m/z values - fwhm <- get_fwhm(mass_max, resol) mz_min <- mass_max - 2 * fwhm mz_max <- mass_max + 2 * fwhm mz_range <- mz_max - mz_min - mass_vector2 <- seq(mz_min, mz_max, length = mz_range * int_factor) + mass_vector_eq <- seq(mz_min, mz_max, length = 100) # estimate area under the curve - area_curve <- sum(scale * dnorm(mass_vector2, mass_max, sigma)) / 100 + area_curve <- sum(scale_factor * dnorm(mass_vector_eq, mass_max, sigma)) / 100 return(area_curve) } @@ -625,32 +636,34 @@ get_fwhm <- function(query_mass, resol) { #' @param resol: Value for resolution (integer) #' #' @return fwhm: Value for full width at half maximum (float) - - # set aberrant values of query_mass to zero - if (is.nan(query_mass)) query_mass <- 0 - if (is.na(query_mass)) query_mass <- 0 - if (is.null(query_mass)) query_mass <- 0 - if (query_mass < 0) query_mass <- 0 + + # set aberrant values of query_mass to default value of 200 + if (is.na(query_mass)) { + query_mass <- 200 + } + if (query_mass < 0) { + query_mass <- 200 + } # calculate resolution at given m/z value resol_mz <- resol * (1 / sqrt(2) ^ (log2(query_mass / 200))) # calculate full width at half maximum fwhm <- query_mass / resol_mz + return(fwhm) } -fit_optim <- function(mass_vector, int_vector, resol, scanmode) { +fit_optim <- function(mass_vector, int_vector, resol) { #' Determine optimized fit of Gaussian curve to small region of m/z #' #' @param mass_vector: Vector of m/z values for a region of interest (float) #' @param int_vector: Vector of intensities for a region of interest (float) #' @param resol: Value for resolution (integer) - #' @param scanmode: Scan mode, positive or negative (string) #' #' @return roi_value_list: list of fit values for region of interest (list) - # Number used to calculate area under Gaussian curve - int_factor <- 1 * 10^5 - factor <- 1.5 + # initial value for scale_factor + scale_factor <- 1.5 + # Find the index in int_vector with the highest intensity max_index <- which(int_vector == max(int_vector))[1] mass_max <- mass_vector[max_index] @@ -658,15 +671,15 @@ fit_optim <- function(mass_vector, int_vector, resol, scanmode) { # get peak width fwhm <- get_fwhm(mass_max, resol) # simplify the peak shape: represent it by a triangle - mass_max_simple <- c(mass_max - factor * fwhm, mass_max, mass_max + factor * fwhm) + mass_max_simple <- c(mass_max - scale_factor * fwhm, mass_max, mass_max + scale_factor * fwhm) int_max_simple <- c(0, int_max, 0) # define mass_diff as difference between last and first value of mass_max_simple - mass_diff <- mass_max_simple[length(mass_max_simple)] - mass_max_simple[1] + # mass_diff <- mass_max_simple[length(mass_max_simple)] - mass_max_simple[1] # generate a second mass_vector with equally spaced m/z values - mass_vector2 <- seq(mass_max_simple[1], mass_max_simple[length(mass_max_simple)], - length = mass_diff * int_factor) - sigma <- get_stdev(mass_vector2, int_max_simple) + mass_vector_eq <- seq(mass_max_simple[1], mass_max_simple[length(mass_max_simple)], + length = 100 * length(mass_max_simple)) + sigma <- get_stdev(mass_vector_eq, int_max_simple) # define optimization function for optim based on normal distribution opt_f <- function(p, mass_vector, int_vector, sigma, mass_max) { curve <- p * dnorm(mass_vector, mass_max, sigma) @@ -675,26 +688,26 @@ fit_optim <- function(mass_vector, int_vector, resol, scanmode) { # get optimal value for fitted Gaussian curve opt_fit <- optimize(opt_f, c(0, 100000), tol = 0.0001, mass_vector, int_vector, sigma, mass_max) - scale <- opt_fit$minimum + scale_factor <- opt_fit$minimum # get an estimate of the area under the peak - area <- estimate_area(mass_max, resol, scale, sigma) + area <- estimate_area(mass_max, resol, scale_factor, sigma) # put all values for this region of interest into a list roi_value_list <- list("mean" = mass_max, "area" = area, - "min" = mass_vector2[1], - "max" = mass_vector2[length(mass_vector2)]) + "min" = mass_vector_eq[1], + "max" = mass_vector_eq[length(mass_vector_eq)]) return(roi_value_list) } -within_ppm <- function(mean, scale, sigma, area, mass_vector2, mass_vector, ppm = 4, resol) { +within_ppm <- function(mean, scale_factor, sigma, area, mass_vector_eq, mass_vector, ppm = 4, resol) { #' Test whether two mass ranges are within ppm distance of each other #' #' @param mean: Value for mean m/z (float) - #' @param scale: Initial value used to estimate scaling parameter (integer) + #' @param scale_factor: Initial value used to estimate scaling parameter (integer) #' @param sigma: Value for standard deviation (float) #' @param area: Value for area under the curve (float) - #' @param mass_vector2: Vector of equally spaced m/z values (float) + #' @param mass_vector_eq: Vector of equally spaced m/z values (float) #' @param mass_vector: Vector of m/z values for a region of interest (float) #' @param ppm: Value for distance between two values of mass (integer) #' @param resol: Value for resolution (integer) @@ -704,7 +717,7 @@ within_ppm <- function(mean, scale, sigma, area, mass_vector2, mass_vector, ppm # sort index <- order(mean) mean <- mean[index] - scale <- scale[index] + scale_factor <- scale_factor[index] sigma <- sigma[index] area <- area[index] @@ -718,16 +731,16 @@ within_ppm <- function(mean, scale, sigma, area, mass_vector2, mass_vector, ppm # avoid double occurrance in sum if ((i - 1) %in% summed) next - result_values <- sum_curves(mean[i - 1], mean[i], scale[i - 1], scale[i], sigma[i - 1], sigma[i], - mass_vector2, mass_vector, resol) + result_values <- sum_curves(mean[i - 1], mean[i], scale_factor[i - 1], scale_factor[i], sigma[i - 1], sigma[i], + mass_vector_eq, mass_vector, resol) summed <- c(summed, i - 1, i) if (is.nan(result_values$mean)) result_values$mean <- 0 mean[i - 1] <- result_values$mean mean[i] <- result_values$mean area[i - 1] <- result_values$area area[i] <- result_values$area - scale[i - 1] <- result_values$scale - scale[i] <- result_values$scale + scale_factor[i - 1] <- result_values$scale_factor + scale_factor[i] <- result_values$scale_factor sigma[i - 1] <- result_values$sigma sigma[i] <- result_values$sigma @@ -739,41 +752,64 @@ within_ppm <- function(mean, scale, sigma, area, mass_vector2, mass_vector, ppm if (length(remove) != 0) { mean <- mean[-c(remove)] area <- area[-c(remove)] - scale <- scale[-c(remove)] + scale_factor <- scale_factor[-c(remove)] sigma <- sigma[-c(remove)] } - list_params <- list("mean" = mean, "area" = area, "scale" = scale, "sigma" = sigma, "qual" = NULL) + list_params <- list("mean" = mean, "area" = area, "scale_factor" = scale_factor, "sigma" = sigma, "qual" = NULL) return(list_params) } -sum_curves <- function(mean1, mean2, scale1, scale2, sigma1, sigma2, mass_vector2, mass_vector, resol) { +sum_curves <- function(mean1, mean2, scale_factor1, scale_factor2, sigma1, sigma2, mass_vector_eq, mass_vector, resol) { #' Sum two curves #' #' @param mean1: Value for mean m/z of first peak (float) #' @param mean2: Value for mean m/z of second peak (float) - #' @param scale1: Initial value used to estimate scaling parameter for first peak (integer) - #' @param scale2: Initial value used to estimate scaling parameter for second peak (integer) + #' @param scale_factor1: Initial value used to estimate scaling parameter for first peak (integer) + #' @param scale_factor2: Initial value used to estimate scaling parameter for second peak (integer) #' @param sigma1: Value for standard deviation for first peak (float) #' @param sigma2: Value for standard deviation for second peak (float) - #' @param mass_vector2: Vector of equally spaced m/z values (float) + #' @param mass_vector_eq: Vector of equally spaced m/z values (float) #' @param mass_vector: Vector of m/z values for a region of interest (float) #' @param resol: Value for resolution (integer) #' #' @return list_params: list of parameters indicating quality of fit (list) - sum_fit <- (scale1 * dnorm(mass_vector2, mean1, sigma1)) + (scale2 * dnorm(mass_vector2, mean2, sigma2)) + sum_fit <- (scale_factor1 * dnorm(mass_vector_eq, mean1, sigma1)) + (scale_factor2 * dnorm(mass_vector_eq, mean2, sigma2)) - mean1_plus2 <- weighted.mean(c(mean1, mean2), c(max(scale1 * dnorm(mass_vector2, mean1, sigma1)), - max(scale2 * dnorm(mass_vector2, mean2, sigma2)))) + mean1_plus2 <- weighted.mean(c(mean1, mean2), c(max(scale_factor1 * dnorm(mass_vector_eq, mean1, sigma1)), + max(scale_factor2 * dnorm(mass_vector_eq, mean2, sigma2)))) # get new values for parameters fwhm <- get_fwhm(mean1_plus2, resol) area <- max(sum_fit) - scale <- scale1 + scale2 + scale_factor <- scale_factor1 + scale_factor2 sigma <- (fwhm / 2) * 0.85 - list_params <- list("mean" = mean1_plus2, "area" = area, "scale" = scale, "sigma" = sigma) + list_params <- list("mean" = mean1_plus2, "area" = area, "scale_factor" = scale_factor, "sigma" = sigma) return(list_params) } +check_overlap <- function(range1, range2) { + #' Modify range1 and range2 in case of overlap + #' + #' @param range1: Vector of m/z values for first peak (float) + #' @param range2: Vector of m/z values for second peak (float) + #' + #' @return new_ranges: list of two ranges (list) + + # Check for overlap + if (length(intersect(range1, range2)) == 2) { + if (length(range1) >= length(range2)) { + range1 <- range1[-length(range1)] + } else { + range2 <- range2[-1] + } + } else if (length(intersect(range1, range2)) == 3) { + range1 <- range1[-length(range1)] + range2 <- range2[-1] + } + new_ranges <- list("range1" = range1, "range2" = range2) + return(new_ranges) +} + From 62fc22503e990e70921ba3f41e827c9b874fa57c Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 11 Feb 2025 11:17:01 +0100 Subject: [PATCH 037/161] added unit tests for DIMS peak finding --- DIMS/preprocessing/peak_finding_functions.R | 13 +- DIMS/tests/testthat.R | 6 + .../testthat/test_peak_finding_functions.R | 218 ++++++++++++++++++ 3 files changed, 231 insertions(+), 6 deletions(-) create mode 100644 DIMS/tests/testthat.R create mode 100644 DIMS/tests/testthat/test_peak_finding_functions.R diff --git a/DIMS/preprocessing/peak_finding_functions.R b/DIMS/preprocessing/peak_finding_functions.R index 947fea1f..1baef120 100644 --- a/DIMS/preprocessing/peak_finding_functions.R +++ b/DIMS/preprocessing/peak_finding_functions.R @@ -38,8 +38,8 @@ search_mzrange <- function(ints_fullrange, resol, sample_name, peak_thresh) { } # check if there are more intensities than maximum for region of interest if (length(int_vector) > max_roi_length) { + print("vector of intensities is longer than max") print(length(int_vector)) - print(running_index) # trim lowest intensities to zero #int_vector[which(int_vector < min(int_vector) * 1.1)] <- 0 # split the range into multiple sub ranges @@ -107,7 +107,7 @@ search_mzrange <- function(ints_fullrange, resol, sample_name, peak_thresh) { fit_gaussian <- function(mass_vector_eq, mass_vector, int_vector, resol, force_nr, use_bounds) { - #' Fit 1, 2, 3 or 4 Gaussian peaks in small region of m/z + #' Fit 1 or 2 Gaussian peaks in small region of m/z #' #' @param mass_vector_eq: Vector of equally spaced m/z values (float) #' @param mass_vector: Vector of m/z values for a region of interest (float) @@ -329,14 +329,15 @@ fit_1peak <- function(mass_vector_eq, mass_vector, int_vector, max_index, # optimize scaling factor fq <- 0 scale_factor <- 0 - if (sum(int_vector) > sum(params1[2] * dnorm(mass_vector, fitted_mz, sigma))) { + if (sum(int_vector) > sum(fitted_nr * dnorm(mass_vector, fitted_mz, sigma))) { # increase scale_factor until convergence while ((round(fq, digits = 3) != round(fq_new, digits = 3)) && (scale_factor_new < 10000)) { fq <- fq_new scale_factor <- scale_factor_new # fit 1 peak fitted_peak <- optimize_1gaussian(mass_vector, int_vector, sigma, weighted_mu, scale_factor, use_bounds) - params1 <- fitted_peak$par + fitted_mz <- fitted_peak$par[1] + fitted_nr <- fitted_peak$par[2] # get new value for fit quality and scale_factor fq_new <- get_fit_quality(mass_vector, int_vector, fitted_mz, resol, fitted_nr, sigma)$fq_new scale_factor_new <- 1.2 * scale_factor @@ -348,11 +349,11 @@ fit_1peak <- function(mass_vector_eq, mass_vector, int_vector, max_index, scale_factor <- scale_factor_new # fit 1 peak fitted_peak <- optimize_1gaussian(mass_vector, int_vector, sigma, weighted_mu, scale_factor, use_bounds) - params1 <- fitted_peak$par + fitted_mz <- fitted_peak$par[1] + fitted_nr <- fitted_peak$par[2] # get new value for fit quality and scale_factor fq_new <- get_fit_quality(mass_vector, int_vector, fitted_mz, resol, fitted_nr, sigma)$fq_new scale_factor_new <- 0.8 * scale_factor - print(scale_factor_new) } } # use optimized scale_factor factor to fit 1 peak diff --git a/DIMS/tests/testthat.R b/DIMS/tests/testthat.R new file mode 100644 index 00000000..a467339d --- /dev/null +++ b/DIMS/tests/testthat.R @@ -0,0 +1,6 @@ +# Run all unit tests +library(testthat) +# enable snapshots +local_edition(3) + +testthat::test_file("testthat/test_peak_finding_functions.R") diff --git a/DIMS/tests/testthat/test_peak_finding_functions.R b/DIMS/tests/testthat/test_peak_finding_functions.R new file mode 100644 index 00000000..a0db0ca9 --- /dev/null +++ b/DIMS/tests/testthat/test_peak_finding_functions.R @@ -0,0 +1,218 @@ +# unit tests for PeakFinding functions: + +source("../preprocessing/peak_finding_functions.R") + +# test fit_quality function: +test_that("fit quality is correctly calculated", { + # initialize + test_resol <- 140000 + test_scale_factor <- 2 + test_sigma <- 0.0001 + test_sum_fit <- NULL + + # create peak info to test on: + test_mass_vector <- rep(70.00938, 5) + 1:5 * 0.00006 + test_int_vector <- c(9000, 16000, 19000, 15000, 6000) + test_mz_max <- max(test_mass_vector) + + expect_type(get_fit_quality(test_mass_vector, test_int_vector, test_mz_max, test_resol, test_scale_factor, test_sigma, test_sum_fit), "list") + expect_equal(get_fit_quality(test_mass_vector, test_int_vector, test_mz_max, test_resol, test_scale_factor, test_sigma, test_sum_fit)$fq_new, 2.426, tolerance = 0.001, TRUE) +}) + +# test get_stdev: +test_that("standard deviation is correctly calculated", { + # create peak info to test on: + test_mass_vector <- rep(70.00938, 5) + 1:5 * 0.00006 + test_int_vector <- c(9000, 16000, 19000, 15000, 6000) + test_resol <- 140000 + + expect_type(get_stdev(test_mass_vector, test_int_vector, test_resol), "double") + expect_equal(get_stdev(test_mass_vector, test_int_vector, test_resol), 0.000126, tolerance = 0.00001, TRUE) +}) + +# test estimate_area +test_that("area under the curve is correctly calculated", { + # create peak info to test on: + test_mass_vector <- rep(70.00938, 5) + 1:5 * 0.00006 + test_mz_max <- max(test_mass_vector) + test_resol <- 140000 + test_scale_factor <- 2 + test_sigma <- 0.0001 + + expect_type(estimate_area(test_mz_max, test_resol, test_scale_factor, test_sigma), "double") + expect_equal(estimate_area(test_mz_max, test_resol, test_scale_factor, test_sigma), 1673.061, tolerance = 0.001, TRUE) +}) + +# test get_fwhm +test_that("fwhm is correctly calculated", { + # create peak info to test on: + test_mass_vector <- rep(70.00938, 5) + 1:5 * 0.00006 + test_mz_max <- max(test_mass_vector) + test_resol <- 140000 + + expect_type(get_fwhm(test_mz_max, test_resol), "double") + expect_equal(get_fwhm(test_mz_max, test_resol), 0.000295865, tolerance = 0.000001, TRUE) +}) + +# test get_fwhm for NA +test_that("fwhm for mass NA gives standard fwhm", { + test_resol <- 140000 + + expect_type(get_fwhm(NA, test_resol), "double") + expect_equal(get_fwhm(NA, test_resol), 0.001428571, tolerance = 0.000001, TRUE) +}) + +# test within_ppm +test_that("two peaks are within 5 ppm", { + # create peak info to test on: + test_mass_vector <- rep(70.00938, 5) + 1:5 * 0.00006 + test_mz_max <- max(test_mass_vector) + test_scale_factor <- 2 + test_sigma <- 0.0001 + test_area <- 20000 + test_mass_vector_eq <- seq(min(test_mass_vector), max(test_mass_vector), length = 100) + test_ppm <- 5 + test_resol <- 140000 + + expect_type(within_ppm(test_mz_max, test_scale_factor, test_sigma, test_area, test_mass_vector_eq, test_mass_vector, test_ppm, test_resol), "list") + expect_equal(within_ppm(test_mz_max, test_scale_factor, test_sigma, test_area, test_mass_vector_eq, test_mass_vector, test_ppm, test_resol)$mean, 70.00962, tolerance = 0.0001, TRUE) +}) + +# test sum_curves +test_that("two curves are correctly summed", { + # create peak info to test on: + test_mass_vector <- rep(70.00938, 5) + 1:5 * 0.00006 + test_mass_vector_eq <- seq(min(test_mass_vector), max(test_mass_vector), length = 100) + test_mean1 <- 70.00938 + test_mean2 <- 70.00962 + test_scale_factor1 <- 2 + test_scale_factor2 <- 4 + test_sigma1 <- 0.0001 + test_sigma2 <- 0.0002 + test_resol <- 140000 + + expect_type(sum_curves(test_mean1, test_mean2, test_scale_factor1, test_scale_factor2, test_sigma1, test_sigma2, test_mass_vector_eq, test_mass_vector, test_resol), "list") + expect_equal(sum_curves(test_mean1, test_mean2, test_scale_factor1, test_scale_factor2, test_sigma1, test_sigma2, test_mass_vector_eq, test_mass_vector, test_resol)$mean, 70.0095, tolerance = 0.0001, TRUE) +}) + +# test fit_optim +test_that("optimal peak fit can be found", { + # create peak info to test on: + test_mass_vector <- rep(70.00938, 5) + 1:5 * 0.00006 + test_int_vector <- c(9000, 16000, 19000, 15000, 6000) + test_resol <- 140000 + # create output to test on: + test_output <- list(mean = 70.0095, area = 5009.596, min = 70.00906, max = 70.00994) + + expect_type(fit_optim(test_mass_vector, test_int_vector, test_resol), "list") + for (key in names(test_output)) { + expect_equal(fit_optim(test_mass_vector, test_int_vector, test_resol)[[key]], test_output[[key]], tolerance = 0.0001, TRUE) + } +}) + +# test optimize_1gaussian: +test_that("optimal value for m/z and scale_factor can be found", { + # create peak info to test on: + test_mass_vector <- rep(70.00938, 5) + 1:5 * 0.00006 + test_int_vector <- c(9000, 16000, 19000, 15000, 6000) + test_sigma <- 0.0001 + test_query_mass <- median(test_mass_vector) + test_scale_factor <- 2 + test_use_bounds <- FALSE + + expect_type(optimize_1gaussian(test_mass_vector, test_int_vector, test_sigma, test_query_mass, test_scale_factor, test_use_bounds), "list") + expect_equal(optimize_1gaussian(test_mass_vector, test_int_vector, test_sigma, test_query_mass, test_scale_factor, test_use_bounds)$par[1], 70.00949, tolerance = 0.0001, TRUE) + expect_equal(optimize_1gaussian(test_mass_vector, test_int_vector, test_sigma, test_query_mass, test_scale_factor, test_use_bounds)$par[2], 4.570024, tolerance = 0.0001, TRUE) +}) + +# test fit_1peak: +test_that("correct mean m/z and scale_factor can be found for 1 peak", { + # create peak info to test on: + test_mass_vector <- rep(70.00938, 5) + 1:5 * 0.00006 + test_int_vector <- c(9000, 16000, 19000, 15000, 6000) + test_mass_vector_eq <- seq(min(test_mass_vector), max(test_mass_vector), length = 100) + test_max_index <- which(test_int_vector == max(test_int_vector)) + test_resol <- 140000 + test_fit_quality <- 0.5 + test_use_bounds <- FALSE + # create output to test on: + test_output <- list(mean = 70.0095, scale_factor = 5.23334, sigma = 0.000126, qual = 0.24300) + + expect_type(fit_1peak(test_mass_vector_eq, test_mass_vector, test_int_vector, test_max_index, test_resol, test_fit_quality, test_use_bounds), "list") + for (key in names(test_output)) { + expect_equal(fit_1peak(test_mass_vector_eq, test_mass_vector, test_int_vector, test_max_index, test_resol, test_fit_quality, test_use_bounds)[[key]], test_output[[key]], tolerance = 0.0001, TRUE) + } +}) + +# test optimize_2gaussians: +test_that("optimal value for m/z and scale_factor can be found", { + # create info for two peaks in a small region + test_mass_vector_2peaks <- rep(70.00938, 14) + 1:14 * 0.00006 + test_int_vector_2peaks <- rep(c(2000, 9000, 16000, 19000, 15000, 6000, 3000), 2) + test_mass_vector_eq <- seq(min(test_mass_vector_2peaks), max(test_mass_vector_2peaks), length = 100) + test_max_index <- which(test_int_vector_2peaks == max(test_int_vector_2peaks)) + test_sigma1 <- 0.0001 + test_sigma2 <- 0.0002 + test_query_mass1 <- test_max_index[1] + test_query_mass2 <- test_max_index[2] + test_scale_factor1 <- 2 + test_scale_factor2 <- 3 + test_use_bounds <- FALSE + + expect_type(optimize_2gaussians(test_mass_vector_2peaks, test_int_vector_2peaks, test_sigma1, test_sigma2, test_query_mass1, test_scale_factor1, test_query_mass2, test_scale_factor2, test_use_bounds), "list") + expect_equal(optimize_2gaussians(test_mass_vector_2peaks, test_int_vector_2peaks, test_sigma1, test_sigma2, test_query_mass1, test_scale_factor1, test_query_mass2, test_scale_factor2, test_use_bounds)$par[1], 4, tolerance = 0.1, TRUE) +}) + +# test fit_2peaks: +test_that("correct mean m/z and scale_factor can be found for 2 peaks", { + # create info for two peaks in a small region + test_mass_vector_2peaks <- rep(70.00938, 14) + 1:14 * 0.00006 + test_int_vector_2peaks <- rep(c(2000, 9000, 16000, 19000, 15000, 6000, 3000), 2) + test_mass_vector_eq <- seq(min(test_mass_vector_2peaks), max(test_mass_vector_2peaks), length = 100) + test_max_index <- which(test_int_vector_2peaks == max(test_int_vector_2peaks)) + test_resol <- 140000 + test_fit_quality <- 0.5 + test_use_bounds <- FALSE + # create output to test on: + test_output <- list(mean = c(70.00954, 70.00998), scale_factor = c(4.823598, 4.828072), sigma = c(0.0001257425, 0.0001257436), qual = 0.38341) + + expect_type(fit_2peaks(test_mass_vector_eq, test_mass_vector_2peaks, test_int_vector_2peaks, test_max_index, + test_resol, test_use_bounds, test_fit_quality), "list") + for (key in names(test_output)) { + expect_equal(fit_2peaks(test_mass_vector_eq, test_mass_vector_2peaks, test_int_vector_2peaks, test_max_index, + test_resol, test_use_bounds, test_fit_quality)[[key]], test_output[[key]], tolerance = 0.0001, TRUE) + } +}) + +# test fit_gaussian(mass_vector2, mass_vector, int_vector, resol, force, use_bounds) +test_that("initial gaussian fit is done correctly", { + # create peak info to test on: + test_mass_vector <- rep(70.00938, 5) + 1:5 * 0.00006 + test_int_vector <- c(9000, 16000, 19000, 15000, 6000) + test_mass_vector_eq <- seq(min(test_mass_vector), max(test_mass_vector), length = 100) + test_resol <- 140000 + test_use_bounds <- FALSE + test_force_nr <- 1 + + expect_type(fit_gaussian(test_mass_vector_eq, test_mass_vector, test_int_vector, test_resol, test_force_nr, test_use_bounds), "list") + expect_equal(fit_gaussian(test_mass_vector_eq, test_mass_vector, test_int_vector, test_resol, test_force_nr, test_use_bounds)$mean, 70.00956, tolerance = 0.00001, TRUE) +}) + +# test search_mzrange(ints_fullrange, resol, sample_name, peak_thresh) +test_that("all peak finding functions work together", { + # enable snapshot + local_edition(3) + # create info for ten peaks separated by zero + test_large_mass_vector <- rep(70.00938, 80) + 1:80 * 0.00006 + test_large_int_vector <- rep(c(2000, 9000, 16000, 19000, 15000, 6000, 3000, 0), 10) + names(test_large_int_vector) <- test_large_mass_vector + test_resol <- 140000 + test_sample_name <- "C1.1" + test_peak_thresh <- 100 + + expect_type(search_mzrange(test_large_int_vector, test_resol, test_sample_name, test_peak_thresh), "list") + expect_snapshot(search_mzrange(test_large_int_vector, test_resol, test_sample_name, test_peak_thresh), error = FALSE) +}) + +# test fit_gaussian(mass_vector2, mass_vector, int_vector, resol, force, use_bounds) +test_that("fit gaussian") From 5d311a3203e0eee999781d9621c10f01c63508f9 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 8 Jul 2025 15:27:24 +0200 Subject: [PATCH 038/161] changed variable name tmp to replicates_persample --- DIMS/MakeInit.R | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/DIMS/MakeInit.R b/DIMS/MakeInit.R index 44d49962..9ff22623 100644 --- a/DIMS/MakeInit.R +++ b/DIMS/MakeInit.R @@ -1,5 +1,3 @@ -## adapted from makeInit in old pipeline - # define parameters args <- commandArgs(trailingOnly = TRUE) @@ -15,12 +13,12 @@ group_names_unique <- unique(group_names) # generate the replication pattern repl_pattern <- c() for (sample_group in 1:nr_sample_groups) { - tmp <- c() + replicates_persample <- c() for (repl in nr_replicates:1) { index <- ((sample_group * nr_replicates) - repl) + 1 - tmp <- c(tmp, sample_names[index]) + replicates_persample <- c(replicates_persample, sample_names[index]) } - repl_pattern <- c(repl_pattern, list(tmp)) + repl_pattern <- c(repl_pattern, list(replicates_persample)) } names(repl_pattern) <- group_names_unique From 708b872f548ffee4a4b114a2b2b1ba20c6cfdc0a Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 8 Jul 2025 15:29:54 +0200 Subject: [PATCH 039/161] omitted obsolete lines --- DIMS/GenerateBreaks.R | 2 -- 1 file changed, 2 deletions(-) diff --git a/DIMS/GenerateBreaks.R b/DIMS/GenerateBreaks.R index ddb68c56..007d5ab7 100644 --- a/DIMS/GenerateBreaks.R +++ b/DIMS/GenerateBreaks.R @@ -1,5 +1,3 @@ -## adapted from 1-generateBreaksFwhm.HPC.R ## - # load required package suppressPackageStartupMessages(library("xcms")) From 0521390b07a4ea54810ea80abc8e9d54766dac19 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 8 Jul 2025 15:33:21 +0200 Subject: [PATCH 040/161] added weighted mean for half-bad TICs --- DIMS/AssignToBins.R | 76 ++++++++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 29 deletions(-) diff --git a/DIMS/AssignToBins.R b/DIMS/AssignToBins.R index a25a5e70..8b31af7e 100644 --- a/DIMS/AssignToBins.R +++ b/DIMS/AssignToBins.R @@ -6,51 +6,63 @@ cmd_args <- commandArgs(trailingOnly = TRUE) mzml_filepath <- cmd_args[1] breaks_filepath <- cmd_args[2] -trimparams_filepath <- cmd_args[3] +trim_parameters_filepath <- cmd_args[3] resol <- as.numeric(cmd_args[4]) -trim <- 0.1 -dims_thresh <- 100 -# load breaks_file: contains breaks_fwhm, breaks_fwhm_avg +# load breaks_file: contains breaks_fwhm, breaks_fwhm_avg, load(breaks_filepath) -# load trim paramters: trim_left_neg, trim_left_pos, trim_right_neg & trim_right_pos -load(trimparams_filepath) +# load trim parameters file: contains trim_left_neg, trim_left_pos, trim_right_neg & trim_right_pos +load(trim_parameters_filepath) # get sample name -sample_name <- sub("\\..*$", "", basename(mzml_filepath)) +techrep_name <- sub("\\..*$", "", basename(mzml_filepath)) options(digits = 16) # Initialize pos_results <- NULL neg_results <- NULL - -# read in the data for 1 sample -raw_data <- suppressMessages(xcms::xcmsRaw(mzml_filepath)) - -# for TIC plots: prepare txt files with data for plots -tic_intensity_persample <- cbind(round(raw_data@scantime, 2), raw_data@tic) -colnames(tic_intensity_persample) <- c("retention_time", "tic_intensity") -write.table(tic_intensity_persample, file = paste0(sample_name, "_TIC.txt")) - -# Create empty placeholders for later use bins <- rep(0, length(breaks_fwhm) - 1) pos_bins <- bins neg_bins <- bins +dims_thresh <- 100 -# Generate a matrix +# read in the data for 1 sample +raw_data <- suppressMessages(xcms::xcmsRaw(mzml_filepath)) + +# Generate a matrix with retention times and intensities raw_data_matrix <- xcms::rawMat(raw_data) # Get time values for positive and negative scans pos_times <- raw_data@scantime[raw_data@polarity == "positive"] neg_times <- raw_data@scantime[raw_data@polarity == "negative"] # Select scans between trim_left and trim_right -pos_times <- pos_times[pos_times > trim_left_pos & pos_times < trim_right_pos] -neg_times <- neg_times[neg_times > trim_left_neg & neg_times < trim_right_neg] +pos_times_trimmed <- pos_times[pos_times > trim_left_pos & pos_times < trim_right_pos] +neg_times_trimmed <- neg_times[neg_times > trim_left_neg & neg_times < trim_right_neg] +# get TIC intensities for areas between trim_left and trim_right +tic_intensity_persample <- cbind(raw_data@scantime, raw_data@tic) +colnames(tic_intensity_persample) <- c("retention_time", "tic_intensity") +tic_intensity_pos <- tic_intensity_persample[tic_intensity_persample[ , "retention_time"] > min(pos_times_trimmed) & + tic_intensity_persample[ , "retention_time"] < max(pos_times_trimmed), ] +tic_intensity_neg <- tic_intensity_persample[tic_intensity_persample[ , "retention_time"] > min(neg_times_trimmed) & + tic_intensity_persample[ , "retention_time"] < max(neg_times_trimmed), ] +# calculate weighted mean of intensities for pos and neg separately +mean_pos <- weighted.mean(tic_intensity_pos[ , "tic_intensity"], tic_intensity_pos[ , "tic_intensity"]) +mean_neg <- weighted.mean(tic_intensity_neg[ , "tic_intensity"], tic_intensity_neg[ , "tic_intensity"]) +# intensity per scan should be at least 80% of weighted mean +dims_thresh_pos <- 0.8 * mean_pos +dims_thresh_neg <- 0.8 * mean_neg + +# Generate an index with which to select values for each mode +#pos_index <- which(raw_data_matrix[, "time"] %in% pos_times) +#neg_index <- which(raw_data_matrix[, "time"] %in% neg_times) +# select only data from scans which pass the dims_thresh_pos and *_neg filter +pos_times_pass <- tic_intensity_pos[which(tic_intensity_pos[ , "tic_intensity"] > dims_thresh_pos), "retention_time"] +neg_times_pass <- tic_intensity_neg[which(tic_intensity_neg[ , "tic_intensity"] > dims_thresh_neg), "retention_time"] # Generate an index with which to select values for each mode -pos_index <- which(raw_data_matrix[, "time"] %in% pos_times) -neg_index <- which(raw_data_matrix[, "time"] %in% neg_times) +pos_index <- which(raw_data_matrix[, "time"] %in% pos_times_pass) +neg_index <- which(raw_data_matrix[, "time"] %in% neg_times_pass) # Separate each mode into its own matrix pos_raw_data_matrix <- raw_data_matrix[pos_index, ] neg_raw_data_matrix <- raw_data_matrix[neg_index, ] @@ -76,10 +88,10 @@ bin_indices_neg <- cut( if (nrow(pos_raw_data_matrix) > 0) { # set NA in intensities to zero pos_raw_data_matrix[is.na(pos_raw_data_matrix[, "intensity"]), "intensity"] <- 0 - # aggregate intensities, calculate mean, use only values above dims_thresh + # aggregate intensities, calculate mean, use only values above dims_thresh_pos aggr_int_pos <- stats::aggregate(pos_raw_data_matrix[, "intensity"], list(bin_indices_pos), - FUN = function(x) { mean(x[which(x > dims_thresh)]) }) + FUN = function(x) { mean(x) }) # set NA to zero in second column aggr_int_pos[is.na(aggr_int_pos[, 2]), 2] <- 0 pos_bins[aggr_int_pos[, 1]] <- aggr_int_pos[, 2] @@ -87,10 +99,10 @@ if (nrow(pos_raw_data_matrix) > 0) { if (nrow(neg_raw_data_matrix) > 0) { # set NA in intensities to zero neg_raw_data_matrix[is.na(neg_raw_data_matrix[, "intensity"]), "intensity"] <- 0 - # aggregate intensities, calculate mean, use only values above dims_thresh + # aggregate intensities, calculate mean, use only values above dims_thresh_neg aggr_int_neg <- stats::aggregate(neg_raw_data_matrix[, "intensity"], list(bin_indices_neg), - FUN = function(x) { mean(x[which(x > dims_thresh)]) }) + FUN = function(x) { mean(x) }) # set NA to zero in second column aggr_int_neg[is.na(aggr_int_neg[, 2]), 2] <- 0 neg_bins[aggr_int_neg[, 1]] <- aggr_int_neg[, 2] @@ -108,8 +120,8 @@ pos_results_transpose <- t(pos_results) neg_results_transpose <- t(neg_results) # Add file names as row names -rownames(pos_results_transpose) <- sample_name -rownames(neg_results_transpose) <- sample_name +rownames(pos_results_transpose) <- techrep_name +rownames(neg_results_transpose) <- techrep_name # delete the last value of breaks_fwhm_avg to match dimensions of pos_results and neg_results breaks_fwhm_avg_minuslast <- breaks_fwhm_avg[-length(breaks_fwhm_avg)] @@ -126,4 +138,10 @@ neg_results_final <- t(neg_results_transpose) peak_list <- list("pos" = pos_results_final, "neg" = neg_results_final, "breaksFwhm" = breaks_fwhm) -save(peak_list, file = paste0(sample_name, ".RData")) +save(peak_list, file = paste0(techrep_name, ".RData")) + +# for TIC plots: write txt files with data including threshold values for plots +dims_thresh <- c(rep(dims_thresh_pos, length(pos_times)), rep(dims_thresh_neg, length(neg_times))) +tic_intensity_persample <- cbind(round(raw_data@scantime, 2), raw_data@tic, round(dims_thresh, 0)) +colnames(tic_intensity_persample) <- c("retention_time", "tic_intensity", "threshold") +write.table(tic_intensity_persample, file = paste0(techrep_name, "_TIC.txt")) From 15403cd559ab7f953858bfee430df3575a670dec Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 8 Jul 2025 15:39:05 +0200 Subject: [PATCH 041/161] replaced AverageTechReplicates step by EvaluateTics --- DIMS/EvaluateTics.R | 65 ++++++++++++++++++++++++++++++++++---------- DIMS/EvaluateTics.nf | 3 +- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/DIMS/EvaluateTics.R b/DIMS/EvaluateTics.R index 5b8c1e30..bc66a34e 100644 --- a/DIMS/EvaluateTics.R +++ b/DIMS/EvaluateTics.R @@ -13,7 +13,6 @@ highest_mz_file <- cmd_args[5] highest_mz <- get(load(highest_mz_file)) trim_params_filepath <- cmd_args[6] thresh2remove <- 1000000000 -dims_thresh <- 100 remove_from_repl_pattern <- function(bad_samples, repl_pattern, nr_replicates) { # collect list of samples to remove from replication pattern @@ -76,31 +75,62 @@ for (sample_nr in 1:length(repl_pattern)) { if (sum(peak_list$neg[, 1]) < thresh2remove) { cat(" ... Removed") remove_neg <- c(remove_neg, tech_reps[file_nr]) - } + } tech_reps_array_neg <- cbind(tech_reps_array_neg, peak_list$neg) } } +# negative scan mode +print("neg") pattern_list <- remove_from_repl_pattern(remove_neg, repl_pattern, nr_replicates) repl_pattern_filtered <- pattern_list$pattern +print(repl_pattern_filtered) +print(length(repl_pattern_filtered)) save(repl_pattern_filtered, file = "negative_repl_pattern.RData") -write.table( - remove_neg, - file = "miss_infusions_negative.txt", - row.names = FALSE, - col.names = FALSE, - sep = "\t" +# save replication pattern in txt file for use in Nextflow +allsamples_techreps_neg <- matrix("", ncol = 3, nrow = length(repl_pattern_filtered)) +for (sample_nr in 1:length(repl_pattern_filtered)) { + allsamples_techreps_neg[sample_nr, 1] <- names(repl_pattern_filtered)[sample_nr] + allsamples_techreps_neg[sample_nr, 2] <- paste0(repl_pattern_filtered[[sample_nr]], collapse = ";") +} +allsamples_techreps_neg[, 3] <- "negative" + +# write information on miss_infusions +write.table(remove_neg, + file = "miss_infusions_negative.txt", + row.names = FALSE, + col.names = FALSE, + sep = "\t" ) +# positive scan mode pattern_list <- remove_from_repl_pattern(remove_pos, repl_pattern, nr_replicates) +print(pattern_list) repl_pattern_filtered <- pattern_list$pattern save(repl_pattern_filtered, file = "positive_repl_pattern.RData") -write.table( - remove_pos, - file = "miss_infusions_positive.txt", - row.names = FALSE, - col.names = FALSE, - sep = "\t" +# save replication pattern in txt file for use in Nextflow +allsamples_techreps_pos <- matrix("", ncol = 3, nrow = length(repl_pattern_filtered)) +for (sample_nr in 1:length(repl_pattern_filtered)) { + allsamples_techreps_pos[sample_nr, 1] <- names(repl_pattern_filtered)[sample_nr] + allsamples_techreps_pos[sample_nr, 2] <- paste0(repl_pattern_filtered[[sample_nr]], collapse = ";") +} +allsamples_techreps_pos[, 3] <- "positive" + +# combine information on samples and technical replicates for pos and neg +allsamples_techreps <- rbind(allsamples_techreps_pos, allsamples_techreps_neg) +write.table(allsamples_techreps, + file = paste0("replicates_per_sample.txt"), + col.names = FALSE, + row.names = FALSE, + sep = "," +) + +# write information on miss_infusions +write.table(remove_pos, + file = "miss_infusions_positive.txt", + row.names = FALSE, + col.names = FALSE, + sep = "\t" ) ## generate TIC plots @@ -127,8 +157,11 @@ for (sample_nr in c(1:length(repl_pattern))) { sample_name <- names(repl_pattern)[sample_nr] for (file_nr in 1:length(tech_reps)) { plot_nr <- plot_nr + 1 - # repl1_nr <- read.table(paste(paste(outdir, "2-pklist/", sep = "/"), tech_reps[file_nr], "_TIC.txt", sep = "")) repl1_nr <- read.table(paste0(tech_reps[file_nr], "_TIC.txt")) + # get threshold values per technical replicate + dims_thresh_pos <- repl1_nr[1, "threshold"] + dims_thresh_neg <- repl1_nr[nrow(repl1_nr), "threshold"] + # for replicates with bad TIC, determine what color the border of the plot should be bad_color_pos <- tech_reps[file_nr] %in% remove_pos bad_color_neg <- tech_reps[file_nr] %in% remove_neg if (bad_color_neg & bad_color_pos) { @@ -147,6 +180,8 @@ for (sample_nr in c(1:length(repl_pattern))) { geom_vline(xintercept = trim_right_pos, col = "red", linetype = 2, linewidth = 0.3) + geom_vline(xintercept = trim_left_neg, col = "red", linetype = 2, linewidth = 0.3) + geom_vline(xintercept = trim_right_neg, col = "red", linetype = 2, linewidth = 0.3) + + geom_hline(yintercept = dims_thresh_pos, col = "green", linetype = 2, linewidth = 0.3) + + geom_hline(yintercept = dims_thresh_neg, col = "blue", linetype = 2, linewidth = 0.3) + labs(x = "t (s)", y = "tic_intensity", title = paste0(tech_reps[file_nr], " || ", sample_name)) + theme(plot.background = element_rect(fill = plot_color), axis.text = element_text(size = 4), diff --git a/DIMS/EvaluateTics.nf b/DIMS/EvaluateTics.nf index 7ce58684..831c9500 100644 --- a/DIMS/EvaluateTics.nf +++ b/DIMS/EvaluateTics.nf @@ -15,7 +15,8 @@ process EvaluateTics { path(trim_params_file) output: - path('*_repl_pattern.RData'), emit: pattern_files + path('*_repl_pattern.RData'), emit: pattern_files + path('replicates_per_sample.txt'), emit: sample_techreps path('miss_infusions_negative.txt') path('miss_infusions_positive.txt') path('*_TICplots.pdf') From 6a7ada6d303af488141a77252b7b8487ad759b4c Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 8 Jul 2025 15:39:59 +0200 Subject: [PATCH 042/161] replaced AverageTechReplicates step by EvaluateTics --- DIMS/AverageTechReplicates.R | 200 ---------------------------------- DIMS/AverageTechReplicates.nf | 35 ------ 2 files changed, 235 deletions(-) delete mode 100644 DIMS/AverageTechReplicates.R delete mode 100644 DIMS/AverageTechReplicates.nf diff --git a/DIMS/AverageTechReplicates.R b/DIMS/AverageTechReplicates.R deleted file mode 100644 index 98a2d414..00000000 --- a/DIMS/AverageTechReplicates.R +++ /dev/null @@ -1,200 +0,0 @@ -# adapted from 3-AverageTechReplicates.R - -# load packages -library("ggplot2") -library("gridExtra") - -# define parameters -cmd_args <- commandArgs(trailingOnly = TRUE) - -init_file <- cmd_args[1] -nr_replicates <- as.numeric(cmd_args[2]) -run_name <- cmd_args[3] -dims_matrix <- cmd_args[4] -highest_mz_file <- cmd_args[5] -highest_mz <- get(load(highest_mz_file)) -breaks_filepath <- cmd_args[6] -thresh2remove <- 1000000000 -dims_thresh <- 100 - -remove_from_repl_pattern <- function(bad_samples, repl_pattern, nr_replicates) { - # collect list of samples to remove from replication pattern - remove_from_group <- NULL - for (sample_nr in 1:length(repl_pattern)){ - repl_pattern_1sample <- repl_pattern[[sample_nr]] - remove <- NULL - for (file_nr in 1:length(repl_pattern_1sample)) { - if (repl_pattern_1sample[file_nr] %in% bad_samples) { - remove <- c(remove, file_nr) - } - } - if (length(remove) == nr_replicates) { - remove_from_group <- c(remove_from_group, sample_nr) - } - if (!is.null(remove)) { - repl_pattern[[sample_nr]] <- repl_pattern[[sample_nr]][-remove] - } - } - if (length(remove_from_group) != 0) { - repl_pattern <- repl_pattern[-remove_from_group] - } - return(list("pattern" = repl_pattern)) -} - -# load init_file: contains repl_pattern -load(init_file) - -# load breaks_file: contains breaks_fwhm, breaks_fwhm_avg, -# trim_left_neg, trim_left_pos, trim_right_neg & trim_right_pos -load(breaks_filepath) - -# lower the threshold below which a sample will be removed for DBS and for high m/z -if (dims_matrix == "DBS") { - thresh2remove <- 500000000 -} -if (highest_mz > 700) { - thresh2remove <- 1000000 -} - - -# remove technical replicates which are below the threshold -remove_neg <- NULL -remove_pos <- NULL -cat("Pklist sum threshold to remove technical replicate:", thresh2remove, "\n") -for (sample_nr in 1:length(repl_pattern)) { - tech_reps <- as.vector(unlist(repl_pattern[sample_nr])) - tech_reps_array_pos <- NULL - tech_reps_array_neg <- NULL - sum_neg <- 0 - sum_pos <- 0 - nr_pos <- 0 - nr_neg <- 0 - for (file_nr in 1:length(tech_reps)) { - load(paste(tech_reps[file_nr], ".RData", sep = "")) - cat("\n\nParsing", tech_reps[file_nr]) - # negative scanmode - cat("\n\tNegative peak_list sum", sum(peak_list$neg[, 1])) - if (sum(peak_list$neg[, 1]) < thresh2remove) { - cat(" ... Removed") - remove_neg <- c(remove_neg, tech_reps[file_nr]) - } else { - nr_neg <- nr_neg + 1 - sum_neg <- sum_neg + peak_list$neg - } - tech_reps_array_neg <- cbind(tech_reps_array_neg, peak_list$neg) - # positive scanmode - cat("\n\tPositive peak_list sum", sum(peak_list$pos[, 1])) - if (sum(peak_list$pos[, 1]) < thresh2remove) { - cat(" ... Removed") - remove_pos <- c(remove_pos, tech_reps[file_nr]) - } else { - nr_pos <- nr_pos + 1 - sum_pos <- sum_pos + peak_list$pos - } - tech_reps_array_pos <- cbind(tech_reps_array_pos, peak_list$pos) - } - # save to file - if (nr_neg != 0) { - sum_neg[, 1] <- sum_neg[, 1] / nr_neg - colnames(sum_neg) <- names(repl_pattern)[sample_nr] - save(sum_neg, file = paste0(names(repl_pattern)[sample_nr], "_neg_avg.RData")) - } - if (nr_pos != 0) { - sum_pos[, 1] <- sum_pos[, 1] / nr_pos - colnames(sum_pos) <- names(repl_pattern)[sample_nr] - save(sum_pos, file = paste0(names(repl_pattern)[sample_nr], "_pos_avg.RData")) - } -} - -pattern_list <- remove_from_repl_pattern(remove_neg, repl_pattern, nr_replicates) -repl_pattern_filtered <- pattern_list$pattern -save(repl_pattern_filtered, file = "negative_repl_pattern.RData") -write.table( - remove_neg, - file = "miss_infusions_negative.txt", - row.names = FALSE, - col.names = FALSE, - sep = "\t" -) - -pattern_list <- remove_from_repl_pattern(remove_pos, repl_pattern, nr_replicates) -repl_pattern_filtered <- pattern_list$pattern -save(repl_pattern_filtered, file = "positive_repl_pattern.RData") -write.table( - remove_pos, - file = "miss_infusions_positive.txt", - row.names = FALSE, - col.names = FALSE, - sep = "\t" -) - -## generate TIC plots -# get all txt files -tic_files <- list.files("./", full.names = TRUE, pattern = "*TIC.txt") -all_samps <- sub("_TIC\\..*$", "", basename(tic_files)) - -# determine maximum intensity -highest_tic_max <- 0 -for (file in tic_files) { - tic <- read.table(file) - this_tic_max <- max(tic$tic_intensity) - if (this_tic_max > highest_tic_max) { - highest_tic_max <- this_tic_max - max_sample <- sub("_TIC\\..*$", "", basename(file)) - } -} - -# create a list with information for all TIC plots -tic_plot_list <- list() -plot_nr <- 0 -for (sample_nr in c(1:length(repl_pattern))) { - tech_reps <- as.vector(unlist(repl_pattern[sample_nr])) - sample_name <- names(repl_pattern)[sample_nr] - for (file_nr in 1:length(tech_reps)) { - plot_nr <- plot_nr + 1 - # repl1_nr <- read.table(paste(paste(outdir, "2-pklist/", sep = "/"), tech_reps[file_nr], "_TIC.txt", sep = "")) - repl1_nr <- read.table(paste0(tech_reps[file_nr], "_TIC.txt")) - bad_color_pos <- tech_reps[file_nr] %in% remove_pos - bad_color_neg <- tech_reps[file_nr] %in% remove_neg - if (bad_color_neg & bad_color_pos) { - plot_color <- "#F8766D" - } else if (bad_color_pos) { - plot_color <- "#ED8141" - } else if (bad_color_neg) { - plot_color <- "#BF80FF" - } else { - plot_color <- "white" - } - tic_plot <- ggplot(repl1_nr, aes(retention_time, tic_intensity)) + - geom_line(linewidth = 0.3) + - geom_hline(yintercept = highest_tic_max, col = "grey", linetype = 2, linewidth = 0.3) + - geom_vline(xintercept = trim_left_pos, col = "red", linetype = 2, linewidth = 0.3) + - geom_vline(xintercept = trim_right_pos, col = "red", linetype = 2, linewidth = 0.3) + - geom_vline(xintercept = trim_left_neg, col = "red", linetype = 2, linewidth = 0.3) + - geom_vline(xintercept = trim_right_neg, col = "red", linetype = 2, linewidth = 0.3) + - labs(x = "t (s)", y = "tic_intensity", title = paste0(tech_reps[file_nr], " || ", sample_name)) + - theme(plot.background = element_rect(fill = plot_color), - axis.text = element_text(size = 4), - axis.title = element_text(size = 4), - plot.title = element_text(size = 6)) - tic_plot_list[[plot_nr]] <- tic_plot - } -} - -# create a layout matrix dependent on number of replicates -layout <- matrix(1:(10 * nr_replicates), 10, nr_replicates, TRUE) -# put TIC plots in matrix -tic_plot_pdf <- marrangeGrob( - grobs = tic_plot_list, - nrow = 10, ncol = nr_replicates, - layout_matrix = layout, - top = quote(paste( - "TICs of run", run_name, - " \n colors: red = both modes misinjection, orange = pos mode misinjection, purple = neg mode misinjection \n ", - g, "/", npages - )) -) - -# save to file -ggsave(filename = paste0(run_name, "_TICplots.pdf"), - tic_plot_pdf, width = 21, height = 29.7, units = "cm") diff --git a/DIMS/AverageTechReplicates.nf b/DIMS/AverageTechReplicates.nf deleted file mode 100644 index f4b85b9a..00000000 --- a/DIMS/AverageTechReplicates.nf +++ /dev/null @@ -1,35 +0,0 @@ -process AverageTechReplicates { - tag "DIMS AverageTechReplicates" - label 'AverageTechReplicates' - container = 'docker://umcugenbioinf/dims:1.3' - shell = ['/bin/bash', '-euo', 'pipefail'] - - input: - path(rdata_file) - path(tic_txt_files) - path(init_file) - val(nr_replicates) - val(analysis_id) - val(matrix) - path(highest_mz_file) - path(breaks_file) - - output: - path('*_repl_pattern.RData'), emit: pattern_files - path('*_avg.RData'), emit: binned_files - path('miss_infusions_negative.txt') - path('miss_infusions_positive.txt') - path('*_TICplots.pdf') - - script: - """ - Rscript ${baseDir}/CustomModules/DIMS/AverageTechReplicates.R $init_file \ - $params.nr_replicates \ - $analysis_id \ - $matrix \ - $highest_mz_file \ - $breaks_file - """ -} - - From 6438e0e6cb0c24bf440b98b632b4d5abb46b9fb5 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 8 Jul 2025 15:43:38 +0200 Subject: [PATCH 043/161] removed breaks as input for PeakFinding --- DIMS/PeakFinding.nf | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/DIMS/PeakFinding.nf b/DIMS/PeakFinding.nf index dad2a497..04c82540 100644 --- a/DIMS/PeakFinding.nf +++ b/DIMS/PeakFinding.nf @@ -5,13 +5,14 @@ process PeakFinding { shell = ['/bin/bash', '-euo', 'pipefail'] input: - tuple(path(rdata_file), path(breaks_file)) + path(rdata_file) + each path(sample_techreps) output: - path '*tive.RData' + path '*tive.RData', optional: true script: """ - Rscript ${baseDir}/CustomModules/DIMS/PeakFinding.R $rdata_file $breaks_file $params.resolution $params.preprocessing_scripts_dir + Rscript ${baseDir}/CustomModules/DIMS/PeakFinding.R $rdata_file $params.resolution $params.preprocessing_scripts_dir """ } From 1ef197d7c52981cba19be936299c1a95c7492f3b Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 8 Jul 2025 15:44:27 +0200 Subject: [PATCH 044/161] changed PeakFinding to new two-step method --- DIMS/PeakFinding.R | 66 +++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/DIMS/PeakFinding.R b/DIMS/PeakFinding.R index c54a64cf..529370d4 100644 --- a/DIMS/PeakFinding.R +++ b/DIMS/PeakFinding.R @@ -2,57 +2,57 @@ cmd_args <- commandArgs(trailingOnly = TRUE) replicate_rdatafile <- cmd_args[1] -breaks_file <- cmd_args[2] -resol <- as.numeric(cmd_args[3]) -preprocessing_scripts_dir <- cmd_args[4] -peak_thresh <- 2000 +resol <- as.numeric(cmd_args[2]) +preprocessing_scripts_dir <- cmd_args[3] +# use fixed theshold between noise and signal for peak +peak_thresh <- 2000 -# load in function scripts +# source functions script source(paste0(preprocessing_scripts_dir, "peak_finding_functions.R")) -load(breaks_file) +library(dplyr) -# Load output of AssignToBins for a sample -sample_techrepl <- get(load(replicate_rdatafile)) +# Load output of AssignToBins (peak_list) for a technical replicate +load(replicate_rdatafile) techrepl_name <- colnames(peak_list$pos)[1] +# load list of technical replicates per sample that passed threshold filter +techreps_passed <- read.table("replicates_per_sample.txt", sep=",") + # Initialize options(digits = 16) # run the findPeaks function scanmodes <- c("positive", "negative") for (scanmode in scanmodes) { - # turn dataframe with intensities into a named list + # get intensities for scan mode if (scanmode == "positive") { ints_perscanmode <- peak_list$pos } else if (scanmode == "negative") { ints_perscanmode <- peak_list$neg } - - ints_fullrange <- as.vector(ints_perscanmode) - names(ints_fullrange) <- rownames(ints_perscanmode) - + + # check whether technical replicate has passed threshold filter for this scanmode + techreps_scanmode <- techreps_passed[grep(scanmode, techreps_passed[, 3]), ] + # if techrep is ok, it will be found. If not, skip this techrep. + if (length(grep(techrepl_name, techreps_scanmode)) == 0) { + break + } + + # put mz and intensities into dataframe + ints_fullrange <- as.data.frame(cbind(mz = as.numeric(rownames(ints_perscanmode)), + int = as.numeric(ints_perscanmode))) + # look for m/z range for all peaks - allpeaks_values <- search_mzrange(ints_fullrange, resol, techrepl_name, peak_thresh) - - # turn the list into a dataframe - outlist_techrep <- NULL - outlist_techrep <- cbind("samplenr" = allpeaks_values$nr, - "mzmed.pkt" = allpeaks_values$mean, - "fq" = allpeaks_values$qual, - "mzmin.pkt" = allpeaks_values$min, - "mzmax.pkt" = allpeaks_values$max, - "height.pkt" = allpeaks_values$area) - - # remove peaks with height = 0 - outlist_techrep <- outlist_techrep[outlist_techrep[, "height.pkt"] != 0, ] - + regions_of_interest <- search_regions_of_interest(ints_fullrange) + + # fit Gaussian curve and calculate integrated area under curve + integrated_peak_df <- integrate_peaks(ints_fullrange, regions_of_interest, resol, peak_thresh) + + # add sample name to dataframe + integrated_peak_df <- as.data.frame(cbind(samplenr = techrepl_name, integrated_peak_df)) + # save output to file - save(outlist_techrep, file = paste0(techrepl_name, "_", scanmode, ".RData")) - - # generate text output to log file on number of spikes for this sample - # spikes are peaks that are too narrow, e.g. 1 data point - cat(paste("There were", allpeaks_values$spikes, "spikes")) - + save(integrated_peak_df, file = paste0(techrepl_name, "_", scanmode, ".RData")) } From 9272ec3f93884064b0949d06bcc14c523b65a5a8 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 8 Jul 2025 15:46:17 +0200 Subject: [PATCH 045/161] functions for new two-step PeakFinding method --- DIMS/preprocessing/peak_finding_functions.R | 956 ++++---------------- 1 file changed, 194 insertions(+), 762 deletions(-) diff --git a/DIMS/preprocessing/peak_finding_functions.R b/DIMS/preprocessing/peak_finding_functions.R index 1baef120..8c2f62d7 100644 --- a/DIMS/preprocessing/peak_finding_functions.R +++ b/DIMS/preprocessing/peak_finding_functions.R @@ -1,633 +1,204 @@ # functions for peak finding -search_mzrange <- function(ints_fullrange, resol, sample_name, peak_thresh) { - #' Divide the full m/z range into regions of interest with min, max and mean m/z + +search_regions_of_interest <- function(ints_fullrange) { + #' Divide the full m/z range into regions of interest with indices #' - #' @param ints_fullrange: Named list of intensities (float) - #' @param resol: Value for resolution (integer) - #' @param sample_name: Sample name (string) - #' @param peak_thresh: Value for noise level threshold (integer) + #' @param ints_fullrange: Matrix with m/z values and intensities (float) #' - #' @return allpeaks_values: list of m/z regions of interest + #' @return regions_of_interest: matrix of m/z regions of interest (integer) - # initialize list to store results for all peaks - allpeaks_values <- list("mean" = NULL, "area" = NULL, "nr" = NULL, - "min" = NULL, "max" = NULL, "qual" = NULL, "spikes" = 0) - # find indices where intensity is not equal to zero - nonzero_positions <- as.vector(which(ints_fullrange != 0)) - - # initialize - # start position of the first peak - start_index <- nonzero_positions[1] - # maximum length of region of interest - max_roi_length <- 15 - - # find regions of interest - for (running_index in 1:length(nonzero_positions)) { - # find position of the end of a peak. - if (running_index < length(nonzero_positions) && (nonzero_positions[running_index + 1] - nonzero_positions[running_index]) > 1) { - end_index <- nonzero_positions[running_index] - # get m/z values and intensities for this region of interest - mass_vector <- as.numeric(names(ints_fullrange)[c(start_index:end_index)]) - int_vector <- as.vector(ints_fullrange[c(start_index:end_index)]) - # check if intensity is above threshold or the maximum intensity is NaN - if (max(int_vector) < peak_thresh || is.nan(max(int_vector))) { - # go to next region of interest - start_index <- nonzero_positions[running_index + 1] - next - } - # check if there are more intensities than maximum for region of interest - if (length(int_vector) > max_roi_length) { - print("vector of intensities is longer than max") - print(length(int_vector)) - # trim lowest intensities to zero - #int_vector[which(int_vector < min(int_vector) * 1.1)] <- 0 - # split the range into multiple sub ranges - #sub_range <- int_vector - #names(sub_range) <- mass_vector - #allpeaks_values <- search_mzrange(sub_range, allpeaks_values, resol, - # sample_name, peak_thresh) - # A proper peak needs to have at least 3 intensities above threshold - } else if (length(int_vector) > 3) { - # check if the sum of intensities is above zero. Why is this necessary? - #if (sum(int_vector) == 0) next - # define mass_diff as difference between last and first value of mass_vector - # mass_diff <- mass_vector[length(mass_vector)] - mass_vector[1] - # generate a second mass_vector with equally spaced m/z values - mass_vector_eq <- seq(mass_vector[1], mass_vector[length(mass_vector)], - length = 10 * length(mass_vector)) - - # Find the index in int_vector with the highest intensity - # max_index <- which(int_vector == max(int_vector)) - # get initial fit values - roi_values <- fit_gaussian(mass_vector_eq, mass_vector, int_vector, - resol, force_nr = length(max_index), - use_bounds = FALSE) - - if (roi_values$qual[1] == 1) { - # get optimized fit values - roi_values <- fit_optim(mass_vector, int_vector, resol) - # add region of interest to list of all peaks - allpeaks_values$mean <- c(allpeaks_values$mean, roi_values$mean) - allpeaks_values$area <- c(allpeaks_values$area, roi_values$area) - allpeaks_values$nr <- c(allpeaks_values$nr, sample_name) - allpeaks_values$min <- c(allpeaks_values$min, roi_values$min) - allpeaks_values$max <- c(allpeaks_values$max, roi_values$max) - allpeaks_values$qual <- c(allpeaks_values$qual, 0) - allpeaks_values$spikes <- allpeaks_values$spikes + 1 - - } else { - for (j in 1:length(roi_values$mean)){ - allpeaks_values$mean <- c(allpeaks_values$mean, roi_values$mean[j]) - allpeaks_values$area <- c(allpeaks_values$area, roi_values$area[j]) - allpeaks_values$nr <- c(allpeaks_values$nr, sample_name) - allpeaks_values$min <- c(allpeaks_values$min, roi_values$min[1]) - allpeaks_values$max <- c(allpeaks_values$max, roi_values$max[1]) - allpeaks_values$qual <- c(allpeaks_values$qual, roi_values$qual[1]) - } + nonzero_positions <- as.vector(which(ints_fullrange$int != 0)) + + # find regions of interest (look for consecutive numbers) + regions_of_interest_consec <- seqToIntervals(nonzero_positions) + # add length of regions of interest + regions_of_interest_diff <- regions_of_interest_consec[, 2] - regions_of_interest_consec[, 1] + 1 + regions_of_interest_length <- cbind(regions_of_interest_consec, length = regions_of_interest_diff) + # remove short lengths; a peak should have at least 3 data points + if (any(regions_of_interest_length[, "length"] < 3)) { + regions_of_interest_gte3 <- regions_of_interest_length[-which(regions_of_interest_length[, "length"] < 3), ] + } else { + regions_of_interest_gte3 <- regions_of_interest_length + } + # test for length of roi. If length is greater than 11, remove roi and break up into separate rois + remove_roi_index <- c() + new_rois_all <- regions_of_interest_gte3[0, ] + for (roi_nr in 1:nrow(regions_of_interest_gte3)) { + if (regions_of_interest_gte3[roi_nr, "length"] > 11) { + roi <- ints_fullrange[(regions_of_interest_gte3[roi_nr, "from"]:regions_of_interest_gte3[roi_nr, "to"]), ] + roi_intrange <- as.numeric(roi$int) + # look for local minima that separate the peaks + local_min_positions <- which(diff(sign(diff(roi_intrange))) == 2) + 1 + if (length(local_min_positions) > 0) { + remove_roi_index <- c(remove_roi_index, roi_nr) + # find new indices for rois after splitting + start_pos <- regions_of_interest_gte3[roi_nr, "from"] + new_rois <- data.frame(from = 0, to = 0, length = 0) + new_rois_splitroi <- regions_of_interest_gte3[0, ] + for (local_min_index in 1:length(local_min_positions)) { + new_rois[, 1] <- start_pos + new_rois[, 2] <- start_pos + local_min_positions[local_min_index] + new_rois[, 3] <- new_rois[, 2] - new_rois[, 1] + 1 + new_rois_splitroi <- rbind(new_rois_splitroi, new_rois) + start_pos <- new_rois[, 2] } - + # last part + new_rois[, 1] <- start_pos + new_rois[, 2] <- regions_of_interest_gte3[roi_nr, "to"] + new_rois[, 3] <- new_rois[, 2] - new_rois[, 1] + 1 + new_rois_splitroi <- rbind(new_rois_splitroi, new_rois) + # append + new_rois_all <- rbind(new_rois_all, new_rois_splitroi) } else { - - roi_values <- fit_optim(mass_vector, int_vector, resol) - allpeaks_values$mean <- c(allpeaks_values$mean, roi_values$mean) - allpeaks_values$area <- c(allpeaks_values$area, roi_values$area) - allpeaks_values$nr <- c(allpeaks_values$nr, sample_name) - allpeaks_values$min <- c(allpeaks_values$min, roi_values$min) - allpeaks_values$max <- c(allpeaks_values$max, roi_values$max) - allpeaks_values$qual <- c(allpeaks_values$qual, 0) - allpeaks_values$spikes <- allpeaks_values$spikes + 1 + # if there are no local minima, all intensities belong to one peak. + remove_roi_index <- c(remove_roi_index, roi_nr) + # look for maximum and take 5 intensities to the left and right + max_index <- which(roi_intrange == max(roi_intrange)) + max_pos <- as.numeric(rownames(roi)[max_index]) + new_roi <- data.frame(from = 0, to = 0, length = 0) + new_roi[, 1] <- max_pos - 5 + new_roi[, 2] <- max_pos + 5 + new_roi[, 3] <- new_roi[, 2] - new_roi[, 1] + 1 + # append + new_rois_all <- rbind(new_rois_all, new_roi) } } - start_index <- nonzero_positions[running_index + 1] } - return(allpeaks_values) -} - -fit_gaussian <- function(mass_vector_eq, mass_vector, int_vector, - resol, force_nr, use_bounds) { - #' Fit 1 or 2 Gaussian peaks in small region of m/z - #' - #' @param mass_vector_eq: Vector of equally spaced m/z values (float) - #' @param mass_vector: Vector of m/z values for a region of interest (float) - #' @param int_vector: Value used to calculate area under Gaussian curve (integer) - #' @param resol: Value for resolution (integer) - #' @param force_nr: Number of local maxima in int_vector (integer) - #' @param use_bounds: Boolean to indicate whether boundaries are to be used - #' - #' @return roi_value_list: list of fit values for region of interest (list) - - # Find the index in int_vector with the highest intensity - max_index <- which(int_vector == max(int_vector)) - - # Initialise - peak_mean <- NULL - peak_area <- NULL - peak_qual <- NULL - peak_min <- NULL - peak_max <- NULL - fit_quality1 <- 0.15 - fit_quality <- 0.2 - # set initial value for scale factor - scale_factor <- 2 - - # One local maximum: - if (force_nr == 1) { - # determine fit values for 1 Gaussian peak - fit_values <- fit_1peak(mass_vector_eq, mass_vector, int_vector, max_index, resol, - fit_quality1, use_bounds) - - # test if the mean is outside the m/z range - if (fit_values$mean[1] < mass_vector[1] || fit_values$mean[1] > mass_vector[length(mass_vector)]) { - # run this function again with fixed boundaries - return(fit_gaussian(mass_vector_eq, mass_vector, int_vector, resol, - force_nr = 1, use_bounds = TRUE)) - } else { - # test if the fit is bad - if (fit_values$qual > fit_quality1) { - # Try to fit two curves; find two local maxima. NB: max_index (now new_index) removed from fit_gaussian - new_index <- which(diff(sign(diff(int_vector))) == -2) + 1 - # test if there are two indices in new_index - if (length(new_index) != 2) { - new_index <- round(length(mass_vector) / 3) - new_index <- c(new_index, 2 * new_index) - } - # run this function again with two local maxima - return(fit_gaussian(mass_vector_eq, mass_vector, int_vector, - resol, force_nr = 2, use_bounds = FALSE)) - # good fit - } else { - peak_mean <- c(peak_mean, fit_values$mean) - peak_area <- c(peak_area, estimate_area(fit_values$mean, resol, fit_values$scale_factor, - fit_values$sigma)) - peak_qual <- fit_values$qual - peak_min <- mass_vector[1] - peak_max <- mass_vector[length(mass_vector)] - } - } - - #### Two local maxima; need at least 6 data points for this #### - } else if (force_nr == 2 && (length(mass_vector) > 6)) { - # determine fit values for 2 Gaussian peaks - fit_values <- fit_2peaks(mass_vector_eq, mass_vector, int_vector, max_index, resol, - use_bounds, fit_quality) - # test if one of the means is outside the m/z range - if (fit_values$mean[1] < mass_vector[1] || fit_values$mean[1] > mass_vector[length(mass_vector)] || - fit_values$mean[2] < mass_vector[1] || fit_values$mean[2] > mass_vector[length(mass_vector)]) { - # check if fit quality is bad - if (fit_values$qual > fit_quality) { - # run this function again with fixed boundaries - return(fit_gaussian(mass_vector_eq, mass_vector, int_vector, resol, - force_nr = 2, use_bounds = TRUE)) - } else { - # check which mean is outside range and remove it from the list of means - # NB: peak_mean and other variables have not been given values from 2-peak fit yet! - for (i in 1:length(fit_values$mean)){ - if (fit_values$mean[i] < mass_vector[1] || fit_values$mean[i] > mass_vector[length(mass_vector)]) { - peak_mean <- c(peak_mean, -i) - peak_area <- c(peak_area, -i) - } else { - peak_mean <- c(peak_mean, fit_values$mean[i]) - peak_area <- c(peak_area, fit_values$area[i]) - } - } - peak_qual <- fit_values$qual - peak_min <- mass_vector[1] - peak_max <- mass_vector[length(mass_vector)] - } - # if all means are within range - } else { - # check for bad fit - if (fit_values$qual > fit_quality) { - # Try to fit three curves; find three local maxima - new_index <- which(diff(sign(diff(int_vector))) == -2) + 1 - # test if there are three indices in new_index - if (length(new_index) != 3) { - new_index <- round(length(mass_vector) / 4) - new_index <- c(new_index, 2 * new_index, 3 * new_index) - } - # run this function again with three local maxima - return(fit_gaussian(mass_vector_eq, mass_vector, int_vector, - resol, force_nr = 3, use_bounds = FALSE)) - # good fit, all means are within m/z range - } else { - # check if means are within 3 ppm and sum if so - tmp <- fit_values$qual - nr_means_new <- -1 - nr_means <- length(fit_values$mean) - while (nr_means != nr_means_new) { - nr_means <- length(fit_values$mean) - fit_values <- within_ppm(fit_values$mean, fit_values$scale_factor, fit_values$sigma, fit_values$area, - mass_vector_eq, mass_vector, ppm = 4, resol) - nr_means_new <- length(fit_values$mean) - } - # restore original quality score - fit_values$qual <- tmp - - for (i in 1:length(fit_values$mean)){ - peak_mean <- c(peak_mean, fit_values$mean[i]) - peak_area <- c(peak_area, fit_values$area[i]) - } - peak_qual <- fit_values$qual - peak_min <- mass_vector[1] - peak_max <- mass_vector[length(mass_vector)] - } - } - - } else { # More than two local maxima; fit 1 peak. - fit_quality1 <- 0.40 - use_bounds <- TRUE - max_index <- which(int_vector == max(int_vector)) - fit_values <- fit_1peak(mass_vector_eq, mass_vector, int_vector, max_index, resol, - fit_quality1, use_bounds) - # check for bad fit - if (fit_values$qual > fit_quality1) { - # get fit values from fit_optim - fit_values <- fit_optim(mass_vector, int_vector, resol) - peak_mean <- c(peak_mean, fit_values$mean) - peak_area <- c(peak_area, fit_values$area) - peak_min <- fit_values$min - peak_max <- fit_values$max - peak_qual <- 0 - } else { - peak_mean <- c(peak_mean, fit_values$mean) - peak_area <- c(peak_area, estimate_area(fit_values$mean, resol, fit_values$scale_factor, fit_values$sigma)) - peak_qual <- fit_values$qual - peak_min <- mass_vector[1] - peak_max <- mass_vector[length(mass_vector)] - } + # remove rois that have been split into chunks or shortened + #print(dim(regions_of_interest_gte3)) + if (length(remove_roi_index) > 0) { + regions_of_interest_minus_short <- regions_of_interest_gte3[-remove_roi_index, ] + } else { + regions_of_interest_minus_short <- regions_of_interest_gte3 + } + #print(dim(regions_of_interest_minus_short)) + # combine remaining rois with info on chunks + regions_of_interest_split <- rbind(regions_of_interest_minus_short, new_rois_all) + # remove regions of interest with short lengths again + if (any(regions_of_interest_split[, "length"] < 3)) { + regions_of_interest_final <- regions_of_interest_split[-which(regions_of_interest_split[, "length"] < 3), ] + } else { + regions_of_interest_final <- regions_of_interest_split } - - # put all values for this region of interest into a list - roi_value_list <- list("mean" = peak_mean, - "area" = peak_area, - "qual" = peak_qual, - "min" = peak_min, - "max" = peak_max) - return(roi_value_list) -} - -fit_1peak <- function(mass_vector_eq, mass_vector, int_vector, max_index, - resol, fit_quality, use_bounds) { - #' Fit 1 Gaussian peak in small region of m/z - #' - #' @param mass_vector_eq: Vector of equally spaced m/z values (float) - #' @param mass_vector: Vector of m/z values for a region of interest (float) - #' @param int_vector: Value used to calculate area under Gaussian curve (integer) - #' @param max_index: Index in int_vector with the highest intensity (integer) - #' @param resol: Value for resolution (integer) - #' @param fit_quality: Value indicating quality of fit of Gaussian curve (float) - #' @param use_bounds: Boolean to indicate whether boundaries are to be used - #' - #' @return roi_value_list: list of fit values for region of interest (list) - - # set initial value for scale_factor - scale_factor <- 2 - - if (length(int_vector) < 3) { - message("Range too small, no fit possible!") + # sort on first index + if (nrow(regions_of_interest_final) > 1){ + regions_of_interest_sorted <- regions_of_interest_final %>% dplyr::arrange(from) } else { - if (length(int_vector) == 4) { - # fit 1 peak - weighted_mu <- weighted.mean(mass_vector, int_vector) - sigma <- get_stdev(mass_vector, int_vector) - fitted_peak <- optimize_1gaussian(mass_vector, int_vector, sigma, weighted_mu, scale_factor, use_bounds) - } else { - # set range vector - if ((length(mass_vector) - length(max_index)) < 2) { - range1 <- c((length(mass_vector) - 4) : length(mass_vector)) - } else if (length(max_index) < 2) { - range1 <- c(1:5) - } else { - range1 <- seq(from = (max_index[1] - 2), to = (max_index[1] + 2)) - } - # remove zero at the beginning of range1 - if (range1[1] == 0) { - range1 <- range1[-1] - } - # remove NA - if (length(which(is.na(int_vector[range1]))) != 0) { - range1 <- range1[-which(is.na(int_vector[range1]))] - } - # fit 1 peak - weighted_mu <- weighted.mean(mass_vector[range1], int_vector[range1]) - sigma <- get_stdev(mass_vector[range1], int_vector[range1]) - fitted_peak <- optimize_1gaussian(mass_vector, int_vector, sigma, weighted_mu, scale_factor, use_bounds) - } - - fitted_mz <- fitted_peak$par[1] - fitted_nr <- fitted_peak$par[2] - - # get new value for fit quality and scale_factor - fq_new <- get_fit_quality(mass_vector, int_vector, fitted_mz, resol, fitted_nr, sigma)$fq_new - scale_factor_new <- 1.2 * scale_factor - - # bad fit - if (fq_new > fit_quality) { - # optimize scaling factor - fq <- 0 - scale_factor <- 0 - if (sum(int_vector) > sum(fitted_nr * dnorm(mass_vector, fitted_mz, sigma))) { - # increase scale_factor until convergence - while ((round(fq, digits = 3) != round(fq_new, digits = 3)) && (scale_factor_new < 10000)) { - fq <- fq_new - scale_factor <- scale_factor_new - # fit 1 peak - fitted_peak <- optimize_1gaussian(mass_vector, int_vector, sigma, weighted_mu, scale_factor, use_bounds) - fitted_mz <- fitted_peak$par[1] - fitted_nr <- fitted_peak$par[2] - # get new value for fit quality and scale_factor - fq_new <- get_fit_quality(mass_vector, int_vector, fitted_mz, resol, fitted_nr, sigma)$fq_new - scale_factor_new <- 1.2 * scale_factor - } - } else { - # decrease scale_factor until convergence - while ((round(fq, digits = 3) != round(fq_new, digits = 3)) && (scale_factor_new < 10000)) { - fq <- fq_new - scale_factor <- scale_factor_new - # fit 1 peak - fitted_peak <- optimize_1gaussian(mass_vector, int_vector, sigma, weighted_mu, scale_factor, use_bounds) - fitted_mz <- fitted_peak$par[1] - fitted_nr <- fitted_peak$par[2] - # get new value for fit quality and scale_factor - fq_new <- get_fit_quality(mass_vector, int_vector, fitted_mz, resol, fitted_nr, sigma)$fq_new - scale_factor_new <- 0.8 * scale_factor - } - } - # use optimized scale_factor factor to fit 1 peak - if (fq < fq_new) { - fitted_peak <- optimize_1gaussian(mass_vector, int_vector, sigma, weighted_mu, scale_factor, use_bounds) - fitted_mz <- fitted_peak$par[1] - fitted_nr <- fitted_peak$par[2] - fq_new <- fq - } - } + regions_of_interest_sorted <- regions_of_interest_final } - - roi_value_list <- list("mean" = fitted_mz, "scale_factor" = fitted_nr, "sigma" = sigma, "qual" = fq_new) - return(roi_value_list) + + return(regions_of_interest_sorted) } -fit_2peaks <- function(mass_vector_eq, mass_vector, int_vector, max_index, resol, use_bounds = FALSE, - fit_quality) { - #' Fit 2 Gaussian peaks in a small region of m/z +integrate_peaks <- function(ints_fullrange, regions_of_interest, resol, peak_thresh) { + #' Fit Gaussian peak for each region of interest and integrate area under the curve #' - #' @param mass_vector_eq: Vector of equally spaced m/z values (float) - #' @param mass_vector: Vector of m/z values for a region of interest (float) - #' @param int_vector: Value used to calculate area under Gaussian curve (integer) - #' @param max_index: Index in int_vector with the highest intensity (integer) + #' @param ints_fullrange: Named list of intensities (float) + #' @param regions_of_interest: Named list of intensities (float) #' @param resol: Value for resolution (integer) - #' @param fit_quality: Value indicating quality of fit of Gaussian curve (float) - #' @param use_bounds: Boolean to indicate whether boundaries are to be used - #' - #' @return roi_value_list: list of fit values for region of interest (list) - - peak_mean <- NULL - peak_area <- NULL - peak_scale <- NULL - peak_sigma <- NULL - - # set range vectors for first peak - range1 <- seq(from = (max_index[1] - 2), to = (max_index[1] + 2)) - # remove zero at the beginning of range1 - if (range1[1] == 0) { - range1 <- range1[-1] - } - # set range vectors for second peak - range2 <- seq(from = (max_index[2] - 2), to = (max_index[2] + 2)) - # if range2 ends outside mass_vector, shorten it - if (length(mass_vector) < range2[length(range2)]) { - range2 <- range2[-length(range2)] - } - # check whether the two ranges overlap - range1 <- check_overlap(range1, range2)[[1]] - range2 <- check_overlap(range1, range2)[[2]] - # check for negative or 0 values in range1 or range2 - remove <- which(range1 < 1) - if (length(remove) > 0) { - range1 <- range1[-remove] - } - remove <- which(range2 < 1) - if (length(remove) > 0) { - range2 <- range2[-remove] - } - # remove NA - if (length(which(is.na(int_vector[range1]))) != 0) { - range1 <- range1[-which(is.na(int_vector[range1]))] - } - if (length(which(is.na(int_vector[range2]))) != 0) { - range2 <- range2[-which(is.na(int_vector[range2]))] - } - - # fit 2 peaks, first separately, then together - weighted_mu1 <- weighted.mean(mass_vector[range1], int_vector[range1]) - sigma1 <- get_stdev(mass_vector[range1], int_vector[range1]) - fitted_peak1 <- optimize_1gaussian(mass_vector[range1], int_vector[range1], sigma1, weighted_mu1, scale_factor, use_bounds) - fitted_mz1 <- fitted_peak1$par[1] - fitted_nr1 <- fitted_peak1$par[2] - # second peak - weighted_mu2 <- weighted.mean(mass_vector[range2], int_vector[range2]) - sigma2 <- get_stdev(mass_vector[range2], int_vector[range2]) - fitted_peak2 <- optimize_1gaussian(mass_vector[range2], int_vector[range2], sigma2, weighted_mu2, scale_factor, use_bounds) - fitted_mz2 <- fitted_peak2$par[1] - fitted_nr2 <- fitted_peak2$par[2] - # combined - fitted_2peaks <- optimize_2gaussians(mass_vector, int_vector, sigma1, sigma2, - fitted_mz1, fitted_nr1, - fitted_mz2, fitted_nr2, use_bounds) - fitted_2peaks_mz1 <- fitted_2peaks$par[1] - fitted_2peaks_nr1 <- fitted_2peaks$par[2] - fitted_2peaks_mz2 <- fitted_2peaks$par[3] - fitted_2peaks_nr2 <- fitted_2peaks$par[4] - - # get fit quality - if (is.null(sigma2)) { - sigma2 <- sigma1 + #' @param peak_thresh: Value for noise level threshold (integer) # NOT USED YET + #' + #' @return allpeaks_values: matrix of integrated peaks + + # initialize dataframe to store results for all peaks + allpeaks_values <- matrix(0, nrow = nrow(regions_of_interest), ncol = 5) + colnames(allpeaks_values) <- c("mzmed.pkt", "fq", "mzmin.pkt", "mzmax.pkt", "height.pkt") + + for (roi_nr in 1:nrow(regions_of_interest)) { + # find m/z values and intensities corresponding to the region of interest + index_range <- regions_of_interest[roi_nr, "from"] : regions_of_interest[roi_nr, "to"] + roi_mzrange <- as.numeric(ints_fullrange$mz[index_range]) + roi_intrange <- as.numeric(ints_fullrange$int[index_range]) + # find m/z value for maximum of peak (mu in Gaussian function) + weighted_mzmean <- weighted.mean(roi_mzrange, roi_intrange) + # find expected peak width at this m/z value + fwhm_mzmed <- get_fwhm(weighted_mzmean, resol) + # calculate sigma for Gaussian curve (https://en.wikipedia.org/wiki/Full_width_at_half_maximum) + sigma_peak <- fwhm_mzmed / 2.355 + # find scale factor for Gaussian. Initial estimate is maximum intensity in roi + # divided by 2 for better correlation with intensities from old PeakFinding method + scale_factor <- max(roi_intrange) / 2 + # fit Gaussian peak. Find intensities according to normal distribution + normal_ints <- scale_factor * gaussfunc(roi_mzrange, weighted_mzmean, sigma_peak) + # sum intensities in roi + sum_ints <- sum(roi_intrange) + sum_gauss <- sum(normal_ints) + # calculate quality of fit + quality_fit <- 1 - sum(abs(normal_ints - roi_intrange)) / sum_ints + # put all values into dataframe + allpeaks_values[roi_nr, "mzmed.pkt"] <- weighted_mzmean + allpeaks_values[roi_nr, "fq"] <- quality_fit + allpeaks_values[roi_nr, "mzmax.pkt"] <- max(roi_mzrange) + allpeaks_values[roi_nr, "mzmin.pkt"] <- min(roi_mzrange) + allpeaks_values[roi_nr, "height.pkt"] <- sum_gauss + } + + # remove peaks with height = 0, look for NA or NaN + remove_na <- which(is.na(as.numeric(allpeaks_values[, "height.pkt"]))) + if (length(remove_na) > 0) { + allpeaks_values <- allpeaks_values[-remove_na, ] } - sum_fit <- (fitted_2peaks_nr1 * dnorm(mass_vector, fitted_2peaks_mz1, sigma1)) + - (fitted_2peaks_nr2 * dnorm(mass_vector, fitted_2peaks_mz2, sigma2)) - lowest_mz <- sort(c(fitted_2peaks_mz1, fitted_2peaks_mz2))[1] - fq_new <- get_fit_quality(mass_vector, int_vector, lowest_mz, - resol, sum_fit = sum_fit)$fq_new - - # get parameter values - area1 <- estimate_area(fitted_2peaks_mz1, resol, fitted_2peaks_nr1, sigma1) - area2 <- estimate_area(fitted_2peaks_mz2, resol, fitted_2peaks_nr2, sigma2) - peak_area <- c(peak_area, area1, area2) - peak_mean <- c(peak_mean, fitted_2peaks_mz1, fitted_2peaks_mz2) - peak_scale <- c(peak_scale, fitted_2peaks_nr1, fitted_2peaks_nr2) - peak_sigma <- c(peak_sigma, sigma1, sigma2) - - roi_value_list <- list("mean" = peak_mean, "scale_factor" = peak_scale, "sigma" = peak_sigma, "area" = peak_area, "qual" = fq_new) - return(roi_value_list) -} -optimize_1gaussian <- function(mass_vector, int_vector, sigma, query_mass, scale_factor, use_bounds) { - #' Fit a Gaussian curve for a peak with given parameters - #' - #' @param mass_vector: Vector of masses (float) - #' @param int_vector: Vector of intensities (float) - #' @param sigma: Value for width of the peak (float) - #' @param query_mass: Value for mass at center of peak (float) - #' @param scale_factor: Value for scaling intensities (float) - #' @param use_bounds: Boolean to indicate whether boundaries are to be used - #' - #' @return opt_fit: list of parameters and values describing the optimal fit - - # define optimization function for optim based on normal distribution - opt_f <- function(params) { - d <- params[2] * dnorm(mass_vector, mean = params[1], sd = sigma) - sum((d - int_vector) ^ 2) - } - if (use_bounds) { - # determine lower and upper boundaries - lower <- c(mass_vector[1], 0, mass_vector[1], 0) - upper <- c(mass_vector[length(mass_vector)], Inf, mass_vector[length(mass_vector)], Inf) - # get optimal value for fitted Gaussian curve - opt_fit <- optim(c(as.numeric(query_mass), as.numeric(scale_factor)), - opt_f, control = list(maxit = 10000), method = "L-BFGS-B", - lower = lower, upper = upper) - } else { - opt_fit <- optim(c(as.numeric(query_mass), as.numeric(scale_factor)), - opt_f, control = list(maxit = 10000)) - } - return(opt_fit) + return(allpeaks_values) } -optimize_2gaussians <- function(mass_vector, int_vector, sigma1, sigma2, - query_mass1, scale_factor1, - query_mass2, scale_factor2, use_bounds) { - #' Fit two Gaussian curves for a peak with given parameters - #' - #' @param mass_vector: Vector of masses (float) - #' @param int_vector: Vector of intensities (float) - #' @param sigma1: Value for width of the first peak (float) - #' @param sigma2: Value for width of the second peak (float) - #' @param query_mass1: Value for mass at center of first peak (float) - #' @param scale_factor1: Value for scaling intensities for first peak (float) - #' @param query_mass2: Value for mass at center of second peak (float) - #' @param scale_factor2: Value for scaling intensities for second peak (float) - #' @param use_bounds: Boolean to indicate whether boundaries are to be used - #' - #' @return opt_fit: list of parameters and values describing the optimal fit - - # define optimization function for optim based on normal distribution - opt_f <- function(params) { - d <- params[2] * dnorm(mass_vector, mean = params[1], sd = sigma1) + - params[4] * dnorm(mass_vector, mean = params[3], sd = sigma2) - sum((d - int_vector) ^ 2) - } - - if (use_bounds) { - # determine lower and upper boundaries - lower <- c(mass_vector[1], 0, mass_vector[1], 0) - upper <- c(mass_vector[length(mass_vector)], Inf, mass_vector[length(mass_vector)], Inf) - # get optimal value for 2 fitted Gaussian curves - if (is.null(query_mass2) && is.null(scale_factor2) && is.null(sigma2)) { - sigma2 <- sigma1 - opt_fit <- optim(c(as.numeric(query_mass1), as.numeric(scale_factor1), - as.numeric(query_mass1), as.numeric(scale_factor1)), - opt_f, control = list(maxit = 10000), - method = "L-BFGS-B", lower = lower, upper = upper) - } else { - opt_fit <- optim(c(as.numeric(query_mass1), as.numeric(scale_factor1), - as.numeric(query_mass2), as.numeric(scale_factor2)), - opt_f, control = list(maxit = 10000), - method = "L-BFGS-B", lower = lower, upper = upper) - } - } else { - if (is.null(query_mass2) && is.null(scale_factor2) && is.null(sigma2)) { - sigma2 <- sigma1 - opt_fit <- optim(c(as.numeric(query_mass1), as.numeric(scale_factor1), - as.numeric(query_mass1), as.numeric(scale_factor1)), - opt_f, control = list(maxit = 10000)) - } else { - opt_fit <- optim(c(as.numeric(query_mass1), as.numeric(scale_factor1), - as.numeric(query_mass2), as.numeric(scale_factor2)), - opt_f, control = list(maxit = 10000)) +# in the next version of the docker image, package R.utils will be included +seqToIntervals <- function(idx) { + #' Find consecutive stretches of numbers + #' function seqToIntervals copied from R.utils library + #' see https://rdrr.io/cran/R.utils/src/R/seqToIntervals.R + #' + #' @param idx: Sequence of indices (integers) + #' + #' @return res: Matrix of start and end positions of consecutive numbers (matrix) + + # Clean up sequence + idx <- as.integer(idx) + idx <- unique(idx) + idx <- sort(idx) + + len_idx <- length(idx) + if (len_idx == 0L) { + res <- matrix(NA_integer_, nrow = 0L, ncol = 2L) + colnames(res) <- c("from", "to") + return(res) + } + + # Identify end points of intervals + diff_idx <- diff(idx) + diff_idx <- (diff_idx > 1) + diff_idx <- which(diff_idx) + nr_intervals <- length(diff_idx) + 1 + + # Allocate return matrix + res <- matrix(NA_integer_, nrow = nr_intervals, ncol = 2L) + colnames(res) <- c("from", "to") + + from_value <- idx[1] + to_value <- from_value - 1 + last_value <- from_value + + count <- 1 + for (running_index in seq_along(idx)) { + value <- idx[running_index] + if (value - last_value > 1) { + to_value <- last_value + res[count, ] <- c(from_value, to_value) + from_value <- value + count <- count + 1 } + last_value <- value } - return(opt_fit) -} -get_stdev <- function(mass_vector, int_vector, resol = 140000) { - #' Calculate standard deviation to determine width of a peak - #' - #' @param mass_vector: Vector of 3 mass values (float) - #' @param int_vector: Vector of 3 intensities (float) - #' @param resol: Value for resolution (integer) - #' - #' @return stdev: Value for standard deviation - # find maximum intensity in vector - max_index <- which(int_vector == max(int_vector)) - # find corresponding mass at maximum intensity - max_mass <- mass_vector[max_index] - # calculate resolution at given m/z value - resol_mz <- resol * (1 / sqrt(2) ^ (log2(max_mass / 200))) - # calculate full width at half maximum - fwhm <- max_mass / resol_mz - # calculate standard deviation - stdev <- (fwhm / 2) * 0.85 - return(stdev) -} - -get_fit_quality <- function(mass_vector, int_vector, mu_first, resol, scale_factor = NULL, sigma = NULL, sum_fit = NULL) { - #' Get fit quality for 1 Gaussian peak in small region of m/z - #' - #' @param mass_vector: Vector of m/z values for a region of interest (float) - #' @param int_vector: Value used to calculate area under Gaussian curve (integer) - #' @param mu_first: Value for first peak (float) - #' @param scale_factor: Initial value used to estimate scaling parameter (integer) - #' @param resol: Value for resolution (integer) - #' @param sum_fit: Value indicating quality of fit of Gaussian curve (float) - #' - #' @return list_params: list of parameters indicating quality of fit (list) - if (is.null(sum_fit)) { - mass_vector_int <- mass_vector - int_vector_int <- int_vector - # get new fit quality - fq_new <- mean(abs((scale_factor * dnorm(mass_vector_int, mu_first, sigma)) - int_vector_int) / - rep((max(scale_factor * dnorm(mass_vector_int, mu_first, sigma)) / 2), length(mass_vector_int))) - } else { - sum_fit_int <- sum_fit - int_vector_int <- int_vector - mass_vector_int <- mass_vector - # get new fit quality - fq_new <- mean(abs(sum_fit_int - int_vector_int) / rep(max(sum_fit_int) /2, length(sum_fit_int))) + if (to_value < from_value) { + to_value <- last_value + res[count, ] <- c(from_value, to_value) } - - # Prevent division by 0 - if (is.nan(fq_new)) fq_new <- 1 - - list_params <- list("fq_new" = fq_new, "x_int" = mass_vector_int, "y_int" = int_vector_int) - return(list_params) -} -estimate_area <- function(mass_max, resol, scale_factor, sigma) { - #' Estimate area of Gaussian curve - #' - #' @param mass_max: Value for m/z at maximum intensity of a peak (float) - #' @param resol: Value for resolution (integer) - #' @param scale_factor: Value for peak width (float) - #' @param sigma: Value for standard deviation (float) - #' - #' @return area_curve: Value for area under the Gaussian curve (float) - - # calculate width of peak at half maximum - fwhm <- get_fwhm(mass_max, resol) - - # generate a mass_vector with equally spaced m/z values - mz_min <- mass_max - 2 * fwhm - mz_max <- mass_max + 2 * fwhm - mz_range <- mz_max - mz_min - mass_vector_eq <- seq(mz_min, mz_max, length = 100) - - # estimate area under the curve - area_curve <- sum(scale_factor * dnorm(mass_vector_eq, mass_max, sigma)) / 100 - - return(area_curve) + return(res) } get_fwhm <- function(query_mass, resol) { @@ -641,7 +212,7 @@ get_fwhm <- function(query_mass, resol) { # set aberrant values of query_mass to default value of 200 if (is.na(query_mass)) { query_mass <- 200 - } + } if (query_mass < 0) { query_mass <- 200 } @@ -649,168 +220,29 @@ get_fwhm <- function(query_mass, resol) { resol_mz <- resol * (1 / sqrt(2) ^ (log2(query_mass / 200))) # calculate full width at half maximum fwhm <- query_mass / resol_mz - - return(fwhm) -} - -fit_optim <- function(mass_vector, int_vector, resol) { - #' Determine optimized fit of Gaussian curve to small region of m/z - #' - #' @param mass_vector: Vector of m/z values for a region of interest (float) - #' @param int_vector: Vector of intensities for a region of interest (float) - #' @param resol: Value for resolution (integer) - #' - #' @return roi_value_list: list of fit values for region of interest (list) - - # initial value for scale_factor - scale_factor <- 1.5 - - # Find the index in int_vector with the highest intensity - max_index <- which(int_vector == max(int_vector))[1] - mass_max <- mass_vector[max_index] - int_max <- int_vector[max_index] - # get peak width - fwhm <- get_fwhm(mass_max, resol) - # simplify the peak shape: represent it by a triangle - mass_max_simple <- c(mass_max - scale_factor * fwhm, mass_max, mass_max + scale_factor * fwhm) - int_max_simple <- c(0, int_max, 0) - - # define mass_diff as difference between last and first value of mass_max_simple - # mass_diff <- mass_max_simple[length(mass_max_simple)] - mass_max_simple[1] - # generate a second mass_vector with equally spaced m/z values - mass_vector_eq <- seq(mass_max_simple[1], mass_max_simple[length(mass_max_simple)], - length = 100 * length(mass_max_simple)) - sigma <- get_stdev(mass_vector_eq, int_max_simple) - # define optimization function for optim based on normal distribution - opt_f <- function(p, mass_vector, int_vector, sigma, mass_max) { - curve <- p * dnorm(mass_vector, mass_max, sigma) - return((max(curve) - max(int_vector))^2) - } - - # get optimal value for fitted Gaussian curve - opt_fit <- optimize(opt_f, c(0, 100000), tol = 0.0001, mass_vector, int_vector, sigma, mass_max) - scale_factor <- opt_fit$minimum - - # get an estimate of the area under the peak - area <- estimate_area(mass_max, resol, scale_factor, sigma) - # put all values for this region of interest into a list - roi_value_list <- list("mean" = mass_max, - "area" = area, - "min" = mass_vector_eq[1], - "max" = mass_vector_eq[length(mass_vector_eq)]) - return(roi_value_list) -} -within_ppm <- function(mean, scale_factor, sigma, area, mass_vector_eq, mass_vector, ppm = 4, resol) { - #' Test whether two mass ranges are within ppm distance of each other - #' - #' @param mean: Value for mean m/z (float) - #' @param scale_factor: Initial value used to estimate scaling parameter (integer) - #' @param sigma: Value for standard deviation (float) - #' @param area: Value for area under the curve (float) - #' @param mass_vector_eq: Vector of equally spaced m/z values (float) - #' @param mass_vector: Vector of m/z values for a region of interest (float) - #' @param ppm: Value for distance between two values of mass (integer) - #' @param resol: Value for resolution (integer) - #' - #' @return list_params: list of parameters indicating quality of fit (list) - - # sort - index <- order(mean) - mean <- mean[index] - scale_factor <- scale_factor[index] - sigma <- sigma[index] - area <- area[index] - - summed <- NULL - remove <- NULL - - if (length(mean) > 1) { - for (i in 2:length(mean)) { - if ((abs(mean[i - 1] - mean[i]) / mean[i - 1]) * 10^6 < ppm) { - - # avoid double occurrance in sum - if ((i - 1) %in% summed) next - - result_values <- sum_curves(mean[i - 1], mean[i], scale_factor[i - 1], scale_factor[i], sigma[i - 1], sigma[i], - mass_vector_eq, mass_vector, resol) - summed <- c(summed, i - 1, i) - if (is.nan(result_values$mean)) result_values$mean <- 0 - mean[i - 1] <- result_values$mean - mean[i] <- result_values$mean - area[i - 1] <- result_values$area - area[i] <- result_values$area - scale_factor[i - 1] <- result_values$scale_factor - scale_factor[i] <- result_values$scale_factor - sigma[i - 1] <- result_values$sigma - sigma[i] <- result_values$sigma - - remove <- c(remove, i) - } - } - } - - if (length(remove) != 0) { - mean <- mean[-c(remove)] - area <- area[-c(remove)] - scale_factor <- scale_factor[-c(remove)] - sigma <- sigma[-c(remove)] - } - - list_params <- list("mean" = mean, "area" = area, "scale_factor" = scale_factor, "sigma" = sigma, "qual" = NULL) - return(list_params) + return(fwhm) } -sum_curves <- function(mean1, mean2, scale_factor1, scale_factor2, sigma1, sigma2, mass_vector_eq, mass_vector, resol) { - #' Sum two curves - #' - #' @param mean1: Value for mean m/z of first peak (float) - #' @param mean2: Value for mean m/z of second peak (float) - #' @param scale_factor1: Initial value used to estimate scaling parameter for first peak (integer) - #' @param scale_factor2: Initial value used to estimate scaling parameter for second peak (integer) - #' @param sigma1: Value for standard deviation for first peak (float) - #' @param sigma2: Value for standard deviation for second peak (float) - #' @param mass_vector_eq: Vector of equally spaced m/z values (float) - #' @param mass_vector: Vector of m/z values for a region of interest (float) - #' @param resol: Value for resolution (integer) - #' - #' @return list_params: list of parameters indicating quality of fit (list) - - sum_fit <- (scale_factor1 * dnorm(mass_vector_eq, mean1, sigma1)) + (scale_factor2 * dnorm(mass_vector_eq, mean2, sigma2)) - - mean1_plus2 <- weighted.mean(c(mean1, mean2), c(max(scale_factor1 * dnorm(mass_vector_eq, mean1, sigma1)), - max(scale_factor2 * dnorm(mass_vector_eq, mean2, sigma2)))) - - # get new values for parameters - fwhm <- get_fwhm(mean1_plus2, resol) - area <- max(sum_fit) - scale_factor <- scale_factor1 + scale_factor2 - sigma <- (fwhm / 2) * 0.85 - - list_params <- list("mean" = mean1_plus2, "area" = area, "scale_factor" = scale_factor, "sigma" = sigma) - return(list_params) -} -check_overlap <- function(range1, range2) { - #' Modify range1 and range2 in case of overlap - #' - #' @param range1: Vector of m/z values for first peak (float) - #' @param range2: Vector of m/z values for second peak (float) - #' - #' @return new_ranges: list of two ranges (list) - - # Check for overlap - if (length(intersect(range1, range2)) == 2) { - if (length(range1) >= length(range2)) { - range1 <- range1[-length(range1)] - } else { - range2 <- range2[-1] - } - } else if (length(intersect(range1, range2)) == 3) { - range1 <- range1[-length(range1)] - range2 <- range2[-1] - } - new_ranges <- list("range1" = range1, "range2" = range2) - return(new_ranges) +# from https://rdrr.io/cran/rvmethod/src/R/gaussfit.R +#' Gaussian Function from package rvmethod +#' +#' This function returns the unnormalized (height of 1.0) Gaussian curve with a +#' given center and spread. +#' +#' @param x the vector of values at which to evaluate the Gaussian +#' @param mu the center of the Gaussian +#' @param sigma the spread of the Gaussian (must be greater than 0) +#' @return vector of values of the Gaussian +#' @examples x = seq(-4, 4, length.out = 100) +#' y = gaussfunc(x, 0, 1) +#' plot(x, y) +#' +#' @import stats +#' +#' @export +gaussfunc <- function(x, mu, sigma) { + return(exp(-((x - mu) ^ 2) / (2 * (sigma ^ 2)))) } From e006160d51ab942a4da4f88173b0ba1906717624 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 8 Jul 2025 15:47:10 +0200 Subject: [PATCH 046/161] unit tests for new two-step PeakFinding method --- .../testthat/test_peak_finding_functions.R | 261 +++++------------- 1 file changed, 65 insertions(+), 196 deletions(-) diff --git a/DIMS/tests/testthat/test_peak_finding_functions.R b/DIMS/tests/testthat/test_peak_finding_functions.R index a0db0ca9..1e001a0c 100644 --- a/DIMS/tests/testthat/test_peak_finding_functions.R +++ b/DIMS/tests/testthat/test_peak_finding_functions.R @@ -1,218 +1,87 @@ -# unit tests for PeakFinding functions: +# unit tests for PeakFinding functions: source("../preprocessing/peak_finding_functions.R") -# test fit_quality function: -test_that("fit quality is correctly calculated", { - # initialize - test_resol <- 140000 - test_scale_factor <- 2 - test_sigma <- 0.0001 - test_sum_fit <- NULL - - # create peak info to test on: - test_mass_vector <- rep(70.00938, 5) + 1:5 * 0.00006 - test_int_vector <- c(9000, 16000, 19000, 15000, 6000) - test_mz_max <- max(test_mass_vector) - - expect_type(get_fit_quality(test_mass_vector, test_int_vector, test_mz_max, test_resol, test_scale_factor, test_sigma, test_sum_fit), "list") - expect_equal(get_fit_quality(test_mass_vector, test_int_vector, test_mz_max, test_resol, test_scale_factor, test_sigma, test_sum_fit)$fq_new, 2.426, tolerance = 0.001, TRUE) -}) - -# test get_stdev: -test_that("standard deviation is correctly calculated", { - # create peak info to test on: - test_mass_vector <- rep(70.00938, 5) + 1:5 * 0.00006 - test_int_vector <- c(9000, 16000, 19000, 15000, 6000) - test_resol <- 140000 - - expect_type(get_stdev(test_mass_vector, test_int_vector, test_resol), "double") - expect_equal(get_stdev(test_mass_vector, test_int_vector, test_resol), 0.000126, tolerance = 0.00001, TRUE) -}) - -# test estimate_area -test_that("area under the curve is correctly calculated", { - # create peak info to test on: - test_mass_vector <- rep(70.00938, 5) + 1:5 * 0.00006 - test_mz_max <- max(test_mass_vector) - test_resol <- 140000 - test_scale_factor <- 2 - test_sigma <- 0.0001 - - expect_type(estimate_area(test_mz_max, test_resol, test_scale_factor, test_sigma), "double") - expect_equal(estimate_area(test_mz_max, test_resol, test_scale_factor, test_sigma), 1673.061, tolerance = 0.001, TRUE) -}) - -# test get_fwhm -test_that("fwhm is correctly calculated", { - # create peak info to test on: - test_mass_vector <- rep(70.00938, 5) + 1:5 * 0.00006 - test_mz_max <- max(test_mass_vector) - test_resol <- 140000 - - expect_type(get_fwhm(test_mz_max, test_resol), "double") - expect_equal(get_fwhm(test_mz_max, test_resol), 0.000295865, tolerance = 0.000001, TRUE) -}) - -# test get_fwhm for NA -test_that("fwhm for mass NA gives standard fwhm", { - test_resol <- 140000 - - expect_type(get_fwhm(NA, test_resol), "double") - expect_equal(get_fwhm(NA, test_resol), 0.001428571, tolerance = 0.000001, TRUE) -}) - -# test within_ppm -test_that("two peaks are within 5 ppm", { - # create peak info to test on: - test_mass_vector <- rep(70.00938, 5) + 1:5 * 0.00006 - test_mz_max <- max(test_mass_vector) - test_scale_factor <- 2 - test_sigma <- 0.0001 - test_area <- 20000 - test_mass_vector_eq <- seq(min(test_mass_vector), max(test_mass_vector), length = 100) - test_ppm <- 5 - test_resol <- 140000 - - expect_type(within_ppm(test_mz_max, test_scale_factor, test_sigma, test_area, test_mass_vector_eq, test_mass_vector, test_ppm, test_resol), "list") - expect_equal(within_ppm(test_mz_max, test_scale_factor, test_sigma, test_area, test_mass_vector_eq, test_mass_vector, test_ppm, test_resol)$mean, 70.00962, tolerance = 0.0001, TRUE) -}) - -# test sum_curves -test_that("two curves are correctly summed", { - # create peak info to test on: - test_mass_vector <- rep(70.00938, 5) + 1:5 * 0.00006 - test_mass_vector_eq <- seq(min(test_mass_vector), max(test_mass_vector), length = 100) - test_mean1 <- 70.00938 - test_mean2 <- 70.00962 - test_scale_factor1 <- 2 - test_scale_factor2 <- 4 - test_sigma1 <- 0.0001 - test_sigma2 <- 0.0002 - test_resol <- 140000 +# test search_regions_of_interest function: +test_that("regions of interest are correctly found for a single peak", { + # create intensities to test on + test_stdev <- 0.00006 + test_mass_vector <- rep(70, 10) + 1:10 * test_stdev + test_ints_range <- dnorm(test_mass_vector, mean = 70.0003, sd = test_stdev) + test_ints_fullrange <- as.data.frame(cbind(mz = test_mass_vector, + int = test_ints_range)) - expect_type(sum_curves(test_mean1, test_mean2, test_scale_factor1, test_scale_factor2, test_sigma1, test_sigma2, test_mass_vector_eq, test_mass_vector, test_resol), "list") - expect_equal(sum_curves(test_mean1, test_mean2, test_scale_factor1, test_scale_factor2, test_sigma1, test_sigma2, test_mass_vector_eq, test_mass_vector, test_resol)$mean, 70.0095, tolerance = 0.0001, TRUE) + # expected output + expected_output <- matrix(c(1, 10, 10), nrow = 1, ncol = 3L) + colnames(expected_output) <- c("from", "to", "length") + rownames(expected_output) <- "to" + + # tests + expect_type(search_regions_of_interest(test_ints_fullrange), "double") + expect_equal(nrow(search_regions_of_interest(test_ints_fullrange)), 1) + expect_equal(search_regions_of_interest(test_ints_fullrange), expected_output, tolerance = 0.000001, TRUE) }) - -# test fit_optim -test_that("optimal peak fit can be found", { - # create peak info to test on: - test_mass_vector <- rep(70.00938, 5) + 1:5 * 0.00006 - test_int_vector <- c(9000, 16000, 19000, 15000, 6000) - test_resol <- 140000 - # create output to test on: - test_output <- list(mean = 70.0095, area = 5009.596, min = 70.00906, max = 70.00994) +test_that("regions of interest are correctly found for two peaks", { + # create intensities to test on + test_stdev <- 0.00006 + test_mass_vector <- rep(70, 20) + 1:20 * test_stdev + test_ints_range <- dnorm(test_mass_vector, mean = 70.0003, sd = test_stdev) + + dnorm(test_mass_vector, mean = 70.0005, sd = test_stdev) + test_ints_fullrange <- as.data.frame(cbind(mz = test_mass_vector, + int = test_ints_range)) - expect_type(fit_optim(test_mass_vector, test_int_vector, test_resol), "list") - for (key in names(test_output)) { - expect_equal(fit_optim(test_mass_vector, test_int_vector, test_resol)[[key]], test_output[[key]], tolerance = 0.0001, TRUE) - } -}) - -# test optimize_1gaussian: -test_that("optimal value for m/z and scale_factor can be found", { - # create peak info to test on: - test_mass_vector <- rep(70.00938, 5) + 1:5 * 0.00006 - test_int_vector <- c(9000, 16000, 19000, 15000, 6000) - test_sigma <- 0.0001 - test_query_mass <- median(test_mass_vector) - test_scale_factor <- 2 - test_use_bounds <- FALSE + # expected output + expected_output <- as.data.frame(matrix(c(1, 8, 8, 20, 8, 13), nrow = 2, ncol = 3)) + colnames(expected_output) <- c("from", "to", "length") + rownames(expected_output) <- as.character(c(1, 2)) - expect_type(optimize_1gaussian(test_mass_vector, test_int_vector, test_sigma, test_query_mass, test_scale_factor, test_use_bounds), "list") - expect_equal(optimize_1gaussian(test_mass_vector, test_int_vector, test_sigma, test_query_mass, test_scale_factor, test_use_bounds)$par[1], 70.00949, tolerance = 0.0001, TRUE) - expect_equal(optimize_1gaussian(test_mass_vector, test_int_vector, test_sigma, test_query_mass, test_scale_factor, test_use_bounds)$par[2], 4.570024, tolerance = 0.0001, TRUE) + # tests + expect_type(search_regions_of_interest(test_ints_fullrange), "list") + expect_equal(nrow(search_regions_of_interest(test_ints_fullrange)), 2) + expect_equal(search_regions_of_interest(test_ints_fullrange)[, "length"], as.numeric(expected_output[, "length"])) + expect_equal(search_regions_of_interest(test_ints_fullrange), expected_output, check.attributes = FALSE) }) -# test fit_1peak: -test_that("correct mean m/z and scale_factor can be found for 1 peak", { +# test integrate_peaks function: +test_that("peaks are correctly integrated", { # create peak info to test on: - test_mass_vector <- rep(70.00938, 5) + 1:5 * 0.00006 - test_int_vector <- c(9000, 16000, 19000, 15000, 6000) - test_mass_vector_eq <- seq(min(test_mass_vector), max(test_mass_vector), length = 100) - test_max_index <- which(test_int_vector == max(test_int_vector)) - test_resol <- 140000 - test_fit_quality <- 0.5 - test_use_bounds <- FALSE - # create output to test on: - test_output <- list(mean = 70.0095, scale_factor = 5.23334, sigma = 0.000126, qual = 0.24300) + test_stdev <- 0.00006 + test_mass_vector <- rep(70, 20) + 1:20 * test_stdev + test_ints_range <- dnorm(test_mass_vector, mean = 70.0003, sd = test_stdev) + + dnorm(test_mass_vector, mean = 70.0005, sd = test_stdev) + test_ints_fullrange <- as.data.frame(cbind(mz = test_mass_vector, + int = test_ints_range)) - expect_type(fit_1peak(test_mass_vector_eq, test_mass_vector, test_int_vector, test_max_index, test_resol, test_fit_quality, test_use_bounds), "list") - for (key in names(test_output)) { - expect_equal(fit_1peak(test_mass_vector_eq, test_mass_vector, test_int_vector, test_max_index, test_resol, test_fit_quality, test_use_bounds)[[key]], test_output[[key]], tolerance = 0.0001, TRUE) - } -}) - -# test optimize_2gaussians: -test_that("optimal value for m/z and scale_factor can be found", { - # create info for two peaks in a small region - test_mass_vector_2peaks <- rep(70.00938, 14) + 1:14 * 0.00006 - test_int_vector_2peaks <- rep(c(2000, 9000, 16000, 19000, 15000, 6000, 3000), 2) - test_mass_vector_eq <- seq(min(test_mass_vector_2peaks), max(test_mass_vector_2peaks), length = 100) - test_max_index <- which(test_int_vector_2peaks == max(test_int_vector_2peaks)) - test_sigma1 <- 0.0001 - test_sigma2 <- 0.0002 - test_query_mass1 <- test_max_index[1] - test_query_mass2 <- test_max_index[2] - test_scale_factor1 <- 2 - test_scale_factor2 <- 3 - test_use_bounds <- FALSE + # create regions of interest to test on: + test_regions_of_interest <- as.data.frame(matrix(c(1, 8, 8, 20, 8, 13), nrow = 2, ncol = 3L)) + colnames(test_regions_of_interest) <- c("from", "to", "length") + rownames(test_regions_of_interest) <- as.character(c(1, 2)) - expect_type(optimize_2gaussians(test_mass_vector_2peaks, test_int_vector_2peaks, test_sigma1, test_sigma2, test_query_mass1, test_scale_factor1, test_query_mass2, test_scale_factor2, test_use_bounds), "list") - expect_equal(optimize_2gaussians(test_mass_vector_2peaks, test_int_vector_2peaks, test_sigma1, test_sigma2, test_query_mass1, test_scale_factor1, test_query_mass2, test_scale_factor2, test_use_bounds)$par[1], 4, tolerance = 0.1, TRUE) -}) - -# test fit_2peaks: -test_that("correct mean m/z and scale_factor can be found for 2 peaks", { - # create info for two peaks in a small region - test_mass_vector_2peaks <- rep(70.00938, 14) + 1:14 * 0.00006 - test_int_vector_2peaks <- rep(c(2000, 9000, 16000, 19000, 15000, 6000, 3000), 2) - test_mass_vector_eq <- seq(min(test_mass_vector_2peaks), max(test_mass_vector_2peaks), length = 100) - test_max_index <- which(test_int_vector_2peaks == max(test_int_vector_2peaks)) + # set other parameters test_resol <- 140000 - test_fit_quality <- 0.5 - test_use_bounds <- FALSE - # create output to test on: - test_output <- list(mean = c(70.00954, 70.00998), scale_factor = c(4.823598, 4.828072), sigma = c(0.0001257425, 0.0001257436), qual = 0.38341) + test_peak_thresh <- 2000 + + # expected output + expected_output <- as.data.frame(matrix(c(70.000357, 70.000521, 0.5148094, 0.3016569, 70.00006, 70.00048, + 70.00048, 70.00120, 15526.826791, 11950.90570), nrow = 2, ncol = 5)) + colnames(expected_output) <- c("mzmed.pkt", "fq", "mzmin.pkt", "mzmax.pkt", "height.pkt") - expect_type(fit_2peaks(test_mass_vector_eq, test_mass_vector_2peaks, test_int_vector_2peaks, test_max_index, - test_resol, test_use_bounds, test_fit_quality), "list") - for (key in names(test_output)) { - expect_equal(fit_2peaks(test_mass_vector_eq, test_mass_vector_2peaks, test_int_vector_2peaks, test_max_index, - test_resol, test_use_bounds, test_fit_quality)[[key]], test_output[[key]], tolerance = 0.0001, TRUE) - } + expect_type(integrate_peaks(test_ints_fullrange, test_regions_of_interest, test_resol, test_peak_thresh), "double") + expect_equal(nrow(integrate_peaks(test_ints_fullrange, test_regions_of_interest, test_resol, test_peak_thresh)), 2) + expect_equal(integrate_peaks(test_ints_fullrange, test_regions_of_interest, test_resol, test_peak_thresh)[, "mzmed.pkt"], + expected_output[, "mzmed.pkt"], tolerance = 0.0001) + expect_equal(integrate_peaks(test_ints_fullrange, test_regions_of_interest, test_resol, test_peak_thresh)[, "height.pkt"], + expected_output[, "height.pkt"], tolerance = 0.0001) }) -# test fit_gaussian(mass_vector2, mass_vector, int_vector, resol, force, use_bounds) -test_that("initial gaussian fit is done correctly", { +# test get_fwhm function +test_that("fwhm is correctly calculated", { # create peak info to test on: test_mass_vector <- rep(70.00938, 5) + 1:5 * 0.00006 - test_int_vector <- c(9000, 16000, 19000, 15000, 6000) - test_mass_vector_eq <- seq(min(test_mass_vector), max(test_mass_vector), length = 100) + test_mz_max <- max(test_mass_vector) test_resol <- 140000 - test_use_bounds <- FALSE - test_force_nr <- 1 - - expect_type(fit_gaussian(test_mass_vector_eq, test_mass_vector, test_int_vector, test_resol, test_force_nr, test_use_bounds), "list") - expect_equal(fit_gaussian(test_mass_vector_eq, test_mass_vector, test_int_vector, test_resol, test_force_nr, test_use_bounds)$mean, 70.00956, tolerance = 0.00001, TRUE) -}) -# test search_mzrange(ints_fullrange, resol, sample_name, peak_thresh) -test_that("all peak finding functions work together", { - # enable snapshot - local_edition(3) - # create info for ten peaks separated by zero - test_large_mass_vector <- rep(70.00938, 80) + 1:80 * 0.00006 - test_large_int_vector <- rep(c(2000, 9000, 16000, 19000, 15000, 6000, 3000, 0), 10) - names(test_large_int_vector) <- test_large_mass_vector - test_resol <- 140000 - test_sample_name <- "C1.1" - test_peak_thresh <- 100 - - expect_type(search_mzrange(test_large_int_vector, test_resol, test_sample_name, test_peak_thresh), "list") - expect_snapshot(search_mzrange(test_large_int_vector, test_resol, test_sample_name, test_peak_thresh), error = FALSE) + expect_type(get_fwhm(test_mz_max, test_resol), "double") + expect_equal(get_fwhm(test_mz_max, test_resol), 0.000295865, tolerance = 0.000001, TRUE) }) -# test fit_gaussian(mass_vector2, mass_vector, int_vector, resol, force, use_bounds) -test_that("fit gaussian") From 8102bdb83853b7d02eef2a928543bad0d7dabc4c Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 8 Jul 2025 15:54:33 +0200 Subject: [PATCH 047/161] information for averaging peaks for technical replicates based on txt file with scanmode --- DIMS/AveragePeaks.R | 126 +++++++++++++++++++++---------------------- DIMS/AveragePeaks.nf | 4 +- 2 files changed, 62 insertions(+), 68 deletions(-) diff --git a/DIMS/AveragePeaks.R b/DIMS/AveragePeaks.R index 695c08fa..eef2d8a2 100644 --- a/DIMS/AveragePeaks.R +++ b/DIMS/AveragePeaks.R @@ -1,72 +1,66 @@ +library(dplyr) + # define parameters -# ppm as fixed value, not the same ppm as in peak grouping -ppm_peak <- 2 +cmd_args <- commandArgs(trailingOnly = TRUE) -library(dplyr) +sample_name <- cmd_args[1] +techreps <- cmd_args[2] +scanmode <- cmd_args[3] +tech_reps <- strsplit(techreps, ";")[[1]] +print(sample_name) +print(techreps) +print(scanmode) -scanmodes <- c("positive", "negative") +# set ppm as fixed value, not the same ppm as in peak grouping +ppm_peak <- 2 -for (scanmode in scanmodes){ - # get sample names - load(paste0(scanmode, "_repl_pattern.RData")) - sample_names <- names(repl_pattern_filtered) - # initialize - outlist_total <- NULL - # for each biological sample, average peaks in technical replicates - for (sample_name in sample_names) { - print(sample_name) - # Initialize per sample - peaklist_allrepl <- NULL - nr_repl_persample <- 0 - # averaged_peaks <- matrix(0, nrow = 10 ^ 7, ncol = 6) # how big does it need to be? - averaged_peaks <- matrix(0, nrow = 0, ncol = 6) # append - colnames(averaged_peaks) <- c("samplenr", "mzmed.pkt", "fq", "mzmin.pkt", "mzmax.pkt", "height.pkt") - # load RData files of technical replicates belonging to biological sample - sample_techreps_file <- repl_pattern_filtered[sample_name][[1]] - for (file_nr in 1:length(sample_techreps_file)) { - print(sample_techreps_file[file_nr]) - tech_repl_file <- paste0(sample_techreps_file[file_nr], "_positive.RData") - tech_repl <- get(load(tech_repl_file)) - # combine data for all technical replicates - peaklist_allrepl <- rbind(peaklist_allrepl, tech_repl) - # count number of replicates for each biological sample - nr_repl_persample <- nr_repl_persample + 1 - } - # sort on mass - peaklist_allrepl_df <- as.data.frame(peaklist_allrepl) - peaklist_allrepl_df$mzmed.pkt <- as.numeric(peaklist_allrepl_df$mzmed.pkt) - peaklist_allrepl_df$height.pkt <- as.numeric(peaklist_allrepl_df$height.pkt) - # peaklist_allrepl_sorted <- peaklist_allrepl_df %>% arrange(desc(height.pkt)) - peaklist_allrepl_sorted <- peaklist_allrepl_df %>% arrange(mzmed.pkt) - # average over technical replicates - while (nrow(peaklist_allrepl_sorted) > 1) { - # store row numbers - peaklist_allrepl_sorted$rownr <- 1:nrow(peaklist_allrepl_sorted) - # find the peaks in the dataset with corresponding m/z plus or minus tolerance - reference_mass <- peaklist_allrepl_sorted$mzmed.pkt[1] - mz_tolerance <- (reference_mass * ppm_peak) / 10^6 - minmz_ref <- reference_mass - mz_tolerance - maxmz_ref <- reference_mass + mz_tolerance - select_peak_indices <- which((peaklist_allrepl_sorted$mzmed.pkt > minmz_ref) & (peaklist_allrepl_sorted$mzmed.pkt < maxmz_ref)) - select_peaks <- peaklist_allrepl_sorted[select_peak_indices, ] - nrsamples <- length(select_peak_indices) - # put averaged intensities into a new row and append to averaged_peaks - averaged_1peak <- matrix(0, nrow = 1, ncol = 6) - colnames(averaged_1peak) <- c("samplenr", "mzmed.pkt", "fq", "mzmin.pkt", "mzmax.pkt", "height.pkt") - # calculate m/z values for peak group - averaged_1peak[1, "mzmed.pkt"] <- mean(select_peaks$mzmed.pkt) - averaged_1peak[1, "mzmin.pkt"] <- min(select_peaks$mzmed.pkt) - averaged_1peak[1, "mzmax.pkt"] <- max(select_peaks$mzmed.pkt) - averaged_1peak[1, "fq"] <- nrsamples - averaged_1peak[1, "height.pkt"] <- mean(select_peaks$height.pkt) - # put intensities into proper columns - peaklist_allrepl_sorted <- peaklist_allrepl_sorted[-select_peaks$rownr, ] - averaged_peaks <- rbind(averaged_peaks, averaged_1peak) - } - # add sample name to first column and append to outlist_total for all samples - averaged_peaks[ , "samplenr"] <- sample_name - outlist_total <- rbind(outlist_total, averaged_peaks) - } - save(outlist_total, file = paste0("AvgPeaks_", scanmode, ".RData")) +# average peaks in technical replicates +# Initialize per sample +peaklist_allrepl <- NULL +nr_repl_persample <- 0 +averaged_peaks <- matrix(0, nrow = 0, ncol = 6) +colnames(averaged_peaks) <- c("samplenr", "mzmed.pkt", "fq", "mzmin.pkt", "mzmax.pkt", "height.pkt") +# load RData files of technical replicates belonging to biological sample +for (file_nr in 1:length(tech_reps)) { + tech_repl_file <- paste0(tech_reps[file_nr], "_", scanmode, ".RData") + tech_repl <- get(load(tech_repl_file)) + # combine data for all technical replicates + peaklist_allrepl <- rbind(peaklist_allrepl, tech_repl) + # count number of replicates for each biological sample + nr_repl_persample <- nr_repl_persample + 1 +} +# sort on mass +peaklist_allrepl_df <- as.data.frame(peaklist_allrepl) +peaklist_allrepl_df$mzmed.pkt <- as.numeric(peaklist_allrepl_df$mzmed.pkt) +peaklist_allrepl_df$height.pkt <- as.numeric(peaklist_allrepl_df$height.pkt) +# peaklist_allrepl_sorted <- peaklist_allrepl_df %>% arrange(desc(height.pkt)) +peaklist_allrepl_sorted <- peaklist_allrepl_df %>% arrange(mzmed.pkt) +# average over technical replicates +while (nrow(peaklist_allrepl_sorted) > 1) { + # store row numbers + peaklist_allrepl_sorted$rownr <- 1:nrow(peaklist_allrepl_sorted) + # find the peaks in the dataset with corresponding m/z plus or minus tolerance + reference_mass <- peaklist_allrepl_sorted$mzmed.pkt[1] + mz_tolerance <- (reference_mass * ppm_peak) / 10^6 + minmz_ref <- reference_mass - mz_tolerance + maxmz_ref <- reference_mass + mz_tolerance + select_peak_indices <- which((peaklist_allrepl_sorted$mzmed.pkt > minmz_ref) & (peaklist_allrepl_sorted$mzmed.pkt < maxmz_ref)) + select_peaks <- peaklist_allrepl_sorted[select_peak_indices, ] + nrsamples <- length(select_peak_indices) + # put averaged intensities into a new row and append to averaged_peaks + averaged_1peak <- matrix(0, nrow = 1, ncol = 6) + colnames(averaged_1peak) <- c("samplenr", "mzmed.pkt", "fq", "mzmin.pkt", "mzmax.pkt", "height.pkt") + # calculate m/z values for peak group + averaged_1peak[1, "mzmed.pkt"] <- mean(select_peaks$mzmed.pkt) + averaged_1peak[1, "mzmin.pkt"] <- min(select_peaks$mzmed.pkt) + averaged_1peak[1, "mzmax.pkt"] <- max(select_peaks$mzmed.pkt) + averaged_1peak[1, "fq"] <- nrsamples + averaged_1peak[1, "height.pkt"] <- mean(select_peaks$height.pkt) + # put intensities into proper columns + peaklist_allrepl_sorted <- peaklist_allrepl_sorted[-select_peaks$rownr, ] + averaged_peaks <- rbind(averaged_peaks, averaged_1peak) } +# add sample name to first column +averaged_peaks[ , "samplenr"] <- sample_name +save(averaged_peaks, file = paste0("AvgPeaks_", sample_name, "_", scanmode, ".RData")) diff --git a/DIMS/AveragePeaks.nf b/DIMS/AveragePeaks.nf index 9d49f5f8..de4b21b0 100644 --- a/DIMS/AveragePeaks.nf +++ b/DIMS/AveragePeaks.nf @@ -6,13 +6,13 @@ process AveragePeaks { input: path(rdata_files) - path(replication_pattern) + tuple val(sample_id), val(tech_reps), val(scanmode) output: path 'AvgPeaks_*.RData' script: """ - Rscript ${baseDir}/CustomModules/DIMS/AveragePeaks.R + Rscript ${baseDir}/CustomModules/DIMS/AveragePeaks.R $sample_id $tech_reps $scanmode """ } From d0ed769ec7373ce3cd47243065e4743d99192de4 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 8 Jul 2025 15:56:15 +0200 Subject: [PATCH 048/161] modified input for PeakGrouping corresponding to new PeakFinding method --- DIMS/PeakGrouping.nf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DIMS/PeakGrouping.nf b/DIMS/PeakGrouping.nf index ae4382c7..0f20dc5b 100644 --- a/DIMS/PeakGrouping.nf +++ b/DIMS/PeakGrouping.nf @@ -6,7 +6,7 @@ process PeakGrouping { input: path(hmdbpart_file) - each path(spectrumpeak_file) + path(averagedpeaks_file) each path(pattern_file) output: From a35c4ca7decffc6e3379b44e220a0a47ec5392fb Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 8 Jul 2025 15:57:30 +0200 Subject: [PATCH 049/161] collect averaged peaks per biological sample, corresponding to new PeakFinding method --- DIMS/CollectAveraged.R | 18 ++++++++++++++++++ DIMS/CollectAveraged.nf | 17 +++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100755 DIMS/CollectAveraged.R create mode 100644 DIMS/CollectAveraged.nf diff --git a/DIMS/CollectAveraged.R b/DIMS/CollectAveraged.R new file mode 100755 index 00000000..e6466d9e --- /dev/null +++ b/DIMS/CollectAveraged.R @@ -0,0 +1,18 @@ +# define parameters +cmd_args <- commandArgs(trailingOnly = TRUE) + +scripts_dir <- cmd_args[1] + +# for each scan mode, collect all averaged peak lists per biological sample +scanmodes <- c("positive", "negative") +for (scanmode in scanmodes) { + # get list of files + filled_files <- list.files("./", full.names = TRUE, pattern = paste0(scanmode, ".RData")) + # load files and combine into one object + outlist_total <- NULL + for (file_nr in 1:length(filled_files)) { + peaklist_averaged <- get(load(filled_files[file_nr])) + outlist_total <- rbind(outlist_total, peaklist_averaged) + } + save(outlist_total, file = paste0("AvgPeaks_", scanmode, ".RData")) +} diff --git a/DIMS/CollectAveraged.nf b/DIMS/CollectAveraged.nf new file mode 100644 index 00000000..fc65bf21 --- /dev/null +++ b/DIMS/CollectAveraged.nf @@ -0,0 +1,17 @@ +process CollectAveraged { + tag "DIMS CollectAveraged" + label 'CollectAveraged' + container = 'docker://umcugenbioinf/dims:1.3' + shell = ['/bin/bash', '-euo', 'pipefail'] + + input: + path(averaged_files) + + output: + path('AvgPeaks*.RData'), emit: averaged_peaks + + script: + """ + Rscript ${baseDir}/CustomModules/DIMS/CollectAveraged.R + """ +} From 295e4609abf81ee7384fa50fb5a4f4d958ffcb26 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 15 Jul 2025 14:39:38 +0200 Subject: [PATCH 050/161] fixed path to DIMS peak_finding_functions --- DIMS/tests/testthat/test_peak_finding_functions.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DIMS/tests/testthat/test_peak_finding_functions.R b/DIMS/tests/testthat/test_peak_finding_functions.R index 1e001a0c..bc7f9d86 100644 --- a/DIMS/tests/testthat/test_peak_finding_functions.R +++ b/DIMS/tests/testthat/test_peak_finding_functions.R @@ -1,6 +1,6 @@ # unit tests for PeakFinding functions: -source("../preprocessing/peak_finding_functions.R") +source("../../preprocessing/peak_finding_functions.R") # test search_regions_of_interest function: test_that("regions of interest are correctly found for a single peak", { From 6f8fc0cb57b9399fd34e2f8267d5058f56397caf Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 15 Jul 2025 17:28:18 +0200 Subject: [PATCH 051/161] refactored HMDBparts, segmentation by mz rather than by nr of lines --- DIMS/HMDBparts.R | 100 +++++++++++++++-------------------------------- 1 file changed, 31 insertions(+), 69 deletions(-) diff --git a/DIMS/HMDBparts.R b/DIMS/HMDBparts.R index 232028e6..a7768c72 100755 --- a/DIMS/HMDBparts.R +++ b/DIMS/HMDBparts.R @@ -1,11 +1,9 @@ -# adapted from hmdb_parts.R - # define parameters cmd_args <- commandArgs(trailingOnly = TRUE) db_file <- cmd_args[1] breaks_file <- cmd_args[2] -standard_run <- cmd_args[4] +standard_run <- cmd_args[3] # load file with binning breaks load(breaks_file) @@ -15,8 +13,8 @@ max_mz <- round(breaks_fwhm[length(breaks_fwhm)]) # In case of a standard run use external HMDB parts # m/z is approximately 70 to 600: set limits between 68-71 for min and 599-610 for max if (standard_run == "yes" & min_mz > 68 & min_mz < 71 & max_mz > 599 & max_mz < 610) { - # skip generating HMDB parts - hmdb_parts_path <- cmd_args[3] + # skip generating HMDB parts; copy them from hmdb_parts_path + hmdb_parts_path <- cmd_args[4] # find all files containing hmdb in file name hmdb_parts <- list.files(hmdb_parts_path, pattern = "hmdb") for (hmdb_file in hmdb_parts) { @@ -25,8 +23,25 @@ if (standard_run == "yes" & min_mz > 68 & min_mz < 71 & max_mz > 599 & max_mz < } else { # generate HMDB parts in case of non-standard mz range load(db_file) - ppm <- as.numeric(cmd_args[5]) + # determine segments of m/z for HMDB parts; smaller parts for m/z < 100 + mz_segments <- c() + segment_start <- min_mz + segment_end <- min_mz + 5 + while (segment_end < max_mz) { + if (segment_start < 100) { + mz_segments <- c(mz_segments, segment_start) + segment_start <- segment_start + 5 + segment_end <- segment_end + 5 + } else { + mz_segments <- c(mz_segments, segment_start) + segment_start <- segment_start + 10 + segment_end <- segment_end + 10 + } + } + #last segment + mz_segments <- c(mz_segments, max_mz) + scanmodes <- c("positive", "negative") for (scanmode in scanmodes) { if (scanmode == "negative") { @@ -38,73 +53,20 @@ if (standard_run == "yes" & min_mz > 68 & min_mz < 71 & max_mz > 599 & max_mz < } # filter mass range meassured - hmdb_add_iso = hmdb_add_iso[which(hmdb_add_iso[ , column_label] >= breaks_fwhm[1] & - hmdb_add_iso[ , column_label] <= breaks_fwhm[length(breaks_fwhm)]), ] + hmdb_add_iso = hmdb_add_iso[which(hmdb_add_iso[ , column_label] >= min_mz & + hmdb_add_iso[ , column_label] <= max_mz), ] # sort on mass - outlist <- hmdb_add_iso[order(as.numeric(hmdb_add_iso[ , column_label])),] - nr_rows <- dim(outlist)[1] - - # maximum number of rows per file - sub <- 20000 - end <- 0 - last_line <- sub - check <- 0 - outlist_part <- NULL + sorted_hmdb_add_iso <- hmdb_add_iso[order(as.numeric(hmdb_add_iso[ , column_label])),] + nr_rows <- dim(sorted_hmdb_add_iso)[1] # create parts and save to file - if (nr_rows < sub) { - outlist_part <- outlist - save(outlist_part, file = paste0(scanmode, "_hmdb.1.RData")) - } else if (nr_rows >= sub & (floor(nr_rows / sub) - 1) >= 2) { - for (i in 2:floor(nr_rows / sub) - 1) { - start <- -(sub - 1) + i * sub - end <- i * sub - - if (i > 1){ - outlist_i = outlist[c(start:end),] - nr_moved = 0 - # Use ppm to replace border to avoid cut within peak group - while ((as.numeric(outlist_i[1, column_label]) - as.numeric(outlist_part[last_line, column_label])) * 1e+06 / - as.numeric(outlist_i[1, column_label]) < ppm) { - outlist_part <- rbind(outlist_part, outlist_i[1, ]) - outlist_i <- outlist_i[-1, ] - nr_moved <- nr_moved + 1 - } - - save(outlist_part, file = paste(scanmode, "_", paste("hmdb", i-1, "RData", sep = "."), sep = "")) - check <- check + dim(outlist_part)[1] - - outlist_part <- outlist_i - last_line <- dim(outlist_part)[1] - - } else { - outlist_part <- outlist[c(start:end),] - } - } - - start <- end + 1 - end <- nr_rows - outlist_i <- outlist[c(start:end), ] - nr_moved <- 0 - - if (!is.null(outlist_part)) { - # Calculate ppm and replace border, avoid cut within peak group - while ((as.numeric(outlist_i[1, column_label]) - as.numeric(outlist_part[last_line, column_label])) * 1e+06 / - as.numeric(outlist_i[1, column_label]) < ppm) { - outlist_part <- rbind(outlist_part, outlist_i[1, ]) - outlist_i <- outlist_i[-1, ] - nr_moved <- nr_moved + 1 - } - - save(outlist_part, file = paste0(scanmode, "_hmdb.", i, ".RData")) - check <- check + dim(outlist_part)[1] - } - - outlist_part <- outlist_i - save(outlist_part, file = paste0(scanmode, "_hmdb.", i + 1, ".RData")) - check <- check + dim(outlist_part)[1] + for (mz_part_index in 1:(length(mz_segments) - 1)) { + mz_start <- mz_segments[mz_part_index] + mz_end <- mz_segments[mz_part_index + 1] + outlist_part <- sorted_hmdb_add_iso[sorted_hmdb_add_iso[ , column_label] > mz_start & + sorted_hmdb_add_iso[ , column_label] <= mz_end, ] + save(outlist_part, file = paste0(scanmode, "_hmdb.", mz_part_index, ".RData")) } } } - From da004675870b7f30805629183459d1bcb9c58801 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 15 Jul 2025 17:29:08 +0200 Subject: [PATCH 052/161] obsolete parameter ppm removed --- DIMS/HMDBparts.nf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DIMS/HMDBparts.nf b/DIMS/HMDBparts.nf index 5f19f750..760b28d0 100644 --- a/DIMS/HMDBparts.nf +++ b/DIMS/HMDBparts.nf @@ -13,6 +13,6 @@ process HMDBparts { script: """ - Rscript ${baseDir}/CustomModules/DIMS/HMDBparts.R $hmdb_db_file $breaks_file $params.hmdb_parts_files $params.standard_run $params.ppm + Rscript ${baseDir}/CustomModules/DIMS/HMDBparts.R $hmdb_db_file $breaks_file $params.standard_run $params.hmdb_parts_files """ } From e8d0a7f1aea50bd06442ad0e579ead446f5a87d8 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 7 Aug 2025 17:00:53 +0200 Subject: [PATCH 053/161] Refactored code GenerateViolinPlots --- DIMS/GenerateViolinPlots.R | 649 +++++------------- DIMS/GenerateViolinPlots.nf | 5 +- DIMS/export/generate_violin_plots_functions.R | 572 +++++++++++++++ 3 files changed, 763 insertions(+), 463 deletions(-) create mode 100644 DIMS/export/generate_violin_plots_functions.R diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 263c7201..2e6cb885 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -1,14 +1,3 @@ -# For untargeted metabolomics, this tool calculates probability scores for -# metabolic disorders. In addition, it provides visual support with violin plots -# of the DIMS measurements for the lab specialists. -# Input needed: -# 1. Excel file in which metabolites are listed with their intensities for -# controls (with C in samplename) and patients (with P in samplename) and their -# corresponding Z-scores. -# 2. All files from github: https://github.com/UMCUGenetics/DIMS - -## adapted from 15-dIEM_violin.R - # load packages suppressPackageStartupMessages(library("dplyr")) library(reshape2) @@ -21,462 +10,202 @@ library(stringr) cmd_args <- commandArgs(trailingOnly = TRUE) run_name <- cmd_args[1] -scripts_dir <- cmd_args[2] -z_score <- as.numeric(cmd_args[3]) -path_metabolite_groups <- cmd_args[4] -file_ratios_metabolites <- cmd_args[5] -file_expected_biomarkers_iem <- cmd_args[6] -file_explanation <- cmd_args[7] -file_isomers <- cmd_args[8] - -if (z_score == 1){ - # path: output folder for dIEM and violin plots - output_dir <- "./" - - file.copy(file_isomers, output_dir) - - # load functions - source(paste0(scripts_dir, "check_same_samplename.R")) - source(paste0(scripts_dir, "prepare_data.R")) - source(paste0(scripts_dir, "prepare_data_perpage.R")) - source(paste0(scripts_dir, "prepare_toplist.R")) - source(paste0(scripts_dir, "create_violin_plots.R")) - source(paste0(scripts_dir, "prepare_alarmvalues.R")) - source(paste0(scripts_dir, "output_helix.R")) - source(paste0(scripts_dir, "get_patient_data_to_helix.R")) - source(paste0(scripts_dir, "add_lab_id_and_onderzoeksnummer.R")) - source(paste0(scripts_dir, "is_diagnostic_patient.R")) - - # number of diseases that score highest in algorithm to plot - top_nr_iem <- 5 - # probability score cut-off for plotting the top diseases - threshold_iem <- 5 - # z-score cutoff of axis on the left for top diseases - ratios_cutoff <- -5 - # number of violin plots per page in PDF - nr_plots_perpage <- 20 - - # binary variable: run function, yes(1) or no(0) - if (z_score == 1) { - algorithm <- ratios <- violin <- 1 - } else { - algorithm <- ratios <- violin <- 0 - } - # are the sample names headers on row 1 or row 2 in the DIMS excel? (default 1) - header_row <- 1 - # column name where the data starts (default B) - col_start <- "B" - zscore_cutoff <- 5 - xaxis_cutoff <- 20 - protocol_name <- "DIMS_PL_DIAG" - - #### STEP 1: Preparation #### - # in: run_name, path_dims_file, header_row ||| out: output_dir, DIMS - - # load outlist instead of excel file - load("outlist.RData") - - # save outlist as dims_xls, will be changed during refactor - dims_xls <- outlist - rm(outlist) - - #### STEP 2: Edit DIMS data ##### - # in: dims_xls ||| out: Data, nr_contr, nr_pat - # Input: the xlsx file that comes out of the pipeline with format: - # [plots] [C] [P] [summary columns] [C_Zscore] [P_Zscore] - # Output: "_CSV.csv" file that is suited for the algorithm in shiny. - - # Determine the number of Contols and Patients in column names: - nr_contr <- length(grep("C", names(dims_xls))) / 2 - nr_pat <- length(grep("P", names(dims_xls))) / 2 - # total number of samples - nrsamples <- nr_contr + nr_pat - # check whether the number of intensity columns equals the number of Zscore columns - if (nr_contr + nr_pat != length(grep("_Zscore", names(dims_xls)))) { - cat("\n**** Error: there aren't as many intensities listed as Zscores") - } - cat(paste0("\n\n------------\n", nr_contr, " controls \n", nr_pat, " patients\n------------\n\n")) - - # Move the columns HMDB_code and HMDB_name to the beginning. - hmdb_info_cols <- c(which(colnames(dims_xls) == "HMDB_code"), which(colnames(dims_xls) == "HMDB_name")) - other_cols <- seq_along(1:ncol(dims_xls))[-hmdb_info_cols] - dims_xls_copy <- dims_xls[, c(hmdb_info_cols, other_cols)] - # Remove the columns from 'name' to 'pathway' - from_col <- which(colnames(dims_xls_copy) == "name") - to_col <- which(colnames(dims_xls_copy) == "pathway") - dims_xls_copy <- dims_xls_copy[, -c(from_col:to_col)] - # in case the excel had an empty "plots" column, remove it - if ("plots" %in% colnames(dims_xls_copy)) { - dims_xls_copy <- dims_xls_copy[, -grep("plots", colnames(dims_xls_copy))] - } - # Rename columns - names(dims_xls_copy) <- gsub("avg.ctrls", "Mean_controls", names(dims_xls_copy)) - names(dims_xls_copy) <- gsub("sd.ctrls", "SD_controls", names(dims_xls_copy)) - names(dims_xls_copy) <- gsub("HMDB_code", "HMDB.code", names(dims_xls_copy)) - names(dims_xls_copy) <- gsub("HMDB_name", "HMDB.name", names(dims_xls_copy)) - - # intensity columns and mean and standard deviation of controls - numeric_cols <- c(3:ncol(dims_xls_copy)) - # make sure all values are numeric - dims_xls_copy[, numeric_cols] <- sapply(dims_xls_copy[, numeric_cols], as.numeric) - - if (exists("dims_xls_copy") & (length(dims_xls_copy) < length(dims_xls))) { - cat("\n### Step 2 # Edit dims data is done.\n") - } else { - cat("\n**** Error: Could not execute step 2 \n") - } - - #### STEP 3: Calculate ratios of intensities for metabolites #### - # in: ratios, file_ratios_metabolites, dims_xls_copy, nr_contr, nr_pat ||| out: Zscore (+file) - # This script loads the file with Ratios (file_ratios_metabolites) and calculates - # the ratios of the intensities of the given metabolites. It also calculates - # Zs-cores based on the avg and sd of the ratios of the controls. - - # Input: dataframe with intenstities and Zscores of controls and patients: - # [HMDB.code] [HMDB.name] [C] [P] [Mean_controls] [SD_controls] [C_Zscore] [P_Zscore] - - # Output: "_CSV.csv" file that is suited for the algorithm, with format: - # "_Ratios_CSV.csv" file, same file as above, but with ratio rows added. - - if (ratios == 1) { - cat(paste0("\nloading ratios file:\n -> ", file_ratios_metabolites, "\n")) - ratio_input <- read.csv(file_ratios_metabolites, sep = ";", stringsAsFactors = FALSE) - - # Prepare empty data frame to fill with ratios - ratio_list <- setNames(data.frame(matrix( - ncol = ncol(dims_xls_copy), - nrow = nrow(ratio_input) - )), colnames(dims_xls_copy)) - ratio_list <- as.data.frame(ratio_list) - - # put HMDB info into first two columns of ratio_list - ratio_list[, 1:2] <- ratio_input[, 1:2] - - # look for intensity columns (exclude Zscore columns) - control_cols <- grep("C", colnames(ratio_list)[1:which(colnames(ratio_list) == "Mean_controls")]) - patient_cols <- grep("P", colnames(ratio_list)[1:which(colnames(ratio_list) == "Mean_controls")]) - intensity_cols <- c(control_cols, patient_cols) - # calculate each of the ratios of intensities - for (ratio_index in 1:nrow(ratio_input)) { - ratio_numerator <- ratio_input[ratio_index, "HMDB_numerator"] - ratio_numerator <- strsplit(ratio_numerator, "plus")[[1]] - ratio_denominator <- ratio_input[ratio_index, "HMDB_denominator"] - ratio_denominator <- strsplit(ratio_denominator, "plus")[[1]] - # find these HMDB IDs in dataset. Could be a sum of multiple metabolites - sel_denominator <- sel_numerator <- c() - for (numerator_index in 1:length(ratio_numerator)) { - sel_numerator <- c(sel_numerator, which(dims_xls_copy[, "HMDB.code"] == ratio_numerator[numerator_index])) - } - for (denominator_index in 1:length(ratio_denominator)) { - # special case for sum of metabolites (dividing by one) - if (ratio_denominator[denominator_index] != "one") { - sel_denominator <- c(sel_denominator, which(dims_xls_copy[, "HMDB.code"] == ratio_denominator[denominator_index])) - } - } - # calculate ratio - if (ratio_denominator[denominator_index] != "one") { - ratio_list[ratio_index, intensity_cols] <- apply(dims_xls_copy[sel_numerator, intensity_cols], 2, sum) / - apply(dims_xls_copy[sel_denominator, intensity_cols], 2, sum) - } else { - # special case for sum of metabolites (dividing by one) - ratio_list[ratio_index, intensity_cols] <- apply(dims_xls_copy[sel_numerator, intensity_cols], 2, sum) - } - # calculate log of ratio - ratio_list[ratio_index, intensity_cols] <- log2(ratio_list[ratio_index, intensity_cols]) - } - - # Calculate means and SD's of the calculated ratios for Controls - ratio_list[, "Mean_controls"] <- apply(ratio_list[, control_cols], 1, mean) - ratio_list[, "SD_controls"] <- apply(ratio_list[, control_cols], 1, sd) - - # Calc z-scores with the means and SD's of Controls - zscore_cols <- grep("Zscore", colnames(ratio_list)) - for (sample_index in 1:length(zscore_cols)) { - zscore_col <- zscore_cols[sample_index] - # matching intensity column - int_col <- intensity_cols[sample_index] - # test on column names - if (check_same_samplename(colnames(ratio_list)[int_col], colnames(ratio_list)[zscore_col])) { - # calculate Z-scores - ratio_list[, zscore_col] <- (ratio_list[, int_col] - ratio_list[, "Mean_controls"]) / ratio_list[, "SD_controls"] - } +export_scripts_dir <- cmd_args[2] +path_metabolite_groups <- cmd_args[3] +file_ratios_metabolites <- cmd_args[4] +file_expected_biomarkers_iem <- cmd_args[5] +file_explanation <- cmd_args[6] + +# load functions +source(paste0(export_scripts_dir, "generate_violin_plots_functions.R")) +# load dataframe with intensities and Z-scores for all samples +intensities_zscore_df <- get(load("outlist.RData")) +# read input files +ratios_metabs_df <- read.csv(file_ratios_metabolites, sep = ";", stringsAsFactors = FALSE) +expected_biomarkers_df <- read.csv(file_expected_biomarkers_iem, sep = ";", stringsAsFactors = FALSE) +explanation_violin_plot <- readLines(file_explanation) + + +## Set global variables +output_dir <- "./" # path: output folder for dIEM and violin plots +top_number_iem_diseases <- 5 # number of diseases that score highest in algorithm to plot +threshold_iem <- 5 # probability score cut-off for plotting the top diseases +ratios_cutoff <- -5 # z-score cutoff of axis on the left for top diseases +nr_plots_perpage <- 20 # number of violin plots per page in PDF +zscore_cutoff <- 5 +xaxis_cutoff <- 20 +protocol_name <- "DIMS_PL_DIAG" + +# Remove columns, move HMDB_code & HMDB_name column to the front, change intensity columns to numeric +intensities_zscore_df <- intensities_zscore_df %>% + select(-c(plots, HMDB_name_all, HMDB_ID_all, sec_HMDB_ID, HMDB_key, sec_HMBD_ID_rlvnc, name, + relevance, descr, origin, fluids, tissue, disease, pathway, nr_ctrls)) %>% + relocate(c(HMDB_code, HMDB_name)) %>% + rename(mean_controls = avg_ctrls, sd_controls = sd_ctrls) %>% + mutate(across(!c(HMDB_name, HMDB_code), as.numeric)) + +# Get the controls and patient IDs, select the intensity columns +controls <- colnames(intensities_zscore_df)[grepl("^C", colnames(intensities_zscore_df)) & + !grepl("_Zscore$", colnames(intensities_zscore_df))] +control_intensities_cols_index <- which(colnames(intensities_zscore_df) %in% controls) +nr_of_controls <- length(controls) + +patients <- colnames(intensities_zscore_df)[grepl("^P", colnames(intensities_zscore_df)) & + !grepl("_Zscore$", colnames(intensities_zscore_df))] +patient_intensities_cols_index <- which(colnames(intensities_zscore_df) %in% patients) +nr_of_patients <- length(patients) + +intensity_cols_index <- c(control_intensities_cols_index, patient_intensities_cols_index) +intensity_cols <- colnames(intensities_zscore_df)[intensity_cols_index] + +#### Calculate ratios of intensities for metabolites #### +# Prepare empty data frame to fill with ratios +ratio_zscore_df <- data.frame(matrix( + ncol = ncol(intensities_zscore_df), + nrow = nrow(ratios_metabs_df) +)) +colnames(ratio_zscore_df) <- colnames(intensities_zscore_df) + +# put HMDB info into first two columns of ratio_zscore_df +ratio_zscore_df$HMDB_code <- ratios_metabs_df$HMDB.code +ratio_zscore_df$HMDB_name <- ratios_metabs_df$Ratio_name + +for (row_index in 1:nrow(ratios_metabs_df)) { + numerator_intensities <- get_intentities_for_ratios(ratios_metabs_df, row_index, + intensities_zscore_df, "HMDB_numerator", intensity_cols) + denominator_intensities <- get_intentities_for_ratios(ratios_metabs_df, row_index, + intensities_zscore_df, "HMDB_denominator", intensity_cols) + # calculate intensity ratios + ratio_zscore_df[row_index, intensity_cols_index] <- log2(numerator_intensities / denominator_intensities) +} +# Calculate means and SD's of the calculated ratios for Controls +ratio_zscore_df[, "mean_controls"] <- apply(ratio_zscore_df[, control_intensities_cols_index], 1, mean) +ratio_zscore_df[, "sd_controls"] <- apply(ratio_zscore_df[, control_intensities_cols_index], 1, sd) + +# Calculate Zscores for the ratios +samples_zscore_columns <- get_zscore_columns(colnames(intensities_zscore_df), intensity_cols) +ratio_zscore_df[, samples_zscore_columns] <- (ratio_zscore_df[, intensity_cols] - ratio_zscore_df[, "mean_controls"]) / + ratio_zscore_df[, "sd_controls"] + +intensities_zscore_ratios_df <- rbind(intensities_zscore_df, ratio_zscore_df) + +# for debugging: +save(intensities_zscore_ratios_df, file = paste0(output_dir, "/outlist_with_ratios.RData")) + +# Select only the cols with zscores of the patients +zscore_patients_df <- intensities_zscore_ratios_df %>% select(HMDB_code, HMDB_name, any_of(paste0(patients, "_Zscore"))) +zscore_controls_df <- intensities_zscore_ratios_df %>% select(HMDB_code, HMDB_name, any_of(paste0(controls, "_Zscore"))) + +#### Make violin plots ##### +# preparation +colnames(zscore_patients_df) <- gsub("_Zscore", "", colnames(zscore_patients_df)) +colnames(zscore_controls_df) <- gsub("_Zscore", "", colnames(zscore_controls_df)) + +expected_biomarkers_df <- expected_biomarkers_df %>% rename(HMDB_code = HMDB.code, HMDB_name = Metabolite) + +expected_biomarkers_info <- expected_biomarkers_df %>% + select(c(Disease, HMDB_code, HMDB_name)) %>% + distinct(Disease, HMDB_code, .keep_all = TRUE) + +metabolite_dirs <- list.files(path = path_metabolite_groups, full.names = FALSE, recursive = FALSE) +for (metabolite_dir in metabolite_dirs) { + # create a directory for the output PDFs + pdf_dir <- paste(output_dir, metabolite_dir, sep = "/") + dir.create(pdf_dir, showWarnings = FALSE) + + metab_list_all <- get_list_metabolites(paste(path_metabolite_groups, metabolite_dir, sep = "/")) + + # prepare list of metabolites; max nr_plots_perpage on one page + metab_interest_sorted <- combine_metab_info_zscores(metab_list_all, zscore_patients_df) + metab_interest_controls <- combine_metab_info_zscores(metab_list_all, zscore_controls_df) + metab_perpage <- prepare_data_perpage(metab_interest_sorted, metab_interest_controls, + nr_plots_perpage, nr_of_patients, nr_of_controls) + + # for Diagnostics metabolites to be saved in Helix + if(grepl("Diagnost", pdf_dir)) { + # get table that combines DIMS results with stofgroepen/Helix table + dims_helix_table <- get_patient_data_to_helix(metab_interest_sorted, metab_list_all) + + # check if run contains Diagnostics patients (e.g. "P2024M"), not for research runs + if(any(is_diagnostic_patient(dims_helix_table$Sample))){ + # get output file for Helix + output_helix <- output_for_helix(protocol_name, dims_helix_table) + # write output to file + path_helixfile <- paste0(output_dir, "output_Helix_", run_name,".csv") + write.csv(output_helix, path_helixfile, quote = F, row.names = F) } - - # Add rows of the ratio hmdb codes to the data of zscores from the pipeline. - dims_xls_ratios <- rbind(ratio_list, dims_xls_copy) - - # Edit the DIMS output Zscores of all patients in format: - # HMDB_code patientname1 patientname2 - names(dims_xls_ratios) <- gsub("HMDB.code", "HMDB_code", names(dims_xls_ratios)) - names(dims_xls_ratios) <- gsub("HMDB.name", "HMDB_name", names(dims_xls_ratios)) - - # for debugging: - write.table(dims_xls_ratios, file = paste0(output_dir, "/ratios.txt"), sep = "\t") - - # Select only the cols with zscores of the patients - zscore_patients <- dims_xls_ratios[, c(1, 2, zscore_cols[grep("P", colnames(dims_xls_ratios)[zscore_cols])])] - # Select only the cols with zscores of the controls - zscore_controls <- dims_xls_ratios[, c(1, 2, zscore_cols[grep("C", colnames(dims_xls_ratios)[zscore_cols])])] - } - - #### STEP 4: Run the IEM algorithm ######### - # in: algorithm, file_expected_biomarkers_iem, zscore_patients ||| out: prob_score (+file) - # algorithm taken from DOI: 10.3390/ijms21030979 - - if (algorithm == 1) { - # Load data - cat(paste0("\nloading expected file:\n -> ", file_expected_biomarkers_iem, "\n")) - expected_biomarkers <- read.csv(file_expected_biomarkers_iem, sep = ";", stringsAsFactors = FALSE) - # modify column names - names(expected_biomarkers) <- gsub("HMDB.code", "HMDB_code", names(expected_biomarkers)) - names(expected_biomarkers) <- gsub("Metabolite", "HMDB_name", names(expected_biomarkers)) - - # prepare dataframe scaffold rank_patients - rank_patients <- zscore_patients - # Fill df rank_patients with the ranks for each patient - for (patient_index in 3:ncol(zscore_patients)) { - # number of positive zscores in patient - pos <- sum(zscore_patients[, patient_index] > 0) - # sort the column on zscore; NB: this sorts the entire object, not just one column - rank_patients <- rank_patients[order(-rank_patients[patient_index]), ] - # Rank all positive zscores highest to lowest - rank_patients[1:pos, patient_index] <- as.numeric(ordered(-rank_patients[1:pos, patient_index])) - # Rank all negative zscores lowest to highest - rank_patients[(pos + 1):nrow(rank_patients), patient_index] <- as.numeric(ordered(rank_patients[(pos + 1): - nrow(rank_patients), patient_index])) - } - - # Calculate metabolite score, using the dataframes with only values, and later add the cols without values (1&2). - expected_zscores <- merge(x = expected_biomarkers, y = zscore_patients, by.x = c("HMDB_code"), by.y = c("HMDB_code")) - expected_zscores_original <- expected_zscores - - # determine which columns contain Z-scores and which contain disease info - select_zscore_cols <- grep("_Zscore", colnames(expected_zscores)) - select_info_cols <- 1:(min(select_zscore_cols) - 1) - # set some zscores to zero - select_incr_indisp <- which(expected_zscores$Change == "Increase" & expected_zscores$Dispensability == "Indispensable") - expected_zscores[select_incr_indisp, select_zscore_cols] <- lapply(expected_zscores[select_incr_indisp, - select_zscore_cols], function(x) ifelse (x <= 1.6, 0, x)) - select_decr_indisp <- which(expected_zscores$Change == "Decrease" & expected_zscores$Dispensability == "Indispensable") - expected_zscores[select_decr_indisp, select_zscore_cols] <- lapply(expected_zscores[select_decr_indisp, - select_zscore_cols], function(x) ifelse (x >= -1.2, 0, x)) - - # calculate rank score: - expected_ranks <- merge(x = expected_biomarkers, y = rank_patients, by.x = c("HMDB_code"), by.y = c("HMDB_code")) - rank_scores <- expected_zscores[order(expected_zscores$HMDB_code), select_zscore_cols] / - (expected_ranks[order(expected_ranks$HMDB_code), select_zscore_cols] * 0.9) - # combine disease info with rank scores - expected_metabscore <- cbind(expected_ranks[order(expected_zscores$HMDB_code), select_info_cols], rank_scores) - - # multiply weight score and rank score - weight_score <- expected_zscores - weight_score[, select_zscore_cols] <- expected_metabscore$Total_Weight * expected_metabscore[, select_zscore_cols] - - # sort table on Disease and Absolute_Weight - weight_score <- weight_score[order(weight_score$Disease, weight_score$Absolute_Weight, decreasing = TRUE), ] - - # select columns to check duplicates - dup <- weight_score[, c("Disease", "M.z")] - uni <- weight_score[!duplicated(dup) | !duplicated(dup, fromLast = FALSE), ] - - # calculate probability score - prob_score <- aggregate(uni[, select_zscore_cols], uni["Disease"], sum) - - # list of all diseases that have at least one metabolite Zscore at 0 - for (patient_index in 2:ncol(prob_score)) { - patient_zscore_colname <- colnames(prob_score)[patient_index] - matching_colname_expected <- which(colnames(expected_zscores) == patient_zscore_colname) - # determine which Zscores are 0 for this patient - zscores_zero <- which(expected_zscores[, matching_colname_expected] == 0) - # get Disease for these - disease_zero <- unique(expected_zscores[zscores_zero, "Disease"]) - # set the probability score of these diseases to 0 - prob_score[which(prob_score$Disease %in% disease_zero), patient_index] <- 0 - } - - # determine disease rank per patient - disease_rank <- prob_score - # rank diseases in decreasing order - disease_rank[2:ncol(disease_rank)] <- lapply(2:ncol(disease_rank), function(x) - as.numeric(ordered(-disease_rank[1:nrow(disease_rank), x]))) - # modify column names, Zscores have now been converted to probability scores - colnames(prob_score) <- gsub("_Zscore", "_prob_score", colnames(prob_score)) - colnames(disease_rank) <- gsub("_Zscore", "", colnames(disease_rank)) - - # Create conditional formatting for output Excel sheet. Colors according to values. - wb <- createWorkbook() - addWorksheet(wb, "Probability Scores") - writeData(wb, "Probability Scores", prob_score) - conditionalFormatting(wb, "Probability Scores", cols = 2:ncol(prob_score), rows = 1:nrow(prob_score), - type = "colourScale", style = c("white", "#FFFDA2", "red"), rule = c(1, 10, 100)) - saveWorkbook(wb, file = paste0(output_dir, "/dIEM_algoritme_output_", run_name, ".xlsx"), overwrite = TRUE) - # check whether prob_score df exists and has expected dimensions. - if (exists("expected_biomarkers") & (length(disease_rank) == length(prob_score))) { - cat("\n### Step 4 # Running the IEM algorithm is done.\n\n") + + # make violin plots per patient + for (patient_id in patients) { + # for category Diagnostics, make list of metabolites that exceed alarm values for this patient + # for category Other, make list of top highest and lowest Z-scores for this patient + if (grepl("Diagnost", pdf_dir)) { + top_metabs_patient <- prepare_alarmvalues(patient_id, dims_helix_table) } else { - cat("\n**** Error: Could not run IEM algorithm. Check if path to expected_biomarkers csv-file is correct. \n") + top_metabs_patient <- prepare_toplist(patient_id, zscore_patients) } - rm(wb) + # generate normal violin plots + create_pdf_violin_plots(pdf_dir, patient_id, metab_perpage, top_metabs_patient, explanation_violin_plot) } - #### STEP 5: Make violin plots ##### - # in: algorithm / zscore_patients, violin, nr_contr, nr_pat, Data, path_textfiles, zscore_cutoff, xaxis_cutoff, - # top_diseases, top_metab, output_dir ||| out: pdf file, Helix csv file - - if (violin == 1) { - - # preparation - zscore_patients_copy <- zscore_patients - colnames(zscore_patients) <- gsub("_Zscore", "", colnames(zscore_patients)) - colnames(zscore_controls) <- gsub("_Zscore", "", colnames(zscore_controls)) - - # Make patient list for violin plots - patient_list <- names(zscore_patients)[-c(1, 2)] - - # from table expected_biomarkers, choose selected columns - select_columns <- c("Disease", "HMDB_code", "HMDB_name") - #select_col_nrs <- which(colnames(expected_biomarkers) %in% select_columns) - expected_biomarkers_select <- expected_biomarkers %>% select(all_of(select_columns)) - # remove duplicates - expected_biomarkers_select <- expected_biomarkers_select[!duplicated(expected_biomarkers_select[, c(1, 2)]), ] - - # load file with explanatory information to be included in PDF. - explanation <- readLines(file_explanation) - - # first step: normal violin plots - # Find all text files in the given folder, which contain metabolite lists of which - # each file will be a page in the pdf with violin plots. - # Make a PDF file for each of the categories in metabolite_dirs - metabolite_dirs <- list.files(path = path_metabolite_groups, full.names = FALSE, recursive = FALSE) - for (metabolite_dir in metabolite_dirs) { - # create a directory for the output PDFs - pdf_dir <- paste(output_dir, metabolite_dir, sep = "/") - dir.create(pdf_dir, showWarnings = FALSE) - cat("making plots in category:", metabolite_dir, "\n") - - # get a list of all metabolite files - metabolite_files <- list.files(path = paste(path_metabolite_groups, metabolite_dir, sep = "/"), - pattern = "*.txt", full.names = FALSE, recursive = FALSE) - # put all metabolites into one list - metab_list_all <- list() - metab_list_names <- c() - cat("making plots from the input files:") - # open the text files and add each to a list of dataframes (metab_list_all) - for (file_index in seq_along(metabolite_files)) { - infile <- metabolite_files[file_index] - metab_list <- read.table(paste(path_metabolite_groups, metabolite_dir, infile, sep = "/"), - sep = "\t", header = TRUE, quote = "") - # put into list of all lists - metab_list_all[[file_index]] <- metab_list - metab_list_names <- c(metab_list_names, strsplit(infile, ".txt")[[1]][1]) - cat(paste0("\n", infile)) - } - # include list of classes in metabolite list - names(metab_list_all) <- metab_list_names - - # prepare list of metabolites; max nr_plots_perpage on one page - metab_interest_sorted <- prepare_data(metab_list_all, zscore_patients) - metab_interest_controls <- prepare_data(metab_list_all, zscore_controls) - metab_perpage <- prepare_data_perpage(metab_interest_sorted, metab_interest_controls, nr_plots_perpage, nr_pat, nr_contr) - - # for Diagnostics metabolites to be saved in Helix - if(grepl("Diagnost", pdf_dir)) { - # get table that combines DIMS results with stofgroepen/Helix table - dims_helix_table <- get_patient_data_to_helix(metab_interest_sorted, metab_list_all) - - # check if run contains Diagnostics patients (e.g. "P2024M"), not for research runs - if(any(is_diagnostic_patient(dims_helix_table$Patient))){ - # get output file for Helix - output_helix <- output_for_helix(protocol_name, dims_helix_table) - # write output to file - path_helixfile <- paste0(output_dir, "/output_Helix_", run_name,".csv") - write.csv(output_helix, path_helixfile, quote = F, row.names = F) - } - } - - # make violin plots per patient - for (pt_nr in 1:length(patient_list)) { - pt_name <- patient_list[pt_nr] - # for category Diagnostics, make list of metabolites that exceed alarm values for this patient - # for category Other, make list of top highest and lowest Z-scores for this patient - if (grepl("Diagnost", pdf_dir)) { - top_metab_pt <- prepare_alarmvalues(pt_name, dims_helix_table) - } else { - top_metab_pt <- prepare_toplist(pt_name, zscore_patients) - } - - # generate normal violin plots - create_violin_plots(pdf_dir, pt_name, metab_perpage, top_metab_pt) - - } - - } - - # Second step: dIEM plots in separate directory - diem_plot_dir <- paste(output_dir, "dIEM_plots", sep = "/") - dir.create(diem_plot_dir) - - # Select the metabolites that are associated with the top highest scoring IEM, for each patient - # disease_rank is from step 4: the dIEM algorithm. The lower the value, the more likely. - for (pt_nr in 1:length(patient_list)) { - pt_name <- patient_list[pt_nr] - # get top diseases for this patient - pt_colnr <- which(colnames(disease_rank) == pt_name) - pt_top_indices <- which(disease_rank[, pt_colnr] <= top_nr_iem) - pt_iems <- disease_rank[pt_top_indices, "Disease"] - pt_top_iems <- pt_prob_score_top_iems <- c() - for (single_iem in pt_iems) { - # get the probability score - prob_score_iem <- prob_score[which(prob_score$Disease == single_iem), pt_colnr] - # use only diseases for which probability score is above threshold - if (prob_score_iem >= threshold_iem) { - pt_top_iems <- c(pt_top_iems, single_iem) - pt_prob_score_top_iems <- c(pt_prob_score_top_iems, prob_score_iem) - } - } - - # prepare data for plotting dIEM violin plots - # If prob_score_top_iem is an empty list, don't make a plot - if (length(pt_top_iems) > 0) { - # Sorting from high to low, both prob_score_top_iems and pt_top_iems. - pt_prob_score_order <- order(-pt_prob_score_top_iems) - pt_prob_score_top_iems <- round(pt_prob_score_top_iems, 1) - pt_prob_score_top_iem_sorted <- pt_prob_score_top_iems[pt_prob_score_order] - pt_top_iem_sorted <- pt_top_iems[pt_prob_score_order] - # getting metabolites for each top_iem disease exactly like in metab_list_all - metab_iem_all <- list() - metab_iem_names <- c() - for (single_iem_index in 1:length(pt_top_iem_sorted)) { - single_iem <- pt_top_iem_sorted[single_iem_index] - single_prob_score <- pt_prob_score_top_iem_sorted[single_iem_index] - select_rows <- which(expected_biomarkers_select$Disease == single_iem) - metab_list <- expected_biomarkers_select[select_rows, ] - metab_iem_names <- c(metab_iem_names, paste0(single_iem, ", probability score ", single_prob_score)) - metab_list <- metab_list[, -1] - metab_iem_all[[single_iem_index]] <- metab_list - } - # put all metabolites into one list - names(metab_iem_all) <- metab_iem_names - - # get Zscore information from zscore_patients_copy, similar to normal violin plots - metab_iem_sorted <- prepare_data(metab_iem_all, zscore_patients_copy) - metab_iem_controls <- prepare_data(metab_iem_all, zscore_controls) - # make sure every page has 20 metabolites - diem_metab_perpage <- prepare_data_perpage(metab_iem_sorted, metab_iem_controls, nr_plots_perpage, nr_pat) - # add table of metabolites with increased or decreased Z-scores - top_metab_pt <- prepare_toplist(pt_name, zscore_patients) - - # generate dIEM violin plots - create_violin_plots(diem_plot_dir, pt_name, diem_metab_perpage, top_metab_pt) - - } else { - cat(paste0("\n\n**** This patient had no prob_scores higher than ", threshold_iem, ". - Therefore, this pdf was not made:\t ", pt_name, "_iem \n")) - } - - } +} +#### Run the IEM algorithm ######### +expected_biomarkers_df <- expected_biomarkers_df %>% rename(HMDB_code = HMDB.code, HMDB_name = Metabolite) + +diem_probability_score <- run_diem_algorithm(expected_biomarkers_df, zscore_patients_df, patients) + +save_prob_scores_to_Excel(diem_probability_score, output_dir, run_name) + + +#### Generate dIEM plots ######### +diem_plot_dir <- paste(output_dir, "dIEM_plots", sep = "/") +dir.create(diem_plot_dir) + +colnames(diem_probability_score) <- gsub("_Zscore", "", colnames(diem_probability_score)) +patient_no_iem <- c() + +for (patient_id in patients) { + # Select the top IEMs and filter on the IEM threshold + patient_top_iems_probs <- diem_probability_score %>% + select(c(Disease, !!sym(patient_id))) %>% + arrange(desc(!!sym(patient_id))) %>% + slice(1:top_number_iem_diseases) %>% + filter(!!sym(patient_id) >= threshold_iem) + + if (nrow(patient_top_iems_probs) > 0) { + top_iems <- patient_top_iems_probs %>% pull(Disease) + # Get the metabolites for each IEM and their probability + metabs_iems_names <- c() + metabs_iems <- lapply(top_iems, function(iem) { + iem_probablity <- patient_top_iems_probs %>% filter(Disease == iem) %>% pull(!!sym(patient_id)) + metabs_iems_names <- c(metabs_iems_names, paste0(iem, ", probability score ", iem_probablity)) + metab_iem <- expected_biomarkers_df %>% filter(Disease == iem) %>% select(HMDB_code, HMDB_name) + return(metab_iem) + }) + names(metabs_iems) <- metabs_iems_names + + # Get the Z-scores with metabolite information + metab_iem_sorted <- combine_metab_info_zscores(metabs_iems, zscore_patients_df) + metab_iem_controls <- combine_metab_info_zscores(metabs_iems, zscore_controls_df) + # Get a list of dataframes for each IEM + diem_metab_perpage <- prepare_data_perpage(metab_iem_sorted, metab_iem_controls, + nr_plots_perpage, nr_of_patients, nr_of_controls) + # Get a dataframe of the top metabolites + top_metabs_patient <- prepare_toplist(patient_id, zscore_patients_df) + + # Generate and save dIEM violin plots + create_pdf_violin_plots(diem_plot_dir, patient_id, diem_metab_perpage, top_metabs_patient, explanation_violin_plot) + + } else { + patient_no_iem <- c(patient_no_iem, patient_id) } } + +if (length(patient_no_iem) > 0) { + patient_no_iem <- c(paste0("The following patient(s) did not have dIEM probability scores higher than ", threshold_iem, " :"), + patient_no_iem) + write(file = paste0(output_dir, "missing_probability_scores.txt"), patient_no_iem) +} diff --git a/DIMS/GenerateViolinPlots.nf b/DIMS/GenerateViolinPlots.nf index 1c4b532d..ec65a2e9 100755 --- a/DIMS/GenerateViolinPlots.nf +++ b/DIMS/GenerateViolinPlots.nf @@ -18,11 +18,10 @@ process GenerateViolinPlots { script: """ - Rscript ${baseDir}/CustomModules/DIMS/GenerateViolinPlots.R $analysis_id $params.scripts_dir $params.zscore \ + Rscript ${baseDir}/CustomModules/DIMS/GenerateViolinPlots.R $analysis_id $params.export_scripts_dir \ $params.path_metabolite_groups \ $params.file_ratios_metabolites \ $params.file_expected_biomarkers_IEM \ - $params.file_explanation \ - $params.file_isomers + $params.file_explanation """ } diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R new file mode 100644 index 00000000..b1855a7f --- /dev/null +++ b/DIMS/export/generate_violin_plots_functions.R @@ -0,0 +1,572 @@ +#' Getting the intensities for calculating ratio Z-scores +#' +#' @param ratios_metabs_df: dataframe with HMDB codes for the ratios (dataframe) +#' @param row_index: index of the row in the ratios_metabs_df (integer) +#' @param intensities_zscore_df: dataframe with intensities for each sample (dataframe) +#' @param fraction_side: either numerator or denominator, which side of the fraction (string) +#' @param intensity_cols: names of the columns that contain the intensities (string) +#' +#' @returns fraction_side_intensity: a vector of intensities (vector of integers) +get_intentities_for_ratios <- function(ratios_metabs_df, row_index, intensities_zscore_df, fraction_side, intensity_cols) { + fraction_side_hmdb_ids <- ratios_metabs_df[row_index, fraction_side] + if (grepl("plus", fraction_side_hmdb_ids)) { + fraction_side_hmdb_id_list <- strsplit(fraction_side_hmdb_ids, "plus")[[1]] + fraction_side_intensity_list <- intensities_zscore_df %>% filter(HMDB_code %in% fraction_side_hmdb_id_list) %>% + select(any_of(intensity_cols)) + fraction_side_intensity <- apply(fraction_side_intensity_list, 2, sum) + } else if(fraction_side_hmdb_ids == "one") { + fraction_side_intensity <- 1 + } else { + fraction_side_intensity <- intensities_zscore_df %>% filter(HMDB_code == fraction_side_hmdb_ids) %>% + select(any_of(intensity_cols)) + } + return(fraction_side_intensity) +} + +#' Get the sample IDs for columns that have Z-score and intensities +#' +#' @param colnames_zscore: vector of sample IDs from the dataframe containing Z-scores (vector of strings) +#' @param intensity_cols: vector of sample IDs form the dataframe containing intensities (vector of strings) +#' +#' @returns: vector of sample IDs that are in both input vectors (vector of strings) +get_zscore_columns <- function(colnames_zscore, intensity_cols) { + intersect(paste0(intensity_cols, "_Zscore"), grep("_Zscore", colnames_zscore, value = T)) +} + +#' Get a list with dataframes for all off the metabolite group in a directory +#' +#' @param metab_group_dir: directory containing txt files with metabolites per group (string) +#' +#' @returns: list with dataframes with info on metabolites (list of dataframes) +get_list_metabolites <- function(metab_group_dir) { + # get a list of all metabolite files + metabolite_files <- list.files(metab_group_dir, pattern = "*.txt", full.names = FALSE, recursive = FALSE) + # put all metabolites into one list + metab_list_all <- lapply(paste(metab_group_dir, metabolite_files, sep = "/"), + read.table, sep = "\t", header = TRUE, quote = "") + names(metab_list_all) <- gsub(".txt", "", metabolite_files) + + return(metab_list_all) +} + +#' Combine patient Z-scores with metabolite info +#' +#' @param metab_list_all: list of dataframes with metabolite information for different stofgroepen (list) +#' @param zscore_df: dataframe with metabolite Z-scores for all patient +#' +#' @return: list of dataframes for each stofgroep with data for each metabolite and patient/control per row +combine_metab_info_zscores <- function(metab_list_all, zscore_df) { + # remove HMDB_name column and "_Zscore" from column (patient) names + zscore_df <- zscore_df %>% select(-HMDB_name) %>% + rename_with(~ str_remove(.x, "_Zscore"), .cols = contains("_Zscore")) + + # put data into pages, max 20 violin plots per page in PDF + metab_interest_sorted <- list() + + for (metab_class in names(metab_list_all)) { + metab_df <- metab_list_all[[metab_class]] + # Select HMDB_code and HMDB_name columns + metab_df <- metab_df %>% select(HMDB_code, HMDB_name) + + # Change the HMDB_name column so all names have 45 characters + metab_df <- metab_df %>% mutate(HMDB_name = case_when( + str_length(HMDB_name) > 45 ~ str_c(str_sub(HMDB_name, 1, 42), "..."), + str_length(HMDB_name) < 45 ~ str_pad(HMDB_name, 45, side = "right", pad = " "), + TRUE ~ HMDB_name + )) + + # Join metabolite info with the Z-score dataframe + metab_interest <- metab_df %>% inner_join(zscore_df, by = "HMDB_code") %>% select(-HMDB_code) + + # put the data frame in long format + metab_interest_melt <- reshape2::melt(metab_interest, id.vars = "HMDB_name", variable.name = "Sample", + value.name = "Z_score") + # Add the dataframe sorted on HMDB_name to a list + metab_interest_sorted[[metab_class]] <- metab_interest_melt + } + + return(metab_interest_sorted) +} + +#' Combine patient and control data for each page of the violinplot pdf +#' +#' @param metab_interest_sorted: list of dataframes with data for each metabolite and patient (list) +#' @param metab_interest_contr: list of dataframes with data for each metabolite and control (list) +#' @param nr_plots_perpage: number of plots per page in the violinplot pdf (integer) +#' @param nr_pat: number of patients (integer) +#' @param nr_contr: number of controls (integer) +#' +#' @return: list of dataframes with metabolite Z-scores for each patient and control, +#' the length of list is the number of pages for the violinplot pdf (list) +prepare_data_perpage <- function(metab_interest_sorted, metab_interest_contr, nr_plots_perpage, nr_pat, nr_contr) { + metab_perpage <- list() + metab_category <- c() + + for (metab_class in names(metab_interest_sorted)) { + # Get the data for patients and controls for the metab_interest_sorted list + metab_sort_patients_df <- metab_interest_sorted[[metab_class]] + metab_sort_controls_df <- metab_interest_contr[[metab_class]] + + # Calculate the number of pages + nr_pages <- ceiling(length(unique(metab_sort_patients_df$HMDB_name)) / nr_plots_perpage) + + # Get all metabolites and create list with HMDB naames of max nr_plots_perpage long + metabolites <- unique(metab_sort_patients_df$HMDB_name) + metabolites_in_chunks <- split(metabolites, ceiling(seq_along(metabolites) / nr_plots_perpage)) + nr_chunks <- length(metabolites_in_chunks) + + current_perpage <- lapply(metabolites_in_chunks, function(metab_name) { + patients_df <- metab_sort_patients_df %>% filter(HMDB_name %in% metab_name) + controls_df <- metab_sort_controls_df %>% filter(HMDB_name %in% metab_name) + + # Combine both dataframes + combined_df <- rbind(patients_df, controls_df) + + # Add empty dummy's to extend the number of metabs to the nr_plots_perpage + n_missing <- nr_plots_perpage - length(metab_name) + if (n_missing > 0) { + dummy_names <- paste0(" ", strrep(" ", seq_len(n_missing))) + metab_order <- c(metab_name, dummy_names) + } else { + metab_order <- metab_name + } + attr(combined_df, "y_order") <- rev(metab_order) + + return(combined_df) + }) + # Add new items to main list + metab_perpage <- append(metab_perpage, current_perpage) + # create list of page headers + metab_category <- c(metab_category, paste(metab_class, seq(nr_chunks), sep = "_")) + } + # add page headers to list + names(metab_perpage) <- metab_category + + return(metab_perpage) +} + +#' Get patient data to be uploaded to Helix +#' +#' @param metab_interest_sorted: list of dataframes with metabolite Z-scores for each sample/patient (list) +#' @param metab_list_all: list of tables with metabolites for Helix and violin plots (list) +#' +#' @return: dataframe with patient data with only metabolites for Helix and violin plots +#' with Helix name, high/low Z-score cutoffs +get_patient_data_to_helix <- function(metab_interest_sorted, metab_list_all) { + # Combine Z-scores of metab groups together + df_all_metabs_zscores <- bind_rows(metab_interest_sorted) + + # Change the Sample column to characters, trim HMDB_name and split HMDB_name in new column + df_all_metabs_zscores <- df_all_metabs_zscores %>% + mutate(Sample = as.character(Sample), + HMDB_name = str_trim(HMDB_name, "right"), + HMDB_name_split = str_split_fixed(HMDB_name, "nitine;", 2)[, 1]) + + # Combine stofgroepen + dims_helix_table <- bind_rows(metab_list_all) + + # Filter for Helix metabolites and split HMDB_name column for matching with df_all_metabs_zscores + dims_helix_table <- dims_helix_table %>% + filter(Helix == "ja") %>% + mutate(HMDB_name_split = str_split_fixed(HMDB_name, "nitine;", 2)[, 1]) %>% + select(HMDB_name_split, Helix_naam, high_zscore, low_zscore) + + # Filter DIMS results for metabolites for Helix and combine Helix info + df_metabs_helix <- df_all_metabs_zscores %>% + filter(HMDB_name_split %in% dims_helix_table$HMDB_name_split) %>% + left_join(dims_helix_table, by = join_by(HMDB_name_split)) %>% + select(HMDB_name, Sample, Z_score, Helix_naam, high_zscore, low_zscore) + + return(df_metabs_helix) +} + +#' Check for Diagnostics patients with correct patient number (e.g. starting with "P2024M") +#' +#' @param patient_column: a column from dataframe with IDs (character vector) +#' +#' @return: a logical vector with TRUE or FALSE for each element (vector) +is_diagnostic_patient <- function(patient_column) { + diagnostic_patients <- grepl("^P[0-9]{4}M", patient_column) + + return(diagnostic_patients) +} + +#' Get the output dataframe for Helix +#' +#' @param protocol_name: protocol name (string) +#' @param df_metabs_helix: dataframe with metabolite Z-scores for patients (dataframe) +#' +#' @return: dataframe with patient metabolite Z-scores in correct format for Helix +output_for_helix <- function(protocol_name, df_metabs_helix) { + # Remove positive controls + df_metabs_helix <- df_metabs_helix %>% filter(is_diagnostic_patient(Sample)) + + # Add 'Vial' column, each patient has unique ID + df_metabs_helix <- df_metabs_helix %>% + group_by(Sample) %>% + mutate(Vial = cur_group_id()) %>% + ungroup() + + # Split patient number into labnummer and Onderzoeksnummer + df_metabs_helix <- add_lab_id_and_onderzoeksnummer(df_metabs_helix) + + # Add column with protocol name + df_metabs_helix$Protocol <- protocol_name + + # Change name Z_score and Helix_naam columns to Amount and Name + change_columns <- c(Amount = "Z_score", Name = "Helix_naam") + df_metabs_helix <- df_metabs_helix %>% rename(all_of(change_columns)) + + # Select only necessary columns and set them in correct order + df_metabs_helix <- df_metabs_helix %>% + select(c(Vial, labnummer, Onderzoeksnummer, Protocol, Name, Amount)) + + # Remove duplicate patient-metabolite combinations ("leucine + isoleucine + allo-isoleucin_Z-score" is added 3 times) + df_metabs_helix <- df_metabs_helix %>% + group_by(Onderzoeksnummer, Name) %>% + distinct() %>% + ungroup() + + return(df_metabs_helix) +} + +#' Adding labnummer and Onderzoeksnummer to a dataframe +#' +#' @param df_metabs_helix: dataframe with patient data to be uploaded to Helix +#' +#' @return: dataframe with added labnummer and Onderzoeksnummer columns +add_lab_id_and_onderzoeksnummer <- function(df_metabs_helix) { + # Split patient number into labnummer and Onderzoeksnummer + for (row in 1:nrow(df_metabs_helix)) { + df_metabs_helix[row, "labnummer"] <- gsub("^P|\\.[0-9]*", "", df_metabs_helix[row, "Sample"]) + labnummer_split <- strsplit(as.character(df_metabs_helix[row, "labnummer"]), "M")[[1]] + df_metabs_helix[row, "Onderzoeksnummer"] <- paste0("MB", labnummer_split[1], "/", labnummer_split[2]) + } + + return(df_metabs_helix) +} + +#' Create a dataframe with all metabolites that exceed the min and max Z-score cutoffs +#' +#' @param patient_name: patient code (string) +#' @param dims_helix_table: dataframe with metabolite Z-scores for each patient and Helix info (dataframe) +#' +#' @return: dataframe with metabolites that exceed the min and max Z-score cutoffs for the selected patient +prepare_alarmvalues <- function(patient_name, dims_helix_table) { + # extract data for patient of interest (patient_name) + patient_metabs_helix <- dims_helix_table %>% + filter(Sample == patient_name) %>% + mutate(Z_score = round(Z_score, 2)) + + patient_high_df <- patient_metabs_helix %>% filter(Z_score > high_zscore) + patient_low_df <- patient_metabs_helix %>% filter(Z_score < low_zscore) + + # sort tables on zscore + patient_high_df <- patient_high_df %>% arrange(desc(Z_score)) + patient_low_df <- patient_low_df %>% arrange(Z_score) + # add lines for increased, decreased + extra_line1 <- c("Increased", "") + extra_line2 <- c("Decreased", "") + + # combine the two lists + top_metab_patient <- rbind(extra_line1, patient_high_df, extra_line2, patient_low_df) + top_metab_patient <- top_metab_patient %>% select(c(HMDB_name, Z_score)) + # remove row names + rownames(top_metab_patient) <- NULL + # change column names for display + colnames(top_metab_patient) <- c("Metabolite", "Z-score") + + return(top_metab_patient) +} + +#' Create a dataframe with the top 20 highest and top 10 lowest metabolites per patient +#' +#' @param pt_name: patient code (string) +#' @param zscore_patients: dataframe with metabolite Z-scores per patient (dataframe) +#' @param top_highest: the number of metabolites with the highest Z-score to display in the table (numeric) +#' @param top_lowest: the number of metabolites with the lowest Z-score to display in the table (numeric) +#' +#' @return: dataframe with 30 metabolites and Z-scores (dataframe) +prepare_toplist <- function(patient_id, zscore_patients) { + top_highest <- 20 + top_lowest <- 10 + patient_df <- zscore_patients %>% + select(c(HMDB_code, HMDB_name, all_of(patient_id))) %>% + arrange(desc(across(patient_id))) + + # Get lowest Zscores + patient_df_low <- patient_df[1:top_lowest, ] + patient_df_low <- patient_df_low %>% mutate(across(patient_id, ~ round(.x ,2))) + + # Get highest Zscores + patient_df_high <- patient_df[nrow(patient_df):(nrow(patient_df) - top_highest + 1), ] + patient_df_high <- patient_df_high %>% mutate(across(patient_id, ~ round(.x ,2))) + + # add lines for increased, decreased + extra_line1 <- c("Increased", "", "") + extra_line2 <- c("Decreased", "", "") + top_metab_pt <- rbind(extra_line1, patient_df_high, extra_line2, patient_df_high) + # remove row names + rownames(top_metab_pt) <- NULL + + # change column names for display + colnames(top_metab_pt) <- c("HMDB_ID", "Metabolite", "Z-score") + + return(top_metab_pt) +} + +#' Create a pdf with table with metabolites and violin plots +#' +#' @param pdf_dir: location where to save the pdf file (string) +#' @param patient_id: patient id (string) +#' @param metab_perpage: list of dataframes, each dataframe contains data for a page in de pdf (list) +#' @param top_metab_pt: dataframe with increased and decreased metabolites for this patient (dataframe) +#' @param explanation: text that explains the violin plots and the pipeline version (string) +create_pdf_violin_plots <- function(pdf_dir, patient_id, metab_perpage, top_metab_pt, explanation) { + # set parameters for plots + plot_height <- 9.6 + plot_width <- 6 + + # patient plots, create the PDF device + patient_id_sub <- patient_id + suffix <- "" + if (grepl("Diagnostics", pdf_dir) & is_diagnostic_patient(patient_id)) { + prefix <- "MB" + suffix <- "_DIMS_PL_DIAG" + # substitute P and M in P2020M00001 into right format for Helix + patient_id_sub <- gsub("[PM]", "", patient_id) + patient_id_sub <- gsub("\\..*", "", patient_id_sub) + } else if (grepl("Diagnostics", pdf_dir)) { + prefix <- "Dx_" + } else if (grepl("IEM", pdf_dir)) { + prefix <- "IEM_" + } else { + prefix <- "R_" + } + + pdf(paste0(pdf_dir, "/", prefix, patient_id_sub, suffix, ".pdf"), + onefile = TRUE, + width = plot_width, + height = plot_height) + + # page headers: + page_headers <- names(metab_perpage) + + # put table into PDF file, if not empty + if (!is.null(dim(top_metab_pt))) { + max_rows_per_page <- 35 + total_rows <- nrow(top_metab_pt) + number_of_pages <- ceiling(total_rows / max_rows_per_page) + + # get the names and numbers in the table aligned + table_theme <- ttheme_default(core = list(fg_params = list(hjust = 0, x = 0.05, fontsize = 6)), + colhead = list(fg_params = list(fontsize = 8, fontface = "bold"))) + + for (page in seq(number_of_pages)) { + start_row <- (page - 1) * max_rows_per_page + 1 + end_row <- min(page * max_rows_per_page, total_rows) + page_data <- top_metab_pt[start_row:end_row, ] + + table_grob <- tableGrob(page_data, theme = table_theme, rows = NULL) + + grid.arrange( + table_grob, + top = paste0("Top deviating metabolites for patient: ", patient_id) + ) + } + } + + # violin plots + for (metab_class in names(metab_perpage)) { + # extract list of metabolites to plot on a page + metab_zscores_df <- metab_perpage[[metab_class]] + # extract original data for patient of interest (pt_name) before cut-offs + patient_zscore_df <- metab_zscores_df %>% filter(Sample == patient_id) + + # Remove patient column and change Z-score. If under -5 to -5 and if above 20 to 20. + metab_zscores_df <- metab_zscores_df %>% + filter(Sample != patient_id) %>% + mutate(Z_score = pmin(pmax(Z_score, -5), 20)) + + # subtitle per page + sub_perpage <- gsub("_", " ", metab_class) + # for IEM plots, put subtitle on two lines + sub_perpage <- gsub("probability", "\nprobability", sub_perpage) + + # draw violin plot. + ggplot_object <- create_violin_plot(metab_zscores_df, patient_zscore_df, sub_perpage, patient_id) + + suppressWarnings(print(ggplot_object)) + } + + # add explanation of violin plots, version number etc. + plot(NA, xlim = c(0, 5), ylim = c(0, 5), bty = "n", xaxt = "n", yaxt = "n", xlab = "", ylab = "") + if (length(explanation) > 0) { + text(0.2, 5, explanation[1], pos = 4, cex = 0.8) + for (line_index in 2:length(explanation)) { + text_y_position <- 5 - (line_index * 0.2) + text(-0.2, text_y_position, explanation[line_index], pos = 4, cex = 0.5) + } + } + + # close the PDF file + dev.off() +} + +#' Create violin plots +#' +#' @param metab_zscores_df: dataframe with Z-scores for all samples (dataframe) +#' @param patient_zscore_df: dataframe with Z-scores for the specified patient (dataframe) +#' @param sub_perpage: subtitle of the page (string) +#' @param patient_id: the patient id of the selected patient (string) +#' +#' @returns +create_violin_plot <- function(metab_zscores_df, patient_zscore_df, sub_perpage, patient_id) { + fontsize <- 1 + circlesize <- 0.8 + # Set colors for the violinplot: green, blue, blue/purple, purple, orange, red + colors_plot <- c("#22E4AC", "#00B0F0", "#504FFF", "#A704FD", "#F36265", "#DA0641") + + y_order <- attr(metab_zscores_df, "y_order") + metab_zscores_df$HMDB_name <- rev(factor(metab_zscores_df$HMDB_name, levels = rev(y_order))) + patient_zscore_df$HMDB_name <- rev(factor(patient_zscore_df$HMDB_name, levels = rev(y_order))) + + ggplot_object <- ggplot(metab_zscores_df, aes(x = Z_score, y = HMDB_name)) + + # Make violin plots + geom_violin(scale = "width", na.rm = TRUE) + + # Add Z-score for the selected patient, shape=22 gives square for patient of interest + geom_point(data = patient_zscore_df, aes(color = Z_score), + size = 3.5 * circlesize, shape = 22, fill = "white", na.rm = TRUE) + + # Add the Z-score at the right side of the plot + geom_text( + data = patient_zscore_df, + aes(16, label = paste0("Z=", round(Z_score, 2))), + hjust = "left", vjust = +0.2, size = 3, na.rm = TRUE) + + # Set colour for the Z-score of the selected patient + scale_fill_gradientn( + colors = colors_plot, values = NULL, space = "Lab", na.value = "grey50", guide = "colourbar", + aesthetics = "colour" + ) + + # Add labels to the axis + labs(x = "Z-scores", y = "Metabolites", subtitle = sub_perpage, color = "z-score") + + # Add a title to the page + ggtitle(label = paste0("Results for patient ", patient_id)) + + # Set theme: size and font type of y-axis labels, remove legend and make the + theme( + axis.text.y = element_text(family = "Courier", size = 6), + legend.position = "none", + plot.caption = element_text(size = rel(fontsize)) + ) + + # Set y-axis to set order + scale_y_discrete(limits = y_order) + + # Limit the x-axis to between -5 and 20 + xlim(-5, 20) + + # Set grey vertical lines at -2 and 2 + geom_vline(xintercept = c(-2, 2), col = "grey", lwd = 0.5, lty = 2) + + + return(ggplot_object) +} + +#' Run the dIEM algorithm (DOI: 10.3390/ijms21030979) +#' +#' @param expected_biomarkers_df: table with information for HMDB codes about IEMs (dataframe) +#' @param zscore_patients: dataframe containing Z-scores for patient (dataframe) +#' +#' @returns probability_score: a dataframe with probability scores for IEMs for each patient (dataframe) +run_diem_algorithm <- function(expected_biomarkers_df, zscore_patients_df, sample_cols) { + # Rank the metabolites for each patient individually + ranking_patients <- zscore_patients_df %>% + mutate(across(-c(HMDB_code, HMDB_name), rank_patient_zscores)) + + ranking_patients <- merge(x = expected_biomarkers_df, y = ranking_patients, + by.x = c("HMDB_code"), by.y = c("HMDB_code")) + + zscore_expected_df <- merge(x = expected_biomarkers_df, y = zscore_patients_df, + by.x = c("HMDB_code"), by.y = c("HMDB_code")) + + # Change Z-score to zero for specific cases + zscore_expected_df <- zscore_expected_df %>% mutate(across( + all_of(sample_cols), + ~ case_when( + Change == "Increase" & Dispensability == "Indispensable" & .x <= 1.6 ~ 0, + Change == "Decrease" & Dispensability == "Indispensable" & .x >= -1.2 ~ 0, + TRUE ~ .x + ) + )) + + # Sort both dataframes on HMDB_code for calculating the metabolite score + zscore_expected_df <- zscore_expected_df[order(zscore_expected_df$HMDB_code), ] + ranking_patients <- ranking_patients[order(ranking_patients$HMDB_code), ] + + # Set up dataframe for the metabolite score, copy zscore_expected_df for biomarker info + metabolite_score_info <- zscore_expected_df + # Calculate metabolite score: Z-score/(Rank * 0.9) + metabolite_score_info[sample_cols] <- zscore_expected_df[sample_cols] / (ranking_patients[sample_cols] * 0.9) + + # Calculate the weighted score: metabolite_score * Total_Weight + metabolite_weight_score <- metabolite_score_info %>% + mutate(across( + all_of(sample_cols), + ~ .x * Total_Weight + )) + + #TODO: dit klopt nu niet, checken dat alleen de eerste waarde wordt gebruikt in orginele dIEM algoritme + # Calculate the probability score for each disease - Mz combination + probability_score <- metabolite_weight_score %>% + group_by(Disease, M.z) %>% + summarise(across( + all_of(sample_cols), + ~ sum(.x, na.rm = TRUE) + ), .groups = "drop") + + # Set probability score to 0 for Z-scores == 0 + for (sample_col in sample_cols) { + # Get indexes of Zscore that equal 0 + zscores_zero_idx <- which(zscore_expected_df[[sample_col]] == 0) + # Get diseases that have a Zscore of 0 + diseases_zero <- unique(zscore_expected_df[zscores_zero_idx, "Disease"]) + # Set probabilty of these diseases to 0 + probability_score[probability_score$Disease %in% diseases_zero, sample_col] <- 0 + } + + colnames(probability_score) <- gsub("_Zscore", "_prob_score", colnames(probability_score)) + + return(probability_score) +} + +#' Ranking Z-scores for a patient, separate for positive and negative Z-scores +#' +#' @param zscore_col: vector with Z-scores for a single patient (vector of integers) +#' +#' @returns ranking: a vector of the ranking of the Z-scores (vector of integers) +rank_patient_zscores <- function(zscore_col) { + # Create ranking column with default NA values + ranking <- rep(NA_real_, length(zscore_col)) + + # Get indexes for negative and positive rows + neg_indexes <- which(zscore_col <= 0) + pos_indexes <- which(zscore_col > 0) + + # Rank the negative and positive Zscores + ranking[neg_indexes] <- dense_rank(zscore_col[neg_indexes]) + ranking[pos_indexes] <- dense_rank(-zscore_col[pos_indexes]) + + return(ranking) +} + +#' Save the probability score dataframe as an Excel file +#' +#' @param probability_score: a dataframe containing probability scores for each patient (dataframe) +#' @param output_dir: location where to save the Excel file (string) +#' @param run_name: name of the run, for the file name (string) +save_prob_scores_to_Excel <- function(probability_score, output_dir, run_name) { + # Create conditional formatting for output Excel sheet. Colors according to values. + wb <- createWorkbook() + addWorksheet(wb, "Probability Scores") + writeData(wb, "Probability Scores", probability_score) + conditionalFormatting(wb, "Probability Scores", cols = 2:ncol(probability_score), rows = 1:nrow(probability_score), + type = "colourScale", style = c("white", "#FFFDA2", "red"), rule = c(1, 10, 100)) + saveWorkbook(wb, file = paste0(output_dir, "/dIEM_algoritme_output_", run_name, ".xlsx"), overwrite = TRUE) + rm(wb) +} From 9ffd606737ba8606a0e4bbe710d86508c9b2ed46 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Fri, 15 Aug 2025 16:12:31 +0200 Subject: [PATCH 054/161] Fixed errors --- DIMS/GenerateViolinPlots.R | 2 +- DIMS/export/generate_violin_plots_functions.R | 43 +++++++++++-------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 2e6cb885..67f94b17 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -143,7 +143,7 @@ for (metabolite_dir in metabolite_dirs) { if (grepl("Diagnost", pdf_dir)) { top_metabs_patient <- prepare_alarmvalues(patient_id, dims_helix_table) } else { - top_metabs_patient <- prepare_toplist(patient_id, zscore_patients) + top_metabs_patient <- prepare_toplist(patient_id, zscore_patients_df) } # generate normal violin plots diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index b1855a7f..beb0ddb9 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -30,7 +30,8 @@ get_intentities_for_ratios <- function(ratios_metabs_df, row_index, intensities_ #' #' @returns: vector of sample IDs that are in both input vectors (vector of strings) get_zscore_columns <- function(colnames_zscore, intensity_cols) { - intersect(paste0(intensity_cols, "_Zscore"), grep("_Zscore", colnames_zscore, value = T)) + sample_intersect <- intersect(paste0(intensity_cols, "_Zscore"), grep("_Zscore", colnames_zscore, value = TRUE)) + return(sample_intersect) } #' Get a list with dataframes for all off the metabolite group in a directory @@ -261,16 +262,18 @@ prepare_alarmvalues <- function(patient_name, dims_helix_table) { patient_high_df <- patient_metabs_helix %>% filter(Z_score > high_zscore) patient_low_df <- patient_metabs_helix %>% filter(Z_score < low_zscore) - # sort tables on zscore - patient_high_df <- patient_high_df %>% arrange(desc(Z_score)) - patient_low_df <- patient_low_df %>% arrange(Z_score) + if (nrow(patient_high_df) > 0 | nrow(patient_low_df) > 0) { + # sort tables on zscore + patient_high_df <- patient_high_df %>% arrange(desc(Z_score)) %>% select(c(HMDB_name, Z_score)) + patient_low_df <- patient_low_df %>% arrange(Z_score) %>% select(c(HMDB_name, Z_score)) + } # add lines for increased, decreased extra_line1 <- c("Increased", "") extra_line2 <- c("Decreased", "") # combine the two lists top_metab_patient <- rbind(extra_line1, patient_high_df, extra_line2, patient_low_df) - top_metab_patient <- top_metab_patient %>% select(c(HMDB_name, Z_score)) + # remove row names rownames(top_metab_patient) <- NULL # change column names for display @@ -291,21 +294,21 @@ prepare_toplist <- function(patient_id, zscore_patients) { top_highest <- 20 top_lowest <- 10 patient_df <- zscore_patients %>% - select(c(HMDB_code, HMDB_name, all_of(patient_id))) %>% - arrange(desc(across(patient_id))) + select(HMDB_code, HMDB_name, !!sym(patient_id)) %>% + arrange(!!sym(patient_id)) # Get lowest Zscores patient_df_low <- patient_df[1:top_lowest, ] - patient_df_low <- patient_df_low %>% mutate(across(patient_id, ~ round(.x ,2))) + patient_df_low <- patient_df_low %>% mutate(across(!!sym(patient_id), ~ round(.x ,2))) # Get highest Zscores patient_df_high <- patient_df[nrow(patient_df):(nrow(patient_df) - top_highest + 1), ] - patient_df_high <- patient_df_high %>% mutate(across(patient_id, ~ round(.x ,2))) + patient_df_high <- patient_df_high %>% mutate(across(!!sym(patient_id), ~ round(.x ,2))) # add lines for increased, decreased extra_line1 <- c("Increased", "", "") extra_line2 <- c("Decreased", "", "") - top_metab_pt <- rbind(extra_line1, patient_df_high, extra_line2, patient_df_high) + top_metab_pt <- rbind(extra_line1, patient_df_high, extra_line2, patient_df_low) # remove row names rownames(top_metab_pt) <- NULL @@ -420,7 +423,7 @@ create_pdf_violin_plots <- function(pdf_dir, patient_id, metab_perpage, top_meta #' @param sub_perpage: subtitle of the page (string) #' @param patient_id: the patient id of the selected patient (string) #' -#' @returns +#' @returns ggpplot_object: a violin plot of metabolites that highlights the selected patient (ggplot object) create_violin_plot <- function(metab_zscores_df, patient_zscore_df, sub_perpage, patient_id) { fontsize <- 1 circlesize <- 0.8 @@ -509,16 +512,18 @@ run_diem_algorithm <- function(expected_biomarkers_df, zscore_patients_df, sampl mutate(across( all_of(sample_cols), ~ .x * Total_Weight - )) + )) %>% + arrange(desc(Disease), desc(Absolute_Weight)) - #TODO: dit klopt nu niet, checken dat alleen de eerste waarde wordt gebruikt in orginele dIEM algoritme # Calculate the probability score for each disease - Mz combination - probability_score <- metabolite_weight_score %>% - group_by(Disease, M.z) %>% - summarise(across( - all_of(sample_cols), - ~ sum(.x, na.rm = TRUE) - ), .groups = "drop") + probability_score <- metabolite_weight_score %>% + filter( + !duplicated(select(., Disease, M.z)) | + !duplicated(select(., Disease, M.z), fromLast = FALSE) + ) %>% + group_by(Disease) %>% + summarise(across(all_of(sample_cols), sum), .groups = "drop") + # Set probability score to 0 for Z-scores == 0 for (sample_col in sample_cols) { From aede6ee53a197286609bda7937f7b812c3314717 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 21 Aug 2025 12:13:54 +0200 Subject: [PATCH 055/161] Fixed linting --- DIMS/GenerateViolinPlots.R | 43 ++-- DIMS/export/generate_violin_plots_functions.R | 222 +++++++++--------- 2 files changed, 130 insertions(+), 135 deletions(-) diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 67f94b17..847c4cb5 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -38,7 +38,7 @@ protocol_name <- "DIMS_PL_DIAG" # Remove columns, move HMDB_code & HMDB_name column to the front, change intensity columns to numeric intensities_zscore_df <- intensities_zscore_df %>% - select(-c(plots, HMDB_name_all, HMDB_ID_all, sec_HMDB_ID, HMDB_key, sec_HMBD_ID_rlvnc, name, + select(-c(plots, HMDB_name_all, HMDB_ID_all, sec_HMDB_ID, HMDB_key, sec_HMBD_ID_rlvnc, name, relevance, descr, origin, fluids, tissue, disease, pathway, nr_ctrls)) %>% relocate(c(HMDB_code, HMDB_name)) %>% rename(mean_controls = avg_ctrls, sd_controls = sd_ctrls) %>% @@ -70,11 +70,11 @@ colnames(ratio_zscore_df) <- colnames(intensities_zscore_df) ratio_zscore_df$HMDB_code <- ratios_metabs_df$HMDB.code ratio_zscore_df$HMDB_name <- ratios_metabs_df$Ratio_name -for (row_index in 1:nrow(ratios_metabs_df)) { - numerator_intensities <- get_intentities_for_ratios(ratios_metabs_df, row_index, - intensities_zscore_df, "HMDB_numerator", intensity_cols) - denominator_intensities <- get_intentities_for_ratios(ratios_metabs_df, row_index, - intensities_zscore_df, "HMDB_denominator", intensity_cols) +for (row_index in seq_len(nrow(ratios_metabs_df))) { + numerator_intensities <- get_intentities_for_ratios(ratios_metabs_df, row_index, + intensities_zscore_df, "HMDB_numerator", intensity_cols) + denominator_intensities <- get_intentities_for_ratios(ratios_metabs_df, row_index, + intensities_zscore_df, "HMDB_denominator", intensity_cols) # calculate intensity ratios ratio_zscore_df[row_index, intensity_cols_index] <- log2(numerator_intensities / denominator_intensities) } @@ -103,8 +103,8 @@ colnames(zscore_controls_df) <- gsub("_Zscore", "", colnames(zscore_controls_df) expected_biomarkers_df <- expected_biomarkers_df %>% rename(HMDB_code = HMDB.code, HMDB_name = Metabolite) -expected_biomarkers_info <- expected_biomarkers_df %>% - select(c(Disease, HMDB_code, HMDB_name)) %>% +expected_biomarkers_info <- expected_biomarkers_df %>% + select(c(Disease, HMDB_code, HMDB_name)) %>% distinct(Disease, HMDB_code, .keep_all = TRUE) metabolite_dirs <- list.files(path = path_metabolite_groups, full.names = FALSE, recursive = FALSE) @@ -122,20 +122,20 @@ for (metabolite_dir in metabolite_dirs) { nr_plots_perpage, nr_of_patients, nr_of_controls) # for Diagnostics metabolites to be saved in Helix - if(grepl("Diagnost", pdf_dir)) { + if (grepl("Diagnost", pdf_dir)) { # get table that combines DIMS results with stofgroepen/Helix table dims_helix_table <- get_patient_data_to_helix(metab_interest_sorted, metab_list_all) - + # check if run contains Diagnostics patients (e.g. "P2024M"), not for research runs - if(any(is_diagnostic_patient(dims_helix_table$Sample))){ + if (any(is_diagnostic_patient(dims_helix_table$Sample))) { # get output file for Helix output_helix <- output_for_helix(protocol_name, dims_helix_table) # write output to file - path_helixfile <- paste0(output_dir, "output_Helix_", run_name,".csv") - write.csv(output_helix, path_helixfile, quote = F, row.names = F) + path_helixfile <- paste0(output_dir, "output_Helix_", run_name, ".csv") + write.csv(output_helix, path_helixfile, quote = FALSE, row.names = FALSE) } } - + # make violin plots per patient for (patient_id in patients) { # for category Diagnostics, make list of metabolites that exceed alarm values for this patient @@ -157,7 +157,7 @@ expected_biomarkers_df <- expected_biomarkers_df %>% rename(HMDB_code = HMDB.cod diem_probability_score <- run_diem_algorithm(expected_biomarkers_df, zscore_patients_df, patients) -save_prob_scores_to_Excel(diem_probability_score, output_dir, run_name) +save_prob_scores_to_excel(diem_probability_score, output_dir, run_name) #### Generate dIEM plots ######### @@ -174,7 +174,7 @@ for (patient_id in patients) { arrange(desc(!!sym(patient_id))) %>% slice(1:top_number_iem_diseases) %>% filter(!!sym(patient_id) >= threshold_iem) - + if (nrow(patient_top_iems_probs) > 0) { top_iems <- patient_top_iems_probs %>% pull(Disease) # Get the metabolites for each IEM and their probability @@ -186,26 +186,27 @@ for (patient_id in patients) { return(metab_iem) }) names(metabs_iems) <- metabs_iems_names - + # Get the Z-scores with metabolite information metab_iem_sorted <- combine_metab_info_zscores(metabs_iems, zscore_patients_df) metab_iem_controls <- combine_metab_info_zscores(metabs_iems, zscore_controls_df) # Get a list of dataframes for each IEM diem_metab_perpage <- prepare_data_perpage(metab_iem_sorted, metab_iem_controls, nr_plots_perpage, nr_of_patients, nr_of_controls) - # Get a dataframe of the top metabolites + # Get a dataframe of the top metabolites top_metabs_patient <- prepare_toplist(patient_id, zscore_patients_df) - + # Generate and save dIEM violin plots create_pdf_violin_plots(diem_plot_dir, patient_id, diem_metab_perpage, top_metabs_patient, explanation_violin_plot) - + } else { patient_no_iem <- c(patient_no_iem, patient_id) } } if (length(patient_no_iem) > 0) { - patient_no_iem <- c(paste0("The following patient(s) did not have dIEM probability scores higher than ", threshold_iem, " :"), + patient_no_iem <- c(paste0("The following patient(s) did not have dIEM probability scores higher than ", + threshold_iem, " :"), patient_no_iem) write(file = paste0(output_dir, "missing_probability_scores.txt"), patient_no_iem) } diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index beb0ddb9..0ae7edff 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -11,13 +11,15 @@ get_intentities_for_ratios <- function(ratios_metabs_df, row_index, intensities_ fraction_side_hmdb_ids <- ratios_metabs_df[row_index, fraction_side] if (grepl("plus", fraction_side_hmdb_ids)) { fraction_side_hmdb_id_list <- strsplit(fraction_side_hmdb_ids, "plus")[[1]] - fraction_side_intensity_list <- intensities_zscore_df %>% filter(HMDB_code %in% fraction_side_hmdb_id_list) %>% + fraction_side_intensity_list <- intensities_zscore_df %>% + filter(HMDB_code %in% fraction_side_hmdb_id_list) %>% select(any_of(intensity_cols)) fraction_side_intensity <- apply(fraction_side_intensity_list, 2, sum) - } else if(fraction_side_hmdb_ids == "one") { + } else if (fraction_side_hmdb_ids == "one") { fraction_side_intensity <- 1 } else { - fraction_side_intensity <- intensities_zscore_df %>% filter(HMDB_code == fraction_side_hmdb_ids) %>% + fraction_side_intensity <- intensities_zscore_df %>% + filter(HMDB_code == fraction_side_hmdb_ids) %>% select(any_of(intensity_cols)) } return(fraction_side_intensity) @@ -43,10 +45,10 @@ get_list_metabolites <- function(metab_group_dir) { # get a list of all metabolite files metabolite_files <- list.files(metab_group_dir, pattern = "*.txt", full.names = FALSE, recursive = FALSE) # put all metabolites into one list - metab_list_all <- lapply(paste(metab_group_dir, metabolite_files, sep = "/"), + metab_list_all <- lapply(paste(metab_group_dir, metabolite_files, sep = "/"), read.table, sep = "\t", header = TRUE, quote = "") names(metab_list_all) <- gsub(".txt", "", metabolite_files) - + return(metab_list_all) } @@ -58,34 +60,35 @@ get_list_metabolites <- function(metab_group_dir) { #' @return: list of dataframes for each stofgroep with data for each metabolite and patient/control per row combine_metab_info_zscores <- function(metab_list_all, zscore_df) { # remove HMDB_name column and "_Zscore" from column (patient) names - zscore_df <- zscore_df %>% select(-HMDB_name) %>% + zscore_df <- zscore_df %>% + select(-HMDB_name) %>% rename_with(~ str_remove(.x, "_Zscore"), .cols = contains("_Zscore")) - + # put data into pages, max 20 violin plots per page in PDF metab_interest_sorted <- list() - + for (metab_class in names(metab_list_all)) { metab_df <- metab_list_all[[metab_class]] # Select HMDB_code and HMDB_name columns metab_df <- metab_df %>% select(HMDB_code, HMDB_name) - + # Change the HMDB_name column so all names have 45 characters metab_df <- metab_df %>% mutate(HMDB_name = case_when( str_length(HMDB_name) > 45 ~ str_c(str_sub(HMDB_name, 1, 42), "..."), str_length(HMDB_name) < 45 ~ str_pad(HMDB_name, 45, side = "right", pad = " "), TRUE ~ HMDB_name )) - + # Join metabolite info with the Z-score dataframe metab_interest <- metab_df %>% inner_join(zscore_df, by = "HMDB_code") %>% select(-HMDB_code) - + # put the data frame in long format - metab_interest_melt <- reshape2::melt(metab_interest, id.vars = "HMDB_name", variable.name = "Sample", + metab_interest_melt <- reshape2::melt(metab_interest, id.vars = "HMDB_name", variable.name = "Sample", value.name = "Z_score") # Add the dataframe sorted on HMDB_name to a list metab_interest_sorted[[metab_class]] <- metab_interest_melt } - + return(metab_interest_sorted) } @@ -102,27 +105,27 @@ combine_metab_info_zscores <- function(metab_list_all, zscore_df) { prepare_data_perpage <- function(metab_interest_sorted, metab_interest_contr, nr_plots_perpage, nr_pat, nr_contr) { metab_perpage <- list() metab_category <- c() - + for (metab_class in names(metab_interest_sorted)) { # Get the data for patients and controls for the metab_interest_sorted list metab_sort_patients_df <- metab_interest_sorted[[metab_class]] metab_sort_controls_df <- metab_interest_contr[[metab_class]] - + # Calculate the number of pages nr_pages <- ceiling(length(unique(metab_sort_patients_df$HMDB_name)) / nr_plots_perpage) - + # Get all metabolites and create list with HMDB naames of max nr_plots_perpage long metabolites <- unique(metab_sort_patients_df$HMDB_name) metabolites_in_chunks <- split(metabolites, ceiling(seq_along(metabolites) / nr_plots_perpage)) nr_chunks <- length(metabolites_in_chunks) - + current_perpage <- lapply(metabolites_in_chunks, function(metab_name) { patients_df <- metab_sort_patients_df %>% filter(HMDB_name %in% metab_name) controls_df <- metab_sort_controls_df %>% filter(HMDB_name %in% metab_name) - + # Combine both dataframes combined_df <- rbind(patients_df, controls_df) - + # Add empty dummy's to extend the number of metabs to the nr_plots_perpage n_missing <- nr_plots_perpage - length(metab_name) if (n_missing > 0) { @@ -132,7 +135,7 @@ prepare_data_perpage <- function(metab_interest_sorted, metab_interest_contr, nr metab_order <- metab_name } attr(combined_df, "y_order") <- rev(metab_order) - + return(combined_df) }) # Add new items to main list @@ -142,7 +145,7 @@ prepare_data_perpage <- function(metab_interest_sorted, metab_interest_contr, nr } # add page headers to list names(metab_perpage) <- metab_category - + return(metab_perpage) } @@ -151,33 +154,33 @@ prepare_data_perpage <- function(metab_interest_sorted, metab_interest_contr, nr #' @param metab_interest_sorted: list of dataframes with metabolite Z-scores for each sample/patient (list) #' @param metab_list_all: list of tables with metabolites for Helix and violin plots (list) #' -#' @return: dataframe with patient data with only metabolites for Helix and violin plots +#' @return: dataframe with patient data with only metabolites for Helix and violin plots #' with Helix name, high/low Z-score cutoffs get_patient_data_to_helix <- function(metab_interest_sorted, metab_list_all) { # Combine Z-scores of metab groups together df_all_metabs_zscores <- bind_rows(metab_interest_sorted) - + # Change the Sample column to characters, trim HMDB_name and split HMDB_name in new column df_all_metabs_zscores <- df_all_metabs_zscores %>% mutate(Sample = as.character(Sample), HMDB_name = str_trim(HMDB_name, "right"), HMDB_name_split = str_split_fixed(HMDB_name, "nitine;", 2)[, 1]) - + # Combine stofgroepen dims_helix_table <- bind_rows(metab_list_all) - + # Filter for Helix metabolites and split HMDB_name column for matching with df_all_metabs_zscores dims_helix_table <- dims_helix_table %>% filter(Helix == "ja") %>% mutate(HMDB_name_split = str_split_fixed(HMDB_name, "nitine;", 2)[, 1]) %>% select(HMDB_name_split, Helix_naam, high_zscore, low_zscore) - + # Filter DIMS results for metabolites for Helix and combine Helix info df_metabs_helix <- df_all_metabs_zscores %>% filter(HMDB_name_split %in% dims_helix_table$HMDB_name_split) %>% left_join(dims_helix_table, by = join_by(HMDB_name_split)) %>% select(HMDB_name, Sample, Z_score, Helix_naam, high_zscore, low_zscore) - + return(df_metabs_helix) } @@ -188,7 +191,7 @@ get_patient_data_to_helix <- function(metab_interest_sorted, metab_list_all) { #' @return: a logical vector with TRUE or FALSE for each element (vector) is_diagnostic_patient <- function(patient_column) { diagnostic_patients <- grepl("^P[0-9]{4}M", patient_column) - + return(diagnostic_patients) } @@ -201,33 +204,33 @@ is_diagnostic_patient <- function(patient_column) { output_for_helix <- function(protocol_name, df_metabs_helix) { # Remove positive controls df_metabs_helix <- df_metabs_helix %>% filter(is_diagnostic_patient(Sample)) - + # Add 'Vial' column, each patient has unique ID df_metabs_helix <- df_metabs_helix %>% group_by(Sample) %>% mutate(Vial = cur_group_id()) %>% ungroup() - + # Split patient number into labnummer and Onderzoeksnummer - df_metabs_helix <- add_lab_id_and_onderzoeksnummer(df_metabs_helix) - + df_metabs_helix <- add_lab_id_and_onderzoeksnr(df_metabs_helix) + # Add column with protocol name df_metabs_helix$Protocol <- protocol_name - + # Change name Z_score and Helix_naam columns to Amount and Name change_columns <- c(Amount = "Z_score", Name = "Helix_naam") df_metabs_helix <- df_metabs_helix %>% rename(all_of(change_columns)) - + # Select only necessary columns and set them in correct order df_metabs_helix <- df_metabs_helix %>% select(c(Vial, labnummer, Onderzoeksnummer, Protocol, Name, Amount)) - + # Remove duplicate patient-metabolite combinations ("leucine + isoleucine + allo-isoleucin_Z-score" is added 3 times) df_metabs_helix <- df_metabs_helix %>% group_by(Onderzoeksnummer, Name) %>% distinct() %>% ungroup() - + return(df_metabs_helix) } @@ -236,14 +239,13 @@ output_for_helix <- function(protocol_name, df_metabs_helix) { #' @param df_metabs_helix: dataframe with patient data to be uploaded to Helix #' #' @return: dataframe with added labnummer and Onderzoeksnummer columns -add_lab_id_and_onderzoeksnummer <- function(df_metabs_helix) { +add_lab_id_and_onderzoeksnr <- function(df_metabs_helix) { # Split patient number into labnummer and Onderzoeksnummer - for (row in 1:nrow(df_metabs_helix)) { + for (row in seq_len(nrow(df_metabs_helix))) { df_metabs_helix[row, "labnummer"] <- gsub("^P|\\.[0-9]*", "", df_metabs_helix[row, "Sample"]) labnummer_split <- strsplit(as.character(df_metabs_helix[row, "labnummer"]), "M")[[1]] df_metabs_helix[row, "Onderzoeksnummer"] <- paste0("MB", labnummer_split[1], "/", labnummer_split[2]) } - return(df_metabs_helix) } @@ -258,11 +260,11 @@ prepare_alarmvalues <- function(patient_name, dims_helix_table) { patient_metabs_helix <- dims_helix_table %>% filter(Sample == patient_name) %>% mutate(Z_score = round(Z_score, 2)) - + patient_high_df <- patient_metabs_helix %>% filter(Z_score > high_zscore) patient_low_df <- patient_metabs_helix %>% filter(Z_score < low_zscore) - - if (nrow(patient_high_df) > 0 | nrow(patient_low_df) > 0) { + + if (nrow(patient_high_df) > 0 || nrow(patient_low_df) > 0) { # sort tables on zscore patient_high_df <- patient_high_df %>% arrange(desc(Z_score)) %>% select(c(HMDB_name, Z_score)) patient_low_df <- patient_low_df %>% arrange(Z_score) %>% select(c(HMDB_name, Z_score)) @@ -270,15 +272,15 @@ prepare_alarmvalues <- function(patient_name, dims_helix_table) { # add lines for increased, decreased extra_line1 <- c("Increased", "") extra_line2 <- c("Decreased", "") - + # combine the two lists top_metab_patient <- rbind(extra_line1, patient_high_df, extra_line2, patient_low_df) - + # remove row names rownames(top_metab_patient) <- NULL # change column names for display colnames(top_metab_patient) <- c("Metabolite", "Z-score") - + return(top_metab_patient) } @@ -286,7 +288,7 @@ prepare_alarmvalues <- function(patient_name, dims_helix_table) { #' #' @param pt_name: patient code (string) #' @param zscore_patients: dataframe with metabolite Z-scores per patient (dataframe) -#' @param top_highest: the number of metabolites with the highest Z-score to display in the table (numeric) +#' @param top_highest: the number of metabolites with the highest Z-score to display in the table (numeric) #' @param top_lowest: the number of metabolites with the lowest Z-score to display in the table (numeric) #' #' @return: dataframe with 30 metabolites and Z-scores (dataframe) @@ -296,25 +298,25 @@ prepare_toplist <- function(patient_id, zscore_patients) { patient_df <- zscore_patients %>% select(HMDB_code, HMDB_name, !!sym(patient_id)) %>% arrange(!!sym(patient_id)) - + # Get lowest Zscores patient_df_low <- patient_df[1:top_lowest, ] - patient_df_low <- patient_df_low %>% mutate(across(!!sym(patient_id), ~ round(.x ,2))) - + patient_df_low <- patient_df_low %>% mutate(across(!!sym(patient_id), ~ round(.x, 2))) + # Get highest Zscores patient_df_high <- patient_df[nrow(patient_df):(nrow(patient_df) - top_highest + 1), ] - patient_df_high <- patient_df_high %>% mutate(across(!!sym(patient_id), ~ round(.x ,2))) - + patient_df_high <- patient_df_high %>% mutate(across(!!sym(patient_id), ~ round(.x, 2))) + # add lines for increased, decreased extra_line1 <- c("Increased", "", "") extra_line2 <- c("Decreased", "", "") top_metab_pt <- rbind(extra_line1, patient_df_high, extra_line2, patient_df_low) # remove row names rownames(top_metab_pt) <- NULL - + # change column names for display colnames(top_metab_pt) <- c("HMDB_ID", "Metabolite", "Z-score") - + return(top_metab_pt) } @@ -329,11 +331,11 @@ create_pdf_violin_plots <- function(pdf_dir, patient_id, metab_perpage, top_meta # set parameters for plots plot_height <- 9.6 plot_width <- 6 - + # patient plots, create the PDF device patient_id_sub <- patient_id suffix <- "" - if (grepl("Diagnostics", pdf_dir) & is_diagnostic_patient(patient_id)) { + if (grepl("Diagnostics", pdf_dir) && is_diagnostic_patient(patient_id)) { prefix <- "MB" suffix <- "_DIMS_PL_DIAG" # substitute P and M in P2020M00001 into right format for Helix @@ -346,62 +348,62 @@ create_pdf_violin_plots <- function(pdf_dir, patient_id, metab_perpage, top_meta } else { prefix <- "R_" } - + pdf(paste0(pdf_dir, "/", prefix, patient_id_sub, suffix, ".pdf"), onefile = TRUE, width = plot_width, height = plot_height) - + # page headers: page_headers <- names(metab_perpage) - + # put table into PDF file, if not empty if (!is.null(dim(top_metab_pt))) { max_rows_per_page <- 35 total_rows <- nrow(top_metab_pt) number_of_pages <- ceiling(total_rows / max_rows_per_page) - + # get the names and numbers in the table aligned table_theme <- ttheme_default(core = list(fg_params = list(hjust = 0, x = 0.05, fontsize = 6)), colhead = list(fg_params = list(fontsize = 8, fontface = "bold"))) - + for (page in seq(number_of_pages)) { start_row <- (page - 1) * max_rows_per_page + 1 end_row <- min(page * max_rows_per_page, total_rows) page_data <- top_metab_pt[start_row:end_row, ] - + table_grob <- tableGrob(page_data, theme = table_theme, rows = NULL) - + grid.arrange( table_grob, top = paste0("Top deviating metabolites for patient: ", patient_id) ) } } - + # violin plots for (metab_class in names(metab_perpage)) { # extract list of metabolites to plot on a page metab_zscores_df <- metab_perpage[[metab_class]] # extract original data for patient of interest (pt_name) before cut-offs patient_zscore_df <- metab_zscores_df %>% filter(Sample == patient_id) - - # Remove patient column and change Z-score. If under -5 to -5 and if above 20 to 20. + + # Remove patient column and change Z-score. If under -5 to -5 and if above 20 to 20. metab_zscores_df <- metab_zscores_df %>% filter(Sample != patient_id) %>% mutate(Z_score = pmin(pmax(Z_score, -5), 20)) - + # subtitle per page sub_perpage <- gsub("_", " ", metab_class) # for IEM plots, put subtitle on two lines sub_perpage <- gsub("probability", "\nprobability", sub_perpage) - - # draw violin plot. + + # draw violin plot. ggplot_object <- create_violin_plot(metab_zscores_df, patient_zscore_df, sub_perpage, patient_id) - + suppressWarnings(print(ggplot_object)) } - + # add explanation of violin plots, version number etc. plot(NA, xlim = c(0, 5), ylim = c(0, 5), bty = "n", xaxt = "n", yaxt = "n", xlab = "", ylab = "") if (length(explanation) > 0) { @@ -411,7 +413,7 @@ create_pdf_violin_plots <- function(pdf_dir, patient_id, metab_perpage, top_meta text(-0.2, text_y_position, explanation[line_index], pos = 4, cex = 0.5) } } - + # close the PDF file dev.off() } @@ -420,7 +422,7 @@ create_pdf_violin_plots <- function(pdf_dir, patient_id, metab_perpage, top_meta #' #' @param metab_zscores_df: dataframe with Z-scores for all samples (dataframe) #' @param patient_zscore_df: dataframe with Z-scores for the specified patient (dataframe) -#' @param sub_perpage: subtitle of the page (string) +#' @param sub_perpage: subtitle of the page (string) #' @param patient_id: the patient id of the selected patient (string) #' #' @returns ggpplot_object: a violin plot of metabolites that highlights the selected patient (ggplot object) @@ -429,11 +431,11 @@ create_violin_plot <- function(metab_zscores_df, patient_zscore_df, sub_perpage, circlesize <- 0.8 # Set colors for the violinplot: green, blue, blue/purple, purple, orange, red colors_plot <- c("#22E4AC", "#00B0F0", "#504FFF", "#A704FD", "#F36265", "#DA0641") - + y_order <- attr(metab_zscores_df, "y_order") metab_zscores_df$HMDB_name <- rev(factor(metab_zscores_df$HMDB_name, levels = rev(y_order))) patient_zscore_df$HMDB_name <- rev(factor(patient_zscore_df$HMDB_name, levels = rev(y_order))) - + ggplot_object <- ggplot(metab_zscores_df, aes(x = Z_score, y = HMDB_name)) + # Make violin plots geom_violin(scale = "width", na.rm = TRUE) + @@ -441,53 +443,48 @@ create_violin_plot <- function(metab_zscores_df, patient_zscore_df, sub_perpage, geom_point(data = patient_zscore_df, aes(color = Z_score), size = 3.5 * circlesize, shape = 22, fill = "white", na.rm = TRUE) + # Add the Z-score at the right side of the plot - geom_text( - data = patient_zscore_df, - aes(16, label = paste0("Z=", round(Z_score, 2))), - hjust = "left", vjust = +0.2, size = 3, na.rm = TRUE) + + geom_text(data = patient_zscore_df, + aes(16, label = paste0("Z=", round(Z_score, 2))), + hjust = "left", vjust = +0.2, size = 3, na.rm = TRUE) + # Set colour for the Z-score of the selected patient - scale_fill_gradientn( - colors = colors_plot, values = NULL, space = "Lab", na.value = "grey50", guide = "colourbar", - aesthetics = "colour" - ) + + scale_fill_gradientn(colors = colors_plot, values = NULL, space = "Lab", + na.value = "grey50", guide = "colourbar", aesthetics = "colour") + # Add labels to the axis labs(x = "Z-scores", y = "Metabolites", subtitle = sub_perpage, color = "z-score") + # Add a title to the page ggtitle(label = paste0("Results for patient ", patient_id)) + - # Set theme: size and font type of y-axis labels, remove legend and make the - theme( - axis.text.y = element_text(family = "Courier", size = 6), - legend.position = "none", - plot.caption = element_text(size = rel(fontsize)) - ) + + # Set theme: size and font type of y-axis labels, remove legend and make the + theme(axis.text.y = element_text(family = "Courier", size = 6), + legend.position = "none", + plot.caption = element_text(size = rel(fontsize))) + # Set y-axis to set order scale_y_discrete(limits = y_order) + # Limit the x-axis to between -5 and 20 xlim(-5, 20) + # Set grey vertical lines at -2 and 2 geom_vline(xintercept = c(-2, 2), col = "grey", lwd = 0.5, lty = 2) - - + + return(ggplot_object) } #' Run the dIEM algorithm (DOI: 10.3390/ijms21030979) #' -#' @param expected_biomarkers_df: table with information for HMDB codes about IEMs (dataframe) +#' @param expected_biomarkers_df: table with information for HMDB codes about IEMs (dataframe) #' @param zscore_patients: dataframe containing Z-scores for patient (dataframe) #' #' @returns probability_score: a dataframe with probability scores for IEMs for each patient (dataframe) run_diem_algorithm <- function(expected_biomarkers_df, zscore_patients_df, sample_cols) { # Rank the metabolites for each patient individually - ranking_patients <- zscore_patients_df %>% + ranking_patients <- zscore_patients_df %>% mutate(across(-c(HMDB_code, HMDB_name), rank_patient_zscores)) - + ranking_patients <- merge(x = expected_biomarkers_df, y = ranking_patients, by.x = c("HMDB_code"), by.y = c("HMDB_code")) - + zscore_expected_df <- merge(x = expected_biomarkers_df, y = zscore_patients_df, by.x = c("HMDB_code"), by.y = c("HMDB_code")) - + # Change Z-score to zero for specific cases zscore_expected_df <- zscore_expected_df %>% mutate(across( all_of(sample_cols), @@ -497,34 +494,31 @@ run_diem_algorithm <- function(expected_biomarkers_df, zscore_patients_df, sampl TRUE ~ .x ) )) - + # Sort both dataframes on HMDB_code for calculating the metabolite score zscore_expected_df <- zscore_expected_df[order(zscore_expected_df$HMDB_code), ] ranking_patients <- ranking_patients[order(ranking_patients$HMDB_code), ] - + # Set up dataframe for the metabolite score, copy zscore_expected_df for biomarker info metabolite_score_info <- zscore_expected_df # Calculate metabolite score: Z-score/(Rank * 0.9) metabolite_score_info[sample_cols] <- zscore_expected_df[sample_cols] / (ranking_patients[sample_cols] * 0.9) - + # Calculate the weighted score: metabolite_score * Total_Weight - metabolite_weight_score <- metabolite_score_info %>% + metabolite_weight_score <- metabolite_score_info %>% mutate(across( all_of(sample_cols), ~ .x * Total_Weight )) %>% arrange(desc(Disease), desc(Absolute_Weight)) - + # Calculate the probability score for each disease - Mz combination probability_score <- metabolite_weight_score %>% - filter( - !duplicated(select(., Disease, M.z)) | - !duplicated(select(., Disease, M.z), fromLast = FALSE) - ) %>% - group_by(Disease) %>% + filter(!duplicated(select(., Disease, M.z)) | + !duplicated(select(., Disease, M.z), fromLast = FALSE)) %>% + group_by(Disease) %>% summarise(across(all_of(sample_cols), sum), .groups = "drop") - - + # Set probability score to 0 for Z-scores == 0 for (sample_col in sample_cols) { # Get indexes of Zscore that equal 0 @@ -534,9 +528,9 @@ run_diem_algorithm <- function(expected_biomarkers_df, zscore_patients_df, sampl # Set probabilty of these diseases to 0 probability_score[probability_score$Disease %in% diseases_zero, sample_col] <- 0 } - + colnames(probability_score) <- gsub("_Zscore", "_prob_score", colnames(probability_score)) - + return(probability_score) } @@ -548,15 +542,15 @@ run_diem_algorithm <- function(expected_biomarkers_df, zscore_patients_df, sampl rank_patient_zscores <- function(zscore_col) { # Create ranking column with default NA values ranking <- rep(NA_real_, length(zscore_col)) - + # Get indexes for negative and positive rows neg_indexes <- which(zscore_col <= 0) pos_indexes <- which(zscore_col > 0) - + # Rank the negative and positive Zscores ranking[neg_indexes] <- dense_rank(zscore_col[neg_indexes]) ranking[pos_indexes] <- dense_rank(-zscore_col[pos_indexes]) - + return(ranking) } @@ -565,12 +559,12 @@ rank_patient_zscores <- function(zscore_col) { #' @param probability_score: a dataframe containing probability scores for each patient (dataframe) #' @param output_dir: location where to save the Excel file (string) #' @param run_name: name of the run, for the file name (string) -save_prob_scores_to_Excel <- function(probability_score, output_dir, run_name) { +save_prob_scores_to_excel <- function(probability_score, output_dir, run_name) { # Create conditional formatting for output Excel sheet. Colors according to values. wb <- createWorkbook() addWorksheet(wb, "Probability Scores") writeData(wb, "Probability Scores", probability_score) - conditionalFormatting(wb, "Probability Scores", cols = 2:ncol(probability_score), rows = 1:nrow(probability_score), + conditionalFormatting(wb, "Probability Scores", cols = 2:ncol(probability_score), rows = seq_len(nrow(probability_score)), type = "colourScale", style = c("white", "#FFFDA2", "red"), rule = c(1, 10, 100)) saveWorkbook(wb, file = paste0(output_dir, "/dIEM_algoritme_output_", run_name, ".xlsx"), overwrite = TRUE) rm(wb) From 8094aede0a4bdfe3998e4fcee546785cbd214540 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 21 Aug 2025 12:15:08 +0200 Subject: [PATCH 056/161] Added new package for unit testing --- .github/workflows/dims_test.yml | 2 +- DIMS/tests/testthat.R | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dims_test.yml b/.github/workflows/dims_test.yml index fa2a8aa2..b547d126 100644 --- a/.github/workflows/dims_test.yml +++ b/.github/workflows/dims_test.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 - name: Install dependencies - run: Rscript -e "install.packages(c('testthat', 'withr'))" + run: Rscript -e "install.packages(c('testthat', 'withr', 'vdiffr'))" - name: Run tests run: Rscript tests/testthat.R diff --git a/DIMS/tests/testthat.R b/DIMS/tests/testthat.R index 3f13a069..636acf7d 100644 --- a/DIMS/tests/testthat.R +++ b/DIMS/tests/testthat.R @@ -1,6 +1,7 @@ # Run all unit tests library(testthat) library(withr) +library(vdiffr) # enable snapshots local_edition(3) From 62160b2095f45a79bbf43d64ababe2b577db1a22 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 21 Aug 2025 12:17:34 +0200 Subject: [PATCH 057/161] Added unit tests and associated files for GenerateViolinPlots --- .../generate_qc_output/test_barplot.new.png | Bin 0 -> 46638 bytes .../test_excel_dIEM.xlsx | Bin 0 -> 6754 bytes .../violin-plot-p2025m1.svg | 74 ++++ .../violin_pdf_P2025M1.new.pdf | Bin 0 -> 28962 bytes .../violin_pdf_P2025M1.pdf | Bin 0 -> 28962 bytes .../make_test_data_GenerateViolinPlots.R | 290 +++++++++++++ .../test_acyl_carnitines_controls.txt | 11 + .../fixtures/test_acyl_carnitines_df.txt | 21 + .../test_acyl_carnitines_patients.txt | 11 + .../fixtures/test_crea_gua_controls.txt | 11 + .../testthat/fixtures/test_crea_gua_df.txt | 21 + .../fixtures/test_crea_gua_patients.txt | 11 + .../fixtures/test_df_metabs_helix.txt | 16 + .../fixtures/test_expected_biomarkers_df.txt | 16 + .../fixtures/test_intensities_zscore_df.txt | 7 + .../test_acyl_carnitines.txt | 4 + .../test_metabolite_groups/test_crea_gua.txt | 3 + .../fixtures/test_probability_score_df.txt | 8 + .../fixtures/test_zscore_patient_df.txt | 31 ++ .../testthat/test_generate_violin_plots.R | 397 ++++++++++++++++++ 20 files changed, 932 insertions(+) create mode 100644 DIMS/tests/testthat/_snaps/generate_qc_output/test_barplot.new.png create mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx create mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/violin-plot-p2025m1.svg create mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.new.pdf create mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.pdf create mode 100644 DIMS/tests/testthat/fixtures/make_test_data_GenerateViolinPlots.R create mode 100644 DIMS/tests/testthat/fixtures/test_acyl_carnitines_controls.txt create mode 100644 DIMS/tests/testthat/fixtures/test_acyl_carnitines_df.txt create mode 100644 DIMS/tests/testthat/fixtures/test_acyl_carnitines_patients.txt create mode 100644 DIMS/tests/testthat/fixtures/test_crea_gua_controls.txt create mode 100644 DIMS/tests/testthat/fixtures/test_crea_gua_df.txt create mode 100644 DIMS/tests/testthat/fixtures/test_crea_gua_patients.txt create mode 100644 DIMS/tests/testthat/fixtures/test_df_metabs_helix.txt create mode 100644 DIMS/tests/testthat/fixtures/test_expected_biomarkers_df.txt create mode 100644 DIMS/tests/testthat/fixtures/test_intensities_zscore_df.txt create mode 100644 DIMS/tests/testthat/fixtures/test_metabolite_groups/test_acyl_carnitines.txt create mode 100644 DIMS/tests/testthat/fixtures/test_metabolite_groups/test_crea_gua.txt create mode 100644 DIMS/tests/testthat/fixtures/test_probability_score_df.txt create mode 100644 DIMS/tests/testthat/fixtures/test_zscore_patient_df.txt create mode 100644 DIMS/tests/testthat/test_generate_violin_plots.R diff --git a/DIMS/tests/testthat/_snaps/generate_qc_output/test_barplot.new.png b/DIMS/tests/testthat/_snaps/generate_qc_output/test_barplot.new.png new file mode 100644 index 0000000000000000000000000000000000000000..58f016c18244ebec324251a250268171c93e9633 GIT binary patch literal 46638 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 C+t(xj literal 0 HcmV?d00001 diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx b/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..fa6d905118380c37c24a2dd07c5009e9e5c1a20f GIT binary patch literal 6754 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Z=0.31 +Z=2.34 + + + + +metab3 +metab1 + + + + + + + + +-5 +0 +5 +10 +15 +20 +Z-scores +Metabolites +test acyl carnitines +Results for patient P2025M1 + + diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.new.pdf b/DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.new.pdf new file mode 100644 index 0000000000000000000000000000000000000000..fc5c3d6b710f85266785259b8e01c0c48e6b2e1b GIT binary patch literal 28962 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 amu=|i% select(HMDB_code, HMDB_name, any_of(test_patient_cols)) + + expect_type(combine_metab_info_zscores(test_metab_list_all, test_zscore_patients_df), "list") + expect_identical(names(combine_metab_info_zscores(test_metab_list_all, test_zscore_patients_df)), + c("test_acyl_carnitines", "test_crea_gua")) + + expect_identical(colnames(combine_metab_info_zscores(test_metab_list_all, test_zscore_patients_df)$test_acyl_carnitines), + c("HMDB_name", "Sample", "Z_score")) + expect_identical((combine_metab_info_zscores(test_metab_list_all, + test_zscore_patients_df)$test_acyl_carnitines$Z_score), + c(0.31, 2.34, 2.45, 1.45, 2.14, -1.44, 12.18, -0.18, 3.22, -3.18)) + expect_identical(as.character(combine_metab_info_zscores(test_metab_list_all, + test_zscore_patients_df)$test_acyl_carnitines$Sample), + c("P2025M1", "P2025M1", "P2025M2", "P2025M2", "P2025M3", + "P2025M3", "P2025M4", "P2025M4", "P2025M5", "P2025M5")) + expect_equal(nchar(combine_metab_info_zscores(test_metab_list_all, + test_zscore_patients_df)$test_acyl_carnitines$HMDB_name[1]), + 45) + + expect_identical(colnames(combine_metab_info_zscores(test_metab_list_all, test_zscore_patients_df)$test_crea_gua), + c("HMDB_name", "Sample", "Z_score")) + expect_identical((combine_metab_info_zscores(test_metab_list_all, + test_zscore_patients_df)$test_crea_gua$Z_score), + c(0.84, -0.46, -0.15, -1.51, -0.78, 1.68, 0.84, 1.48, 0.47, 1.18)) + expect_identical(as.character(combine_metab_info_zscores(test_metab_list_all, + test_zscore_patients_df)$test_crea_gua$Sample), + c("P2025M1", "P2025M1", "P2025M2", "P2025M2", "P2025M3", + "P2025M3", "P2025M4", "P2025M4", "P2025M5", "P2025M5")) + expect_equal(nchar(combine_metab_info_zscores(test_metab_list_all, + test_zscore_patients_df)$test_crea_gua$HMDB_name[1]), + 45) +}) + +testthat::test_that("Combine patient and control data for each page of the violinplot pdf", { + test_acyl_carnitines_pat <- read.delim(test_path("fixtures/", "test_acyl_carnitines_patients.txt")) + test_acyl_carnitines_ctrl <- read.delim(test_path("fixtures/", "test_acyl_carnitines_controls.txt")) + + test_crea_gua_pat <- read.delim(test_path("fixtures/", "test_crea_gua_patients.txt")) + test_crea_gua_ctrl <- read.delim(test_path("fixtures/", "test_crea_gua_controls.txt")) + + test_metab_interest_sorted <- list(test_acyl_carnitines_pat, test_crea_gua_pat) + names(test_metab_interest_sorted) <- c("test_acyl_carnitines", "test_crea_gua") + + test_metab_interest_contr <- list(test_acyl_carnitines_ctrl, test_crea_gua_ctrl) + names(test_metab_interest_contr) <- c("test_acyl_carnitines", "test_crea_gua") + + test_nr_plots_perpage <- 1 + test_nr_pat <- 5 + test_nr_contr <- 5 + + expect_type(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, test_nr_contr), + "list") + expect_equal(length(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, test_nr_contr)), + 4) + expect_identical(names(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, test_nr_contr)), + c("test_acyl_carnitines_1", "test_acyl_carnitines_2", "test_crea_gua_1", "test_crea_gua_2")) + expect_identical(unique(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, + test_nr_contr)$test_acyl_carnitines_1$HMDB_name), + c("metab1 ")) + expect_identical(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, + test_nr_contr)$test_acyl_carnitines_1$Sample, + c("P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5", "C101.1", "C102.1", "C103.1", "C104.1", "C105.1")) + + test_nr_plots_perpage <- 2 + + expect_equal(length(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, test_nr_contr)), + 2) + expect_identical(names(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, test_nr_contr)), + c("test_acyl_carnitines_1", "test_crea_gua_1")) + expect_identical(unique(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, + test_nr_contr)$test_acyl_carnitines_1$HMDB_name), + c("metab1 ", "metab3 ")) + expect_identical(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, + test_nr_contr)$test_acyl_carnitines_1$Sample, + c("P2025M1", "P2025M1", "P2025M2", "P2025M2", "P2025M3", + "P2025M3", "P2025M4", "P2025M4", "P2025M5", "P2025M5", + "C101.1", "C101.1", "C102.1", "C102.1", "C103.1", "C103.1", "C104.1", "C104.1", "C105.1", "C105.1")) +}) + +testthat::test_that("Generate a dataframe with information for Helix", { + test_acyl_carnitines_pat <- read.delim(test_path("fixtures/", "test_acyl_carnitines_patients.txt")) + test_crea_gua_pat <- read.delim(test_path("fixtures/", "test_crea_gua_patients.txt")) + + test_metab_interest_sorted <- list(test_acyl_carnitines_pat, test_crea_gua_pat) + names(test_metab_interest_sorted) <- c("test_acyl_carnitines", "test_crea_gua") + + test_acyl_carnitines_df <- read.delim(test_path("fixtures/test_metabolite_groups/", "test_acyl_carnitines.txt")) + test_crea_gua_df <- read.delim(test_path("fixtures/test_metabolite_groups/", "test_crea_gua.txt")) + + test_metab_list_all <- list(test_acyl_carnitines_df, test_crea_gua_df) + names(test_metab_list_all) <- c("test_acyl_carnitines", "test_crea_gua") + + expect_identical(colnames(get_patient_data_to_helix(test_metab_interest_sorted, test_metab_list_all)), + c("HMDB_name", "Sample", "Z_score", "Helix_naam", "high_zscore", "low_zscore")) + expect_equal(dim(get_patient_data_to_helix(test_metab_interest_sorted, test_metab_list_all)), + c(15, 6)) + expect_identical(unique(get_patient_data_to_helix(test_metab_interest_sorted, test_metab_list_all)$HMDB_name), + c("metab1", "metab4", "metab11")) + expect_false("ratio1" %in% get_patient_data_to_helix(test_metab_interest_sorted, test_metab_list_all)$HMDB_name) + expect_equal(get_patient_data_to_helix(test_metab_interest_sorted, test_metab_list_all)$Z_score, + c(0.31, 2.45, 2.14, 12.18, 3.22, 0.84, -0.46, -0.15, -1.51, -0.78, 1.68, 0.84, 1.48, 0.47, 1.18)) +}) + +testthat::test_that("Check for diagnostic patients", { + test_patient_column <- c("P2025M1", "P2025M2", "P2025M3", "P2025M4", "C101.1", "C102.1", "P2025D1", "P225M1") + + expect_equal(is_diagnostic_patient(test_patient_column), c(TRUE, TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE)) + expect_equal(length(is_diagnostic_patient(test_patient_column)), 8) +}) + +testthat::test_that("Adding labnummer and Onderzoeksnummer to the Helix dataframe", { + test_df_metabs_helix <- read.delim(test_path("fixtures/", "test_df_metabs_helix.txt")) + + test_df_metabs_helix <- test_df_metabs_helix %>% + group_by(Sample) %>% + mutate(Vial = cur_group_id()) %>% + ungroup() + + expect_true("labnummer" %in% colnames(add_lab_id_and_onderzoeksnr(test_df_metabs_helix))) + expect_true("Onderzoeksnummer" %in% colnames(add_lab_id_and_onderzoeksnr(test_df_metabs_helix))) + expect_identical(unique(add_lab_id_and_onderzoeksnr(test_df_metabs_helix)$labnummer), + c("2025M1", "2025M2", "2025M3", "2025M4", "2025M5")) + expect_identical(unique(add_lab_id_and_onderzoeksnr(test_df_metabs_helix)$Onderzoeksnummer), + c("MB2025/1", "MB2025/2", "MB2025/3", "MB2025/4", "MB2025/5")) +}) + +testthat::test_that("Make the output for Helix", { + test_protocol_name <- "test_protocol_name" + + test_df_metabs_helix <- read.delim(test_path("fixtures/", "test_df_metabs_helix.txt")) + + expect_equal(dim(output_for_helix(test_protocol_name, test_df_metabs_helix)), + c(15, 6)) + expect_identical(colnames(output_for_helix(test_protocol_name, test_df_metabs_helix)), + c("Vial", "labnummer", "Onderzoeksnummer", "Protocol", "Name", "Amount")) + expect_identical(unique(output_for_helix(test_protocol_name, test_df_metabs_helix)$Protocol), + "test_protocol_name") + expect_identical(unique(output_for_helix(test_protocol_name, test_df_metabs_helix)$labnummer), + c("2025M1", "2025M2", "2025M3", "2025M4", "2025M5")) + expect_identical(unique(output_for_helix(test_protocol_name, test_df_metabs_helix)$Onderzoeksnummer), + c("MB2025/1", "MB2025/2", "MB2025/3", "MB2025/4", "MB2025/5")) + expect_equal(output_for_helix(test_protocol_name, test_df_metabs_helix)$Amount, + c(0.31, 2.45, 2.14, 12.18, 3.22, 0.84, -0.46, -0.15, -1.51, -0.78, 1.68, 0.84, 1.48, 0.47, 1.18)) +}) + +testthat::test_that("Create a dataframe with all metabolites that exceed the min and max Z-score cutoff", { + test_df_metabs_helix <- read.delim(test_path("fixtures/", "test_df_metabs_helix.txt")) + test_patient_id <- "P2025M1" + + expect_equal(dim(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)), + c(2, 2)) + expect_equal(colnames(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)), + c("Metabolite", "Z-score")) + + test_patient_id <- "P2025M2" + expect_equal(dim(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)), + c(4, 2)) + expect_equal(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)$`Z-score`, + c("", "2.45", "", "-1.51")) + + test_patient_id <- "P2025M4" + expect_equal(dim(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)), + c(3, 2)) + expect_equal(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)$`Z-score`, + c("", "12.18", "")) +}) + +testthat::test_that("Create a dataframe with the top 20 highest and top 10 lowest metabolites", { + test_zscore_patient_df <- read.delim(test_path("fixtures/", "test_zscore_patient_df.txt")) + test_patient_id <- "P2025M1" + + expect_equal(dim(prepare_toplist(test_patient_id, test_zscore_patient_df)), + c(32, 3)) + expect_equal(colnames(prepare_toplist(test_patient_id, test_zscore_patient_df)), + c("HMDB_ID", "Metabolite", "Z-score")) + expect_equal(prepare_toplist(test_patient_id, test_zscore_patient_df)$HMDB_ID, + c("Increased", "HMDB030", "HMDB029", "HMDB028", "HMDB027", "HMDB026", "HMDB025", "HMDB024", "HMDB023", + "HMDB022", "HMDB021", "HMDB020", "HMDB019", "HMDB018", "HMDB017", "HMDB016", "HMDB015", "HMDB014", "HMDB013", + "HMDB012", "HMDB011", "Decreased", "HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB005", "HMDB006", + "HMDB007", "HMDB008", "HMDB009", "HMDB010")) + expect_equal(prepare_toplist(test_patient_id, test_zscore_patient_df)$`Z-score`, + c("", "30", "29", "28", "27", "26", "25", "24", "23", "22", "21", "20", "19", "18", "17", "16", "15", + "14", "13", "12", "11", "", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10")) + + test_patient_id <- "P2025M2" + + expect_equal(prepare_toplist(test_patient_id, test_zscore_patient_df)$Metabolite, + c("", "metab1", "metab2", "metab3", "metab4", "metab5", "metab6", "metab7", "metab8", "metab9", "metab10", + "metab11", "metab12", "metab13", "metab14", "metab15", "metab16", "metab17", "metab18", "metab19", "metab20", + "", "metab30", "metab29", "metab28", "metab27", "metab26", "metab25", "metab24", "metab23", "metab22", + "metab21")) + expect_equal(prepare_toplist(test_patient_id, test_zscore_patient_df)$`Z-score`, + c("", "-1", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9", "-10", "-11", "-12", "-13", "-14", "-15", + "-16", "-17", "-18", "-19", "-20", "", "-30", "-29", "-28", "-27", "-26", "-25", "-24", "-23", "-22", "-21")) +}) + +testthat::test_that("Create a pdf with a table of top metabolites and violin plots", { + local_edition(3) + temp_dir <- "./" + dir.create(paste0(temp_dir, "violin_plots/")) + + test_pdf_dir <- paste0(temp_dir, "violin_plots/") + test_patient_id <- "P2025M1" + test_explanation <- "Unit test Generate Violin Plots" + + test_acyl_carnitines_df <- read.delim(test_path("fixtures/", "test_acyl_carnitines_df.txt")) + attr(test_acyl_carnitines_df, "y_order") <- rev(unique(test_acyl_carnitines_df$HMDB_name)) + test_crea_gua_df <- read.delim(test_path("fixtures/", "test_crea_gua_df.txt")) + attr(test_crea_gua_df, "y_order") <- rev(unique(test_crea_gua_df$HMDB_name)) + + test_metab_perpage <- list(test_acyl_carnitines_df, test_crea_gua_df) + names(test_metab_perpage) <- c("test_acyl_carnitines", "test_crea_gua") + + test_top_metab_pt <- data.frame( + Metabolite = c("Increased", "metab1", "Decreased", "metab11"), + `Z-score` = c("", "2.45", "", "-1.51") + ) + + expect_silent(create_pdf_violin_plots(test_pdf_dir, test_patient_id, + test_metab_perpage, test_top_metab_pt, test_explanation)) + + expect_true(file.exists(paste0(test_pdf_dir, "R_P2025M1.pdf"))) + expect_snapshot_file(paste0(test_pdf_dir, "R_P2025M1.pdf"), "violin_pdf_P2025M1.pdf") + + unlink(test_pdf_dir, recursive = TRUE) +}) + +testthat::test_that("Create a violin plot", { + test_patient_id <- "P2025M1" + test_sub_perpage <- "test acyl carnitines" + + test_acyl_carnitines_df <- read.delim(test_path("fixtures/", "test_acyl_carnitines_df.txt")) + attr(test_acyl_carnitines_df, "y_order") <- rev(unique(test_acyl_carnitines_df$HMDB_name)) + + test_patient_zscore_df <- test_acyl_carnitines_df %>% filter(Sample == test_patient_id) + + test_metab_zscores_df <- test_acyl_carnitines_df %>% filter(Sample != test_patient_id) + + expect_silent(create_violin_plot(test_metab_zscores_df, test_patient_zscore_df, test_sub_perpage, test_patient_id)) + + expect_doppelganger("violin_plot_P2025M1", create_violin_plot(test_metab_zscores_df, test_patient_zscore_df, + test_sub_perpage, test_patient_id)) +}) + +testthat::test_that("Run dIEM algorithm", { + test_expected_biomarkers_df <- read.delim(test_path("fixtures/", "test_expected_biomarkers_df.txt")) + test_zscore_patient_df <- read.delim(test_path("fixtures/", "test_zscore_patient_df.txt")) + test_sample_cols <- c("P2025M1", "P2025M2", "P2025M3", "P2025M4") + + expect_equal(dim(run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)), + c(7, 5)) + expect_identical(colnames(run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)), + c("Disease", "P2025M1", "P2025M2", "P2025M3", "P2025M4")) + expect_identical(run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)$Disease, + c("Disease A", "Disease B", "Disease C", "Disease D", "Disease E", "Disease F", "Disease G")) + expect_equal(run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)$P2025M1, + c(10.94172, 0.95343, 12.12121, 0.00000, 44.28850, 0.00000, -38.70370), tolerance = 0.0001) +}) + +testthat::test_that("Ranking Z-scores for a patient", { + test_zscore_col <- c(1, 5, 6, 2, 7, -2, 3) + + expect_equal(length(rank_patient_zscores(test_zscore_col)), 7) + + expect_identical(rank_patient_zscores(test_zscore_col), + c(6, 3, 2, 5, 1, 1, 4)) + + test_zscore_col <- c(3, 2, 1, 3) + + expect_identical(rank_patient_zscores(test_zscore_col), + c(1, 2, 3, 1)) + + test_zscore_col <- c(-1, -2, -3, -4) + + expect_identical(rank_patient_zscores(test_zscore_col), + c(4, 3, 2, 1)) +}) + +testthat::test_that("Saving the probability score dataframe as an Excel file", { + local_edition(3) + test_probability_score_df <- read.delim(test_path("fixtures/", "test_probability_score_df.txt")) + test_output_dir <- "./test_excel" + dir.create(test_output_dir) + + test_run_name <- "test_run" + + expect_silent(save_prob_scores_to_excel(test_probability_score_df, test_output_dir, test_run_name)) + expect_true(file.exists(paste0(test_output_dir, "/dIEM_algoritme_output_", test_run_name, ".xlsx"))) + + expect_snapshot_file(paste0(test_output_dir, "/dIEM_algoritme_output_", test_run_name, ".xlsx"), "test_excel_dIEM.xlsx") + + unlink(test_output_dir, recursive = TRUE) +}) From cf9f3493759d9a57dcb038bcde8e914a9c6284ed Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 21 Aug 2025 13:54:56 +0200 Subject: [PATCH 058/161] Fixed snapshot issues GenerateViolinPlots --- .../test_excel_dIEM.xlsx | Bin 6754 -> 6752 bytes .../violin-plot-p2025m1.svg | 4 ++-- .../violin_pdf_P2025M1.new.pdf | Bin 28962 -> 0 bytes .../violin_pdf_P2025M1.pdf | Bin 28962 -> 28932 bytes 4 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.new.pdf diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx b/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx index fa6d905118380c37c24a2dd07c5009e9e5c1a20f..e1a75d59aa1f5b86eb270ba6265b0a264a876978 100644 GIT binary patch delta 842 zcmaE4^1y^Qz?+#xgn@&DgCQwSbR(|!XK!-ERsN7UbeeI-fD0{fCYURDBd>Ntj)6BOpPl;Nzgjx5yaNnHDn4k6TS*D7A6}Kom zXmjb7Bp3#T8B~2#YSH^ADKYhAWlvhs*V_0mn=ek6GW@lxu0rbc^J9GSD;rFlwAV3t zlq}>FsZ2UXzNchEKnUZO*3T%FExwie2(eB;%*LVa^ zy=r2kFJ5`ntmmWUa@+`j0_BFObiStA-Y+PRSOcN z-fW!^#&vcf2;&V02NO87zjO7of*G9xhaggZLb?z}o$wZBIbZ}tC%n16^%VmHgE!Cy zZXgW6@W_ap|Ub`_?!yM?#DL}pIDB5i%PmnN^2f6dw@16~p zfpYtR?i4{$b7JyS31zSy+>%nZjdwX17%Fn~i%TkVQj7H}a&ypa>zTpN8pz7PuvnUb zK^VoP7n74Dm101)B^io5?*O{!ATt94KZ=?v0kHAq`9;}D`T5z{EGT!6_;4SnDFx_M nWfaZ!lOISLfxR>B%oL$~py8IlFqc44lRw!|N}EkZ93%(;2{r_w delta 840 zcmaE0^2mfYz?+#xgn@&DgJESW|3+Q~MrI(r*?@5cm@)Y_(+iNmNzIe_}?mtn`K7q{=Hi+?{e+8K2-hwICuZrzTC+*O`lJ_ z5Spmt&Y6+83;x+wStECPxfj2@CPSzXmzK^3nR9Z^Iq@lX_VN42KhbW}XJ5CEA;6oRBi&%pK}kji1`Q?#ARj4AH_NbU zK?2p2trNny$}R+9yyDcs%h)XduDl<;DXB3@0Pn-uVwpLtD*sw-4w}5fnA2CO?)?jsmHXecs1=0ccJ-3j+f$ikkZz3=9=H`o$%cIjO~Z6}dU+ zPUxAz&l#NBLl1gAFBpHf4?*JNqkePvjA4N^g - - + + Z=0.31 diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.new.pdf b/DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.new.pdf deleted file mode 100644 index fc5c3d6b710f85266785259b8e01c0c48e6b2e1b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28962 zcma&OcUY6n(VkM6SQAA=x9#n`(Z^@&mh@eprP${8`2pABM04XYuMn#N*fJliQ z1SEjcBqSguAYu@NfRrF1v;ZLq>FvAyUGI6%b)9p4=bV2gbI;7q&d$#6&hCA049*@q zVQgw*zQ*9z*e%vA`mG+H>uXGR9@>cvyt>BTehtL&dPqQQ81mY&fY^|oe;>0weCV*n zA!}0$Q%iFTQyZf-kkiQE|6ktuu-FKol-o|H>j6<$!h&LU!sBA2;$nefXRjlJCl z%>V(no&U3d)&ExDkK7;W>mk=-|4SU=h`bgHJjU#_{!ej;TSyEt?s^cQ&-{PU_ebD= zxWhH%T4?N*ofd~ocS25tMZ^LPfSiZ`d~+-$2pJsm$LyF`z-5tZP`ND`=a9b+{Y7Ic zjpjG#Ckpaddq?)nU0Q!X%zMx2OTC(FrkC&U+VBaZ^bLC9%vt{K1iP`Ixk{myBIS?# zS1pf`e4zAhKl{qG%0Dpm+;Y#?J>7WQJ8@bt|M$?%Ow!6jMo(MMhZnX6R&0k-J^#J* zxrirIq%3Z1Ld3uBjktYF26Ob^ap9J6(qy|}I- z_gRUafkDS4J2;P!f|hM$`==(oHvYEydS@o_f~zOz1YFnUm{a~%UargImLBQZO-@1D zQ}>dCiK++_{@sr@&p38Re^hw5myc%{Y6_%3Y=o)J-)`%AfS#CEoUTcA7ECqwZkYer z7`j{1J%W*gEXF?Ps^kA{J(~OG?`)6pYl#;Tcg+_)35PNaL&pX52y(VDt}&39zotg) zSZrbc)t@qi{+CoT@Td^CcA#u!v1;i@{X5z~$o^ON|5xzZ{CDj9kKp}3#-iz=|64Hr zN8*2pMpIJ@OSAubJig6yAsKggyp>OSmAnf>ZQ?q;wyi39d~ln#kITtBS85)f8?d#! zLvkrpVOq8ZJ!*@y-0Y)~22$nLUOsWT_2D!6kW~253#$|f;Rw|!b;4W1(?xjk3N><8 zaju&KqGFYu95F(rkW*DDL^4*W%tAkU%aBu<>aTd5Y?yjoQb$x7s2Eb&Bm^gqCoRh4 z5J01V$i%jjhv-y=8lq$i)9o6_GN&BUbJz`R#*Jel#^2Mgt8k{Ri{&l}_39K;Rn9UB zS@oM)cYpB*CuY-h&*`V+ozC63P`!HeQaztD8+mJ-D_)C}H_lavQ6n)jiwj}hYM94M?cF_1Y zb!2c0g;@5>>C&Ilb^ABr=Y3Z*;m@Q&Py%#e?%zzg6IlFj1CrQ5J^Y@2(#DjCGhKhm z1RPg?$%ac>IGshX)S4ueKQfO(j(6XtC0q`D_hGZWS?vu0wvudW%X>a=S znR6|p+>Y5dxdr{SfGstxN_gS|Dh(wkpzMXOEv^fr$t=O+K>N@w2K?I2(bH$3P}7mS z`2Bu)=zIbyh&&nknRpj`+r`w;@7C66zwsH?3RWlT;MlLWPTKa?B=ccf((ZULHzgOX zSoh{_Dr0wkUM>cm$9pF<4PQw3Yqa89C^>A*JPPk0<=eQ8zw(+ldf&1dsif94iqE^z zn)I_CDcDVuk4HqlmtsVOA8jo(!&Yx4R8IRkjw0esN5;ehycZ3o)Rmq`qxJ(DDkd2X zDa&Z6@5dTZJv7&e<0g9~u1`PL+TJHXZx*xjG!bH1e`fsR(jjB+l!kQzda{LqnTS~r zl`S!`#f=^5`^axrWOr#sMK(@0f99g|QBOuu;7zh6XB=t*$eoOwXD3IK>X#;j{*sG> z(mAX0w<3AU(}1Q4R&(=`(01=Ge4>bqqOO%Kc_giR0g5xEtM6#8*TVd4jj?W}0))te zsxg6oqbZ^$>SrZ1Q#z$llMUELBAfl-JSW&6q~TLYgvjvq1SG5F7E7w%qB~xL(0Y^q zA!K6oQX7wAx%e_4;|~(-j_+s+#MVf}{fpo8KPluF+dEkovvV=3FQ~QG+{uQue5nrf zWZ9x1sufjF0h`?M!H@&Vp-LX_bKC0`j$kdF4m|YR4spE1)x50lbyF9XnkIA9??PCz``I5bta)5mtBAEDNV>Ib&WnA`X~p(&M4E05w)-i@&2RK@Y2g zo@*knR|6`3k8vEqmy=pGG1ev0C`(U(;+Y(?dorLEb-?fSR#}vFKH??RmBRv*1awk? znlA=YZd%oP>n{dmJ|o=Ov;vI-VXQq{EB+`;!T{hIJVVs^dn@iO^xI~66Hv~TqCSPO z9#SL$Dn4kA9`BQ(YKXi?bG)`y_V(X^7Idz|So@eD`5Bs+tsV60b6~CTdKf?}5zZc3 z3^)iQod9ssTt6k8p$6sBc#6kG{EewPGGbH@En!^7{S99T91wZ3SZL_(Gl}| z6M6v)4kl@l+XetckDk0eV2APSe|&EW9kI5_wz8REy@2ZJQ=p3h{UdgNgamPC!6(+r z*Bwl#aC)TF`a?-@11L!gQ9n>0=_Fm`6!Q&K?@Z^qgkDbjQ|NgP0{~1e0O(XBOyM)n zM(+rzH$PAXvA+;SrW#;sIP^5=&wi4YJGh9+O#|e(2AINfg#NrPQXq9?MP|Y*MlK*S z7k`(=yU5Pg!;GgLg{m^%XhS(Q0zebX-hz4s7#DB^30$g6ntj=Hn$KSY6s{t_0}^S@Dp#h3h++ZO8Z4CvEm>dI5#Q{Tjw-hvD!;y-lb zH*(znKv4()$D{zc$`IrZzTTe#$W=^wWk7@J#Qm7Ie6WY?u~L<@xC>wJ2|ziu25z0MgN0b&5e0PcTSh(9nr=~oM8pPFLnXX zG8rbAlUS&)?D3cUVgLqGS0JOc0G~l&u9%Y&V#`nbxSZRK-z>T^*Nq>S_5$W{Igg=x z>BL=_lN9DIOqtRXzqsp;^6WI6L21I;tiwzVa&c#YxTb(+H2Q(z5G&v0p<4Vs&xEvx z1CjW$XKopM#*w)J_eWL)E>=uaf-oe00f;6~hpqqKxSr+f2Qd`>Uf^?Ig77TtI6QtZ z$3qocO?79(#cX-@S-q$s`YY&7_d~#3F+@;IH&qae6w}v|D!&rv4~Skjoo1ywxh(M4EcQF3lOGR)w@yna!_@RU(g5z#*samd@6r& z6S@5t92f~F$m*t__ziNWblC6?+!a73)5?G(RlMbS5~&t`1I2BduaKX^POD-IemzKO zEf$vku|n*@GPbA*Be(EB1Eg|*~#=eBL{r78nTpMP4cIOb1w(%NII=&6(cyGXDf`Sq@s2sk(}ZI4wIAR3FAKsACCE zs}_4F_u+J9e&4#8&$s{wgZl$u{DN}*?9dWKR^`aP4)06gkEH5KXsdj_8_yOS;AExL z(<<{mPH?9EKn}jRPrzm-;rbhW`HQ}p7~v># z|@jr*KLxdR*Cqw4-boDZOzue2pf)@)_%_>HJCPzOk4zu|;_q_?DJ z&+J^49^!#8{&!=tHhl?%zsQ}2Swly@5N{}o@4zCH-rj-Lf5{JDH+xuhx%2@uF2-GP zE&C>_v^z6UzRF&%mSMTIn9w*mPSGq80ldh7|Xc(kSQv2}{+F!h@b;t55}bDfJ^ zbyW7lf)T+>(7ozXe_)-OPRfEgcsIZPtO#q4$6#8$y-TX`lm29Dxws2IjXn+kso9*9 zp!QWQE@FGxaB!vrlMRPcxhA;8@BKL29XhSFcwp_aRsUqe>DuzsfHp=9W=dmBXn~^8 z1i;di$;dfF(;6~4fOsJuX2VH(g4bC&xWdM7u%~5oV6o0rvd%aGD|ulD(a&PG5m0=Q z2pHWPh>er>6t}1bwq5auwgNN23-g_tfH>=qF}lA&0JlTrmj$aQtK(L_dWQJg=pik9 z23hy-tvUiACfEgt@v&WOID{n5yKg&ADVLa)E z`Ankpo5pC8GNvB7gW8K~xWu#=f#OMY`BIuNfr_CxkOcM(ukGo@FhK@=(@=+i+?WLA zzNS}CX6PmeCo!TNQn!31oK7=apv+RkDL3mTY1wd00<{~hVMQmIfg3rIlpEjRbjI`c zCjNIcxA=~7FJ?(Rooe3pfa*-N<=@ZRlt7sda)Nf#LJi^tQ{!{OH$mhcdNOCJbt_Hy z3aFUlOqHJhU^U9-gg%Tizze=Zud7G%zVb#kW#Nkm^j$lz`R&7`8&prJkH=%kI z(ds`1H+?c}7!<8p3_uQse{yQ*1iEfE68B8B4mGH$Z#uRNj!omE8D>=>c&T^U!DS2DYN!tYE+R=pb?~khXs#!aI=BQ z7RZk>ltrlZ3C}D>*i5VTK_qWy1;+(f2aYFEP6UOhNEQ%PK2AwT4)Q0aRjK1gHRqdN@ zx}n(V4E7~sYjvbOrVTmwjSy_F);dGN0?jN3K1;H@2+E%q)c6J9`3v*8* zse*2%;T^#h1Uz)_A0&;{bXr!ogL>@}sEXv}h%+Xg#1*hZrn#{S+pzj=`nc}Izoe}O zm<8EOlpRBd#wqB_nGxXoa&o{^y4+4sY^s$Vg}IkIp;^?LB+xMEb3#DE>+7YrN`#^b z&9uJ3X+`TbB!|S`;X}*=XJ4Y%2@d?kPb+>tg?_t-FkfRqyG!(r*eU6?3LAwOX>2kzr7uM;G@h`z*PqrUoW{SML^SUW5|GUG@PBBUoP z=*SA4YtSdMt~~f`H(p;}z<)V9%iEi3m1OVCPZLxYX2C_w41z({XjxXKpeX<{thnCq zVnrP?Kw7H@g9+;qq_6rMioWpWMR3mIOB*Wl4X9M?4$Tqe3RvE(58sK9)M|<3Cn`lS z>B#E*&IP54CpdNE^^=@ND+Iz~BvzoVaH?i!lsOp_WM>_4MT?EX6Gc`#tNmvjD=C|& zla+?OW{)wd$4Yw)RM@C0pD}1FB^2s%KhO(xk>m1|#h12BU!YRBn*%L~P&|^1<~W*< z-1q|~WRKtTEE%MAsnxaEx8dY+)#Yi&^>Sz7K~^8Bc;wEkOj{1pM@T@uthMTw7XObsIs2pv6GotX&pky+Wx65eG7 zC?wTNo(Q;$Q(V=Wf7&RlbPhm?&lXOl?~x{69!z>zv`) z`mM_P>cqnpY=d_1($GP^VHRf~Q_;?reKx`!o-xP*T;WoL-&2Pzbs33L`t+b^-t{{M z7{$8Cc%{z|6cXdt_XnieOF2cFXa%Bo?QPBn@K01RN3)>S+n0I;_e+@i8=mFOEaZ*I zXzzIYI!J)yaBrYB`Gx<&@_g00s!tsJd4e-e{lPDNHnGz{jgz2f93Kf^{R^9u904*F zm@wa$cgLIbYcrxoSoEjiUc3E%C!U{a`cMD@5*e)NLK6yK;{whlxH}lL_LNfJLiaMf z?!x9|c^g%qSOKA_G< zI|$wE|E|?foUcZu&AeCXG4PqwtF@6Fkxs`5cVwW-@DUP^qEibFMZV!R)SFZ#JW)HFemwp{;d z7_&>0h8{nJ6R(6d#?Wt;% zo)8Az;tG3$Rg=@)U*iST17^5bIsFBw4gVRoUUtZ97CvLhu_6{>&7x{^pD{08p3;z| z{5e2q=cn*Pn;7)3MD3VgU#k-oU7F*pA)F@WS)Z4XwF_r81X_i8RyFyP&InDAt#;b& zCDuF&zI+2`C|}Vy3@qk7@@(09xht09F0I+D%9Ux`@s&vB!_gV+si~$Ya#WeG6umA4Z_Y(4AM$JwC0Ms8EXQh& zB~o`2=c|eTD2tT9Kqpo*6C;Inq9+cBsS9FnLJ4-@QR3(zET?T)N$SEsDIqQvBvA!< z5422xozGNIof_Q-ECWv(j1Uo?HxIi3zgWmmuXILslA!!vWTugD@!>3A0)}5_ZCL@g-C0 ziu-IuBlaS-N`YFh%tPF%wl%f59kQ+=$UCg~(Wi7d`z0tc5&PrER430s*pnQDHG&Q2 z63@Y)HJ@Q%SywK|4F;=`CU)T`Pz_c}HG--jG2l;=Pm*#k#E9+KhZ^>Xu8i9^KS>UX zGGJ~!_xdn+44m< z?r#c87~D3~`g7yDZ{BA;Z%+56?8t@N^6W=2#k*=#xj93DWieuu_-n{~wHxH#b@s8> z&PfZ-pZXX#eBPQikc*psl~4!R?R?9?m^5k0-Lh;Sjmmq6f7pt@%xNt@&7NCP_py78 zsH>Ay&EPM3+UoZa&Ro9XpHS|}SHJiD{i;&W_Qd8^X)gj z34YR5$0YvJ*`Bzcd93Vfd**z=1zyILUN7GNx^S|4(vm% z-Lvrb=8Ld6>{;a=tC=qh{>;AI zHU9kjs;1cD^Qn6M_j?S%=ejENYwo+{HXdGT4K`XB>bM>eTlTkq?a#Z#1hFzDu;5O|pEy__4<=v2bKzv+r%DyIOlD!jMw5^MKLJ=eJ2jfgp8 zir?qhWn*{PdYc=4;K@RJ+o|v0n_WG3Pt00et~k6qG*q?ExFa`jTZSyey3j~MuJ_T) zPHw$5Y-E#t{cB6c_}|kj>bm2vPmBk;p!l1tYioMIS2b_Ei$5Hal|s!m9jHhvK9IA| z`^5FQg6ZJk!v_NP-X+gKIIf;s*fXxIH=&tN{5u=VJ{Q?-+UL+Xq5YAzf5wW?Ul#IipxFx@z|w)Xl86KTNaq3nQ`xMERoFigDh za%b?HUGZKrle>pvsx#he?}wgW)CHEiN&n7@T11AQzy?~dF{|LfdG!GRh-I7n86hyH(ftL@0AiGnZe73(q z2Hm-Ect1Po(71DC%VVn@7aiNf>}J0`gc@n|$r0!$2~|@O-a}LT#Voxoj#|=d3&qa) zdtMhik385B5Z4|v$r>}84E@U)W_{wi}tU%LlvMWQ9)oY+=)S`#_3u=D}b^#7anc-`b3}%g?AbHTUq@d>`kE ztJhrr$PFG$G=z}z%^VVSULWaAa}WDdEEt@?qTRNuYyyeD<1M(!z{cU>*B|Lto)&kN64yBc`2;9Vm0HK6)WbKVJ8 zKALPisfm)Kzs9^By_Qyuruy%<=Qwk+AYrMSb-HhHl3%>^3u4s&8XGp1MVkUqkWC_**Ab)({$?0axZ5^}OnM5WcW#z7eR?2GOvK zz(Jhu<$JExN&t4yw62}>(QV}0)hHLJ_jsF4&C0CN$JOCcZG*=B{8&upk|$14>+>5O zP%G$ga@iNcO4 z*PAs6*E8TlFrK2B+17FX6S4*~#=Gy8SV#P(sa&50AA<5E&7m(b`N+$c7q**7=PzwA|ZI%kL8DT`BF9Gk={) zqeIOK#^{fcKC9r;5e6*y^X<^61O)8K#hyfqQwx8^tFbFMQe;12T-WEedsJIE6gF-_ z1oJ;3{x-Yh6Smak5_Jmh84_3v{xG`1CppX7#qaLR6W1DdL8PcQ7pPX!&pdPgUrFb- z`MEr)*U7s*x-mC7=K|L$TUAF)cFfO5zRxUBmY(nEQ5BwA`D>FMYWd`-!6P}c0{psL z*QYsbxx3$a&tyC*JL`gAeTJdDJgedOQ0M6qaq+2bQFDzhdaUi4Sus&h>bKI9`msKH zQ1=ROVA1P|pzJCzOV%R_vL}YgP3fa$PU!^qkp*GqcuUrfqW#nc9yW8fUeb;5c)QJd zVlYj)RTOLNEeblXb+Z_?IBN2UhiqRf-$8C!B<$a>Tr;N(DGeO3vuMXYblr@45JtRR zeL;-)IDXvVlm~n71)1+hK=1FO{T6~pkPo-wGpMgQ+WkrX1ECp47t0oF`J4QJH0r4i zqIEgabnf@1;_>4lTWcP$63T+_s&eSWzzbU2-Lw`fYhKD~Lvt zSH+rg7phxIW;JMX{}%`AdbEdH+FcWVBW_n)=hM>26O)abPl~c)8-0h87B2;Cj`XTK*bZ00B}boKq` ztc+d!_*^q~=hW=d`0{*Y(EYs^xF1b1tNLU;{TuU@A%0QX;ncIC+BG=^H8O?qW%8%0 zs|ks?)Y{X{gh2E+ax$Kd2%OejmOA$NwoBp|+@hk}%+}#*@LTPb+U|bAdf}{Ny9-8l z=TGeGQ|Vlswn0FwMUr~o7O_*X3PttwfWMvWpu?iuPsrTq0o>1 zAEAQ071?t?G`IKzrz01l!j!McW0NkJpj+dKi%(WOQU37SVn5+Tbm1lL!h|-y5>iu& z30~2n=&vp##F~+^f1+TV4QrP$`-=ZwsN-x~Dx#&0{c3^!*UU zHHnzx=$%dmUL}l!XU*W{v(6vH2X~t7$$!l1IA4w^8x41muU=d8?Ws=Q*>Z6C=u9Lv zaB1%#w)OV=#o73SHxI+hCvrqCTMlL$k$^3qGIPaoq}+7v-CrLOAD0xuov5J9*F+3{@~v_xSOIQ zIv;Hq+V4FYpHxzlet5PrEpzg~oXx_5^y1K>A`Eo<<6Qgd{NM17{N?aLf`^{SBaIxU zeki&c9qQlz(V!)3^Ounocn5gW)}hXeda9;0a)2D;oIgpNjV|lkKEN6M%?yqHNG)qg z#PT&C*s;DKmf)381IYfRUzkg%@;#&1)FBbqKdQT%dnbWK)4a zCjSuv&9YYuK@(>k*7Q8*_j1m7Rn=5 zG=%beBy8oC>T&73vNqBP>f(3-(!NtfW<}bJRI{ROW^HJkSO3hm3U(9YSs32i-`hbC z6K`&ZJpBH8JNzN_TP!Q}oY!^MlPpe#*KK2nrjz0mdJ_o4x|g*-f1%kO;zafkF*XtE z_4uFi9e6{d+qw06yixnxgN7zVYfO`szA+|otTzjIvap74d}}!(X2zH>@IW%ccI#;nwI$|Ei}NBCgx;jil}r18cr%KI}UKMX@0Y zT6e((J<^?Y#AJ#+GY?(?wPprmqM=`9e zi|;yhXU|B3=ZWk%L1%D#P6c>)`V~5^Hm~$+C{t}=bmn@mMwee=vCazzcH+`sNvaOtBC8je30bm{ z95Qp{`K*&VWMi+{Q+dd;Ub`+o;LK}|RybLhDfWY(o6picj|8Cp=6fq>0^@6Qj3 zTE9Bx;y4ba4m*T9M<)d77lh75hP9%kpE+HW_vIuG=-t ztC@fX;1ufJ5XAU>{<}AG;*4OfwBvXY?UwpY;$OX-F6JqcH(g_-RDDbNEU5l|;?A;} z$@7rqxU<50MTUVICo#i`dn|KnpzrV1(ecV>zNd;3-(4B46`ond`$Bq~&rvN6=!kvi z9JIH8T1@KmE)R%U3F3*O`;!tJgqO;|A35&lxh;2(G6+Zf?n^>{rew&o{`P>kuKc0D zE}=zMlxW@>WA4{iYzj38vVc0(dQs5yxho$Mtu1fn7$Kp;8&9iW=^q!~Q2f*2?Vt~f z>bf1koz_^7@TJp0t~L69iGt!L-^?s!C4&*ZkTy`}H~+5!#3i@-U9dwe+426Q-nEPgl}e5QT6-B08(uQ$G7?D>q!Co~J+f zzPwlG{Yd2yRqtiug~e6t7_=`~y?GxG2jdQbFMMr6Q;1^Cv5%r*VO^QH^@G!82V^bc zlIQ5w70m9?UX3hyR_}Sp!^6{ocgYuhNgau->l_;of~)D}{-mC~`J&L)NI1i>3F}kp zblxGhcD->?R;xLulyKL+@YZ~muKFD5d2w8F!2K=uEgcUbdvwHm3L(o&l*c0Xtoj%$&OPggu|5B;rtbLL z6js7CWbWzUo!1aGM?)Jiyl}hB<3{zj&Bai+xx$Ypa=8xoupp=5kMms$c&$G@U-~eh}lCe$Kesu;4=eY;@({rn6J3edzW&6xB1i z#|oJ9*3UQE^w)dtg5<+1><186JI5r$ZFeA{9F4<>;r$D|=IO8sl2m0;T{CR3{wG1jDcacI=Dxo5{U=b@oX)y=%O0;2>~2Wh4< z<d43PR)Eis#Y;8gz4;FHwJ*e-&O`4vHp|b|Kv<}5qu;JEw$lv}+FI_9B6!|m)8*qdT!$}9JNpBfUTsw(4U0gV$-9S4iX^B$^ z@Ce+?r*Z36qw(wA9{IVS`a%2|<^ujly_iE(jIC9@@=mN*%atUonoU=GC$Je1)$bN= z<~un-HOn|v6wPA%gXPsR2r3%R#`CP|BLjk!v#F|OZn!q?;FuG3F$s-mqdF~L8_!py z*aaNvOwfm5S*z4j?R)LBzr@1Xuf%m;qmSotn0g8;rU7wbVcQq6nQBG|$MJDd1x&RN zp%@nybEz+myd;mPdy{h$|6&bl9{J`{8610mK@ThDz*Kx(9|I((BK5`8ByIb-k7Fan z<1pTSO2{x5PL)J`c_p^GPbZXh`sUM9WpR4BA4pzOBa^C5yq74RZE0cI7OD1oNoM9eT(+jftD`jA&1xUL2zDxk|*pyD48S@ zLdgf=L^9sFA7G^l5bxyDqtx@l6rxg-c&3yHyr?0OZh}Z^234g}qm?W%2`Bj&^q7oY zB;Zg=E<(k#qNe~zb+M+n%W#T|9|+58{&1iou? zcWG4%Ge!hLnQD*$t5-=x@i2vqi&M!a@YSD4zNASen8ITt(85p?oG=B~fZIXB;pCl2 z1cO6lc5|}iL>SosI`4&4bDgO5SghZnPLUt2_>DNjuXf&Ouy~SF|5Ls2h#~(f6)IBV z6)Bs@^;k90cq|dg_G=an?7sf|ne#^+uyw)G=Y-R9#nnTCBwg{6M%vBmmEy|pH*eCH z#?{l&puf;s7ufaHNjFD>8bV{cG}{>}dN_epORbLh#?w6)+L#)8%>U5^Wqqcg?It~h zu~wa!X)Mt@#F1NYm)@p(uy479dfcWj-BVAH_h_Qm6~3>p#;#nhkocZ&9$X6+*6B*x&5I@!jmOoe)T7Fd+xM zt+mIxmmz;I9|HAB*u11k1|{U#WbyqP1D3P><b__ZEvEzl;KJ~oP06SP?F@~Z(1ZkEimzD6wxT$!N{80Wc zuF!u@JTCVtAU}JK!%ZC>ddJ&sVieUa8Koap4f9X&{aL#c81{%KOFY=opL*2+p6 zoLR{#0K>3&7iDjBb*71?%hGX9ZMab*tU$64n~NomGGEYJyA3BXcNg2)oN)wa)97*g zaQ_8}C`VG6jZ9O;tX)#GWsLpO5e=($T0d^cx}lR^ zpr~{xGX

<4|k-V`r3Q;K26nBv-Nm`jo0c+=e=-(n7BcSU?g-z{x#&l1FPUj5jc5H9=iRZMvI+w|N`QeBb`gk-ay;^Dc2)kF(x#4@fk0tey zu#_96Vyya@YDH-)r&e`Lm1i^_%08yR=Hsp?2k77r->MZx=~O3&#l|LAK`!EEIE1S$|O{Ub~Bn?s>MQ5T|@?y1L&?yQCs>wGT* z90=yc=UU6N9<37Mqn|*Zs6Rz#s8*{A0WY)Uo_r+nCBzB{N2U}rMPDa?XTW-znElUG z59hK|8>vp}WgY6(s>pHXJgTAfH@!{2^t8%VE1@bnle%>gAY(;Ad(@&1MejJ>CqUh| z50`r_!hgVVnCqCdP*U9v2CFEiRW)-Te~lBWa@B)Vmk%%Q+UYtxG$4BmX2RBsDtJCY(+uwHt$I8qk-|~Tu>~H3R zR3Ai!rW;x(7u3aL&b=x>5BvA~1UlLYcP_oZ6#zOKDZ?%xZ4?Eljo)(hw<^|}eHSzm zjY6r1yOtfqg$4B|M;$=JB-Q=Q4-X6*PcNcJnb*f{STWkyRERYCW#4-DDU++x(m zFK#VcS$0*o4oR%4%fME5Vd*(S!5fEXI|zjj3`68B58j(xW5{OsS100u{HGu)TxJ$F z%nAU#OQ372PVX6u0|b6HVM`wvzMTk; zP4mz9tx|SfDa`0)DRo)SA(awrDsS)d0=jaLXmkMYu*%+2B-joxe9MV{etxMU9vWs$ zOOCwW4e8;vrHb*!A5Yk?L|yZl7w2syHc^n~%h%3oGO5z)e~97sKnYhWWZU})r4jDU zk3&YU@ebWv;rm~&gjuzwfBpLiw-x1fOMOlQiCcMYc82GDE-OGC%~FnqaKHnw)Vmi3 zWKN1fpa4da7nJgMIE z*g&-=u{%mby`EsG_KVu{FK`2fxDCFH+M!yhKLj3NJn7u_-O9W*y^NYh1Los(>N=py z{D~Z8;np(TTllR?=3ByFs9zjn=VGw^YW{A?6W-phIza3U^9vHuolm`v5LPx`%w)x- z-i3@0o6qk8JLoIwJo`0I^WN-1ZkXEtlwOY5KG$eW&6-SxQP_231ptTBwx=aMO_}iy z)!k=RQJ|}_y32`&*=l{<%~eFQT(jscR4mZamC`@_EWxO=@|QhWkv)sDUl!fX=n*u5 z>3e4J?@(?x6fcbO&wm$R_FpYJ?o%*!$6=%5*pai^H`bA_aFxlfoR58k}vc6|Cyoj9-Qdtizfq_)6af*r`yxaAt(jI-im&lwk=R-lUzP!E9 zmJXo0vFnwJJiPhok03i^;-!v@IJTdtzztn z4rg2kQy+#FDI=i@z%gzyiV|bFS*Yrz;@Cn*8Z1Z+G)&uq_=G-ZelVN6qt$(pyo?}QpgldgTd5?! zR~!?gBxaqe@1FMG2#m)xps9r!8?3_=Y@RfmKGiPeg?&??R%5x&(ruFmk`Aj|6Sd)2 zMfkcg=T2U7>7OLeUh*7KlLL(6H zOSt~mPYZlV!p3e%bQy&kBDEJ{QrLp2SMXZ4!?d(Z^9@y~DYY6P=1eg8T~-Y}vR5~$ z59Zp9R39=6+!fw?{B~j(ZUWgm34?!Dwh?s^3|YuxS#?)O%DKa`TtyNEB#avrxqify zgNLqvCLxb7sxUo~;Mvr!3igxPJDEF(=?*{myy`1R^L{&h>fDn|0tTM+mK`GN$q)P6 zY(daKJ()8|S*G4UCvO+IIX=I>d}RB}T_0}DlIpSjc2%SX>M0j=MSb7Z&vTFkq=U+I zuHN$lJCkEDPOqFJom72ghKI{A)1a!#2zc5CIa^Y$Xs0N% z!_=UR%q&j8F$uPoa#D4b`W;&U8zfzpKT};}s?J~~O$~zI5HY7;yjUR5EXY#+&b*hy zC1s!nh$dU9*VNbpZlepKQ(R#5W|^xbVrLc|oz+T|wCvkVvl}tZny};DFPXG~nwnGW z$96O@8nd}fFmL1NILb9oq2I&`-+cy4?3O5>N9QX>I#@Bgp@O%kWanw|rAK6OOu}>D zE(Y(dhqIcE>ep*#+=uy&XK^-=mce`<;e=r z^&EPEj#X^s>qrmKvI@qh*(m-DUk)A$w!Tq@ipUodl>h#61fd%l$3I$Pfy9Lr`FK7U9<^1khcTt#oOBvT!^o z8+4^t81Yr-(^cGg>GCOBI4cerQ%VNyfwImr1M{|}5PUaIcnV42I6 zlw4*IbcKv!ja!!VSBRM|9qsCv{a9cJq(A)1Z<7s_sqa=mo>Ue86m;9+Fj%^~|3HTz z8P~A~w?buHYIPo3^xjpJF7PPB7jc0~DlQW=)e~44Sdo(>a9kIwK~!i7en>n=b)QJNYlFx&(5fFwu3~A?Qp*edX|7%Y_Bk7dk4_HCgYok)B14S#H`gy zzgy|RW``6#iEN~L#Vb#y!+P?Nk^Imz&`<5eXv`b_k4O%I%>(wZ9|5oURS}+Yuaz0* zA`4(a(hul_x-%44>0(HNu2jU9y>6o+m4_d00ACQ4>inYs{;~kttkZW0_g>$>R%JZ1 zqOToZl<#jdvQfB_GhXdMC@bRJI!`M5>I=CkX>aWnZoEf2IQain_9gIaZQs8|%k8Kw zs#*jcbU@5AsZv9g)Kt_EYN{4dLlKftO0QY9R1Gn-Qd~u8)s#>*mmo#e5c3qokQfq) zcl5sd`~QCTzR&NymrwG^IV*ebwa?o7?5wrVTI*YB!d)f^d4~8sE}1NI9Bu5NQ-VQ1p__6gZYOAMtkS6$t<%c>RnuxE)f*$9i$H;>dmcFlXg-U7b??H|leHH3= z>e?&nPSGURTZ8**zL}NeB3=bilNMYQHEFVcaFIvxg_UV~uy?|4S&038h(JQ2JQ_A?K4|9AildIr4^EENZ#-7=gkC+Ap|_-&xS%PA1m7BUN`Z+zDt#6PMt zr)@?f3K0XoWut`%rb&VYg)Y(m(1KF=ZniKb!GvNViOCaQ{H~-^_hWqaowyoZX`xd% zu#4c07zNQh+*t1d)CdU(uvfu$R~g3pD;Of<~*BwSxytRB{J@Y+Y!XG=GLPOd>X)8!bkOpdg!M^WxpL^_aQU zMI|w$?wH`}V+f>86EDoX_07$Ba_!k{5h!{Uy$MS~W67K4TLY_BG@B$4vMvTx65fPw zWI9ig9JgOI;jtKz7_=v>0CoiMi9XEw*4GKM-XSBjX?bh`PAin=>9LqCpWUHe6lfXm zgqmrEHYbOM!+4?9#`xq3gH&?y_XI9mDB3<{cYM`?)kd4!3=es|j0e_=L5A7t`Bw1e z;a*JJa)ADZTQ27M>YJ-1=5xbI@TTPxG`MB5kD|Xb7xe}*g)-in92lSNSlb*sH3{8y zi4DM?-`Z4VMoI{dGq!?)G^RgJ7zCn$2`owhm$^YYEd(zkaX&EAnppfq6YKn{>~c1Wz}hoX}x0=mrQMw#(kyC z22GKK>AKA3@8ZjltX}aRw8nHoLL%3K8)rQ0UQ@EvKw|;u3`41gRPByenrsCdPl_R* zhoGnUq3FjcEEP(sJgSPMYJhRZQ!X&gM$owPlt>2Q9R4)P*NSy z#f&f)l3;=;`&QCCYdaB7Kq1=E2?;w}^H>6unTKN9;NM|(`ymUPvs?a?W+f|l7$o&r zYsrh#PAp?p_N^Zo62kha59baeseW-b;zna$X*%OCrCKTuadYX5&)8etN)j>q!%1$% zMZiRB-T#(rr1Yn!0jkPMT%y^I2Wk$ME)u>7>zB9$lzPKNf}P1uC8(;j?9ynzw>7E0 zhgBkap;ujzoqE`*nOn;A$n;X*;m6%G>ywZ9aon}rpQFyM1HRftPn^%5_TLiZjf? z*AM0vN#jk=+gZ1gM-yo^VgGkB%?>auPm2c4JArG$~M^yKdGKS z&Q7^$=>IvZxOY@QO6lE;^v03TFI^elWvI76w7=HXmpi8eimb(KvFl6h>#$?r!gSY{ z!yiU8IZAimA2m^2%6t3-F|609@i@j?_58szgSo}>2aK-^vFU!92o$?tvFv^MzF)P` zE6$qq)gLrz&xhw8BkGLjz2B0hg;cJcPw~#G_l!KPf^|pT1eb z(_T=~Fk7ouw;_FAQ)AtJ%Qit%vVCJlzBRwc$0ERm@zbXV>hf{tO4n)+7DkI?cn;(_ zJs{4>$BHyurX&-PoK9__ibHhW;fTD zHGhKSFIZZN*nA#HwL5CQEo9mi&q=&{s|w*HLr?iobm2;eps8@q*(=0F1KkfR`8HR? zZNADrGhfrd?ZeEiuv?p!1z5T}y^-C~Xp<`V{5nUrIO9Qe3ni?;)e>GB3+_X>weYOu z1#?jw-8KWexl6{Eg}S*7wvr{ey58qI*yuc1?$X;pmS8g%gi$sk^h=p^OYN4o+o~33dv?<(CEc-QEpnA3c2EBUPId3S1UhqE0CZ*Xw zpd%DDX>L~b2V~su2{Detma@#>4X>!N;i8@$p){7-JZatBI_4qT{mRq8sVew#GHSiL zD9HPMe@Za1Pbr%u({zT2a8Id8KR`{VEgKg+jjC}WaM6X>`tPr?LkUWuNLAj#ryKNq zPaC0R_FCIGKQ@MZdh@3Uzucy{LDjMOg)80gZ9gHQqL-THW=k~5;7Zk*{ec3|v3qnm z&wU}5=p|aQnH}N2@XEX*8}m+04dfc$WqWiBJU?jH8_KZryWw#)!Fl_j>uL$x>|O6SIGBo>eQ3MS!KiL{=JH`-GDk=NdLQP2 zWj-ymyhz*t?;qflh$8olh=y1`wEx=Jhk1j6y17dvv)f};dELe|sZ|MwdOpZIfVPqkTAfeP2ij^^St(HL zryB>{=%Ys#MWwI!eS9=P&!>RylKd2CM;5nJ`jzZ7Q0_=#-wy+_^|Eklm|dq`1k z|9!$|uEz>=ebA$B%2+nZhsL4_9HR>fQf-zmUc!t66o24~s(#|oR&ZHnehuge%H$}< z2tW6@^~cs%74L2+1L_%~RCoIh-{%9z=)aFDKmJ2=i>;-(j|)%rp!+9q({EH>mtp3r zh4m$TwH(Wa(P(?0E4D!PmQ)RNH+0RarRxzoXeXNaj&_F#)rzpc072f3jlVI*k_s~< zvXtI4MM0{37p9{ql;=vaNovG}7m%wfFG7-#A68GMs4wykC%QH4ejB|#`m+9p6}_mu z`W28SW%zm)0w)!+jnauvshL*e;H)2cwMM+C*-njcA^A0*&E`H2S0g^F?~!cy)UVuP z_W36ID`IfwCUbK85+uFGT6I5{x1xO-U9#Hf^fFug=3CMIf|F5mv&3)o*0zGJ}gLfqrTXPXbDFV`y5m`_iwaR z9itn$+4NkramTmf=cqwp$(TxK%GXWXH>AaUNI7kOD>$>~!4ET0^bbyUTXkV-IyN{u z^0@aJb27K?aTXU)dtFuOR4H}9K^~GxOy(>)7n&?H7e&mBHGHd46|04pp=@8nJ2LK- z{^YP?J&UJ3sk?$Wa^;l6AUHe{vS@=$X!xN01OJ?$I&&d&tG_v$|}dvXBoF2T}A4@sZ|Z`pjo4Y1CBOJP%T>V`U>;$&8H zB!+uBQ~5)LsO0ybPOINxXFa8MjLAsXYjLYyDNs|Fx61TqoC_ap`)=oXv?cfCL-bMM z@PorhA!?($Hx7T;lE6*(x6C_or{qLJJ(Ac4bhz|>Of}Q(>xmlN=5M#?oxedYKi5Z0*V8yO(&qx^Ms)Cv z+#bmn@znZkk}@N%B{q@tVC=jEQ*-VW)|cKh_YMw|19nXkTazFihC9Xul;$ z%U%C(f*a|JBMl2l^1~y=@6K)hkn8peuhqGMM0Y{I}bE-Q|Q`6%~M_k3>NNKC31GVa~WL6?Nf}1!EI?FvS zxBCJuv1{tdRe$r3xmOwv^EL+`JG3>nEwj@HRJ?)JF635A{s`UMG>P5KsT*E>#}Pn zrIxOzn6{nyzk%_yM{d!p&gi$a57SYTSC}QtVnu0(`SLU&%5ik31U(4iss46%5?May z#yG53e)u6;yC()Rz1t;5S7Aw8kE9!yT3=Qeo7mRJ3vL`Z4O8xi>EQif;!F?lw=49) z-5y=GV<@Lvrx_|Zr33}8E@498ODH4FV)aS~%%kGQ_6cf1v}If(zig}s`VHnHM_Hof zbEPXoZA70gJ{n!WQCqf+YCbCjNjKl=4*0P9^;LaC zf_X+k%~^*J^y5}*H)D^62w2YV&aWELS>Fbprel<@@0QeGrQ&Ct{9P9`k!~MU$3KB0 z@}5y*#f>*ZlI%1I=TNoW$vyKZSpNHgx~x_Y3{93>bD7fcM-U#*@4HB2WhEs+Na+W6 z{D|kGe;li`y0(%n9@F~Mh;oTtZKqyLiiF{Rw8lKB3o@CT0jGMlSal$)se86h2}}meig$wfQ6TDuz$onCkG?;CmehaHtHAhf%wPLSsc&1$ z<4|Ks2C;=~tInkw{vbS8QgI{8wVxZix6MX(v4k3Au|=VBYC8s31=_U7x&vOMsuY)E zEFQAs*ID zz`AzcU}`ANgeRmgqglD6RWNpLc3FS0qrPvz05W9rp|T8AI7|2f+=?BVM{jPZdk;e@ zc1`E>wt>t|j&LGupVtQWHT)vcX&SySfN?dOG%x%D&#o@aIx=PS3GZ_d%5chteL_9Q z@0ZvXLr33uxPiHb57`0Hog@|v!-}TJ(+n;AV&K~yjZ@aZl4h9!O~kk6nle(eVP$oq zEpaz0F03T7enUTFU5SYR7Lkb%?9Rv~1j`w%99;+*gt@Y%x4?R%+LhW!t_DAksK4cI zMCVVoeUM`??5s9wcaRH{e4lTUlwjcAO>xac?Og_)k4{?sT>-QaKEVfCISpzFnBQG} z7Nwrm5@?_~@==yI@8DSSq@;PkgLTf;p(Cu2pbK_3kD??>@&gZXe>zx`anU z=2GcG1pB2NZr%h=obc*`4e4qYk{r-fQ$Q%GSAbA+CZ@MFJEK>rA=PrcaKh!0RU|O$ z+HZzcW`rQ{6WjHJ+Z;1Pi;ydzMCjXM!IvS%gxh0su=OrTC<^=fZdi9nRo0E|Lt`^L zTsrm3#dO)iudD>B8aBtG2ONyF*dAUaE{VbB7CdKpMkJ>~CdXS^ z_2}{(vsZ;k-kNeMu(R7;Qxv}K+b#T=U=3`+4NZloA^x~Nnod_s7W4S_&*YNmkoVA` z$G8D3L>06MOZUbp2SIc+fwf>Zo%!%^m!A0S!*j4Dy4-9z5m^lzKy=Of8Y!#o+I_?= z5@)0PQ!}{lpo!L5$jZ0>tyMNG$q52I73E}h&BwTF4)~^_nrM8$oJRbf z4O^Y}I9Cb{KSJjlFx(Fm@Kl(a-|k4xdOxd-Me5c?Z8y+FX7~qUGKk;u=LZPxUST$X zqPjxMCaHK<{04<_(>s0druXyHOijQ}$T`FrjMoI>Y`}Vfa1>*JrfIj8k)=z|q!$5N z8nLlpYi+*EwC*+zDt;yxLl(Dy>ugUETk}^6+{A#{S#C%5)hJlVyacubTWDvUNk(Zh zO=;#YTHd3@m>Jxb8mCsM?k;Zt&MzayY1#*ewbAMe4QndQx+Zu?7V%E{+yG@2_*t^{ zcNDNUSa)Vcr89<_zUkikhExmn)vcXW#adYQP`<;;vf}AO*d&Ybd3vn`6TLvWhIbD7 z1Az-MCYslQm=7^s2j~|DxZl|)vdSKPRf{HbYvKKju`U|-a8MqJ=zv&mT5Kuam?=7! z+HXhrjXq6&^mFTNU=noTUTr&m`5nUWM{*PG<%tqiY>DMyQQrV?4-xguDFQ}jUkq52 zRGJ}c!iRZzE?@y^0dLRmI-xufH<{7=QkeB@!Fo`8l5W|2HfkQj3-`mB(LVHC5seMB z_=vNp^Ta{TTpf91hV4i(fqMP}u?s=gcb4c5-6g7^cL)buB%nr82cK%;*UA2cN;99< zna=_$*2w}AA#YDVM#{}`DNK={1yC2){Q9+L*U2e`smJ7&lF0?`pQ>4BRtuMjqvi>j z&^o{yd!0#EJTgTr_SaCEQxe9w({;&3OZfuadKV*FGA&mSo5nGM@02`fBsSQ(;P5!#`masQ;{W@H#@`%yLFxF(hjILh#9oRJxGgIl*P zc}ZRUGKgg)qB^ne^8?W1&>-qZwoSdEvnwm^>Th5*P@*W)xbY<40q+g}-V)ML#2Xl1 zjrw85z2_X0K9Z+LuNpNO3B52hYEtgdHmO-0MLoOeuJMNS8A^8lGAl3C3m?Wy>C@vF z14qhN{7S^;H?Dy)dnvdXlyE#Geyas==kD8eDEOCqveJ3eb@H|)%xBJ!ngF}*X=K)^|DtE>I0?y^ zq_Accd2w@S5N;Dq=Vdv}*J4cZi=wla?LYK0VoF9u9<97$P?B#dc>?*X;lIPziz?sQ z=xn*fq*6w6jn!AMl_l!A#?7LHXZ5F7-&Sl7*_$8bO!Jxuh@23v-A_ZP4RC0_Z`4ZL z^lxAwE?t9hZ+#XHx(ws5Et#B6JRTKKW}}eNN5ff9i`zA|BZ5V~*Q-Jv!m2w8f`%sh z$uX4J*U`SS{1Bc8z8Wwkm<47@gw%R`g_fMr04- zz*J-}xxA^!uiymW>rWU2I{+R5?hn8m0FeYnF3#?b*WrO8HhXF1<>h2W6jfyG{tYSN z--UYH2kvC%;wNGQ1|T7TQF96OvpWRV1yB-1)YZYdvLZ5yzu19wxfW67*$D?0#KfG_su79*nKS}C}%)#R~>l62hDe9Pe>zE|v z|0a8BEHqC$sZ1>TDVs?ddB1?p_eb8mXHyKEiv>uvL6pvP5@-|k5K~7OaibvntM?`D zgdg~F%j#h0ksK@D@!6(laWQTVTx>^H_HlUZ8}lwNzkX^~C9Dvbc**;eG~@*P7u|Wdb3_ ziu*1-AC{W&HFNfmbb5YC_KD)TzaljpI|1TNZ|MaCub z#EcUNMzb+Z-=vQz#hsgw`H?1IEjaG8&y}!a%^Tz?@Ig%Nhq8*Yw5Oye?$UR(-A?-R z&BrPB5$I(B9newInPn(*E} zGJ}cQ0&%hYg-@N>Igfh=esM#mBmb7jt(1>8tvt^Tq+O4^wuO1U#ofqyGgy`K`_HWt z-6>Y=Z$-~sLkaJ@bmfG|!C>dB-4x{EW<8GW&X}IGSD6gL6Bg3)^DS2Esq^S~Cp{h6 z&nHc~7GLk&WtSJ(cjFqz!nLz=96a^=Kh>Z3ypMZ~gI(*=v;B`8WX7KGCbH>>@biX# z%s&0%NwI_YJ5K-X6Y}AT-lzS-oW|spxun869QF^hnMxna3cb8?=o_#!J#9VCXBy=v zqTMR^;i*;P*(r_#+9z%czWD8e_H})BzH8)c(|3Y}kqbGlb2f9A{LWPGJE~ooZF2u4 zoe#lvPWoVe!`;#o9tX-Dpvu3+NANeat;i~aq@GmPzx3t^IldeLZA=|IVRZ-+hCLB_ z{Df+5jP!|9Uv#f01)e{6LbNFHrzHoTlSZphre_2O_li0|n{ zZc5Ib9L2ms6Rz8t?=-(#e82SI5bcDK?!(;9p02rleiEtznK4qjmf7juLUZMFC#cGF zKF;tQr&yWz)971*7x~KB%LU5M65~~#3N*($=9gLhq1rD!_Q6ld4E!d*{T~qyZY5)?5?-Cx1~4FXMx(2qsJ+++)e8%9jh)|x*^=C-yrcA z(MvqB2j55NU%%ogne)6`SK08hVW8IeleA~XU%hifKSx>_sOzXh1ux((nCQEnSG*i{ zUer@`Njy_*M66CExlPxZ+eh8{*64Mwix^RoINl`tiq)0XB@?~1KSVE!{p zd2_0{?H{j7ta1W%9_ggP>Q}?$O@cZEvm-(WjGjCSh z%(@xZ{)q3gP|n#Nj=6cNEZvdrPgk>$w18UR7B2QF_FXM3>6>{M^lr89=J%g{y5C0& z-?OUX+C_wc&nBPDCsUI(8}UZ-|hBe@Yz#NZLdawS8VBAK(Y zQJq5H4$RbaMoD&`GCp-ZhBM~ut&_y$v_=n?_`AK5;gW|XYa}q%0#;2np5+Ub^!uLh zOs|pq$d<@^JonA+6P?zJiqj+qJ~eOrv|Y0$RTa%XCLq1syjm)UoC1hh4?y?u4Zwqm z@bJ9w)|^hq-ky4yp0fT`&Y0#yPM)rr0oT=nREi&~&QSXY8>|;Yer)se=NrB@jOoY= zYF^)3KQkLwwNT|_OSXIKb)-_Kvd+siz%C#@IGn-3jpWYKOKyq47PuA8Y7yM*`|ZU_ zj+G2}xNY3rxD5X~M5y^1+#NN+Gi|oyOR^*=4OaFAv=c_*Pf3^273joW#oe=PciDzR zXF?0EwbZ{1Lu$RrKA_d7#r>r7$&CgLQEz>R){VrP^`osqk7cQ4o-8gn7yLP;9Xty?Uv=@NxW>)LMx$BkX%ktd zNo(fyr_2v$Vd>cZw>aa&Mnjm5%NiH!G&oX|UOh6`KaqcsA^TBoT{f!xTX}T(n~sds zC0oS{hfXTEXb;AGsA-ZPBzzD3s`J5K_Vr|nS@QZtCDl#+ET1fCxI@+8no)=N8gd

#tg$4$*%=yhlNYE&4r|fVSf4sf3-D35WcY$v`DtnTP0aE1@;#7+gjH(8!*)0oXDTPc# zGhPxc>q@d}PI)i&w(ca2lT8NTqxzpk>)L9z=kCypXDfZZ2*cLC{K(3wtlqk4#_P?q zD0ya?-GYdQ=7NUmibF%Vn$vKZIK*X_imIIx-F9q>=+ap~H>B5c5h)Ia*lz{y81yHn zVVfXVcWoKprUnD>fjhF&d}<0TTj~z6Bpvn+|6{jz*@fcwgUYlNC~>tU#U*jOkA#jzQ&&iIE?F7ub7x`&JQClc8WF97w+-3x#s z+tXYB=giXGOUujG{V&2Hu#UT{s|x@>wudifBOT{5Sf2e=j#jAAe5| zM}I#NFx(C9u?X}|H|fn zFx~z#Q2*$V2rJ)v ziFo~sCIbL@{U=RUUPbvIGysARFpB=8uB?KrEMVsSou=^jy3*3Jd%@@bQ4XM~{FA1v z2-t^zE2kn4+ywtYlaW|ikq1WSKX}M0%S!*_ zdX?p*|4~<2UK%h>|ElZf09Fg3a6!F2Hz*fDOD{;UatS{Cn~FaJZic&?(^f amu=|iF3I^gV*dX%^|;-=2I~^}ful@n3f7-wJup21}>%0H+kjaCik48=*?; zf%bI^%MN>;ne}~{TNVrTZ1C9wtZN$%FTj4SMy%ip%1(Gux=Z*!HULGZC(3Uk8diuB z(|jFj5?4v%pe<^UO;|Y>hgMR=6y=cw}H#W7DU>Pgy@Qc*l^{ zNL`vi9am!c7kF#XJ))vnZ!sKNx7)6GZLD|Gy+5zRK73C49O7Y30u0*+PODcQOA;RW z`kepw(jaP^NIY$U3;TIvXT4cdqw8@oWFF>D+1aQwSFgpK4ZFPfY*a?D&gFSqod35K zN}jj2*)(5&&K0kU*t_ibL*-faqD8@T<}S{9dUMW7(_fnwuRj=gVsh(zT5WhqWze%? z^!&~{4mWKGKfFAxUBZ1QBQ`37Kt$=f|^L4RTLsYnj#X2N$H0n z)Hwm#$y69w^Z94cPf{!)^Ts+!_lEJSm$gy%$3>1KVwI7PN;FR+M|`0$1X&1xMP6aui~<^RIefot z#&Filf16eW=T>coOHYr=w-uB;gfd-j$2D4#f@`yv;+QuAWMt{5yNs2`l+U`9^9Mzx zE#LDKg1&JVxd3^5Unt07MDv*la_DA^Aoq>C9NOP1r9IKL$!1)yJ?X)R2?Dbjm(ANq zboW1BYk@QdWhGJcqit8NJ@%Y1XHA{5Qy7=)i4Bb5)zxGiUQbFwLty=P+#_Nf>wK@D z%p=jaz+&Fji66N^h=qix6m!ly9Gz>`fNgW|A0FlCa4tW|8}ewb)g$kRGkht|sQr7Q z>r=KYb1@dSh0h2mO`Y^5jkyT2L5SJs5%xVmV7Xf=$ZHy)E7#gW5s){p)p0V}XD4^1 zY#S$V3E2<#W?3~@Ajlf!zUEl8%qg{Y_hGs(K}KH~-kKO`WedebP<#h!h*i7WTxFE8 zthNt9Gv|kgI4e0NPxcIDHVfeLwZYrkH!R@$MVa!y?p&BEca0vraqN~c zNM%23uGA$xI2J2aBgb^ynR;k*cQN~(VC&Mu{YyT}q&mX);)tOgH;c#!XQqe}JToyI z6<3q)gD|wjd3&O~7z^-ofb7-JoSAp%YaA2J`A9Rypt1HgyrZ%NgZsdakv9eNL>KFP zp$#U2vHbXodQ>^$>Idziis))#+_Mxt zMYQ{Cc7=0?hd1HrG4_`q$H9!O@5^r(gY~(3jeQ)o5qv|mn-5gq2o1KC zr9u==K=6;GZ+90KtM`~~a!0x}2K$g8AOeI0!Mlgx;j>{1HS`|*#|q^lY(~<^4yb+* zGzo4_rGdac`~)2AP%Q|eGbsMb(db9CL(ui;T#z5OD|D=WR&)Iy>pZCKR`(LN=mpQc z{qIt|yBE%TtiC57k_7%*fsw%dSrEK&E2HWl^q#c7{#R0ctuELDy69p@Qr%g5Y+42Y z)`u82{7SmQjVJ(_3&DmQ{ML#3uyJkJSben!Y}$BV{rcKOy>S`B@*dp0OM(3#=h#B{ zqcHVYM^b$Q*XJM3OmsG7nB@b#H)T7s9@&}*2h6+TWESCq?e@4#GZnkK%H96H#$Mttp(%R}esOx(7Htktn@7V;HQ- zqC6pO+1KR#L~)=#&ubt4ElQgOVm#H0q?E_%yER9`X_1tlTu4gQ2~7t!oZvX_BNmYUclNe6BQa!k(w}3hy=MZo*uo zGz+sDbG@j*rnqw7i?;{&mDZi$WNL1?qNZR;?;tj#zJY&|Rw$yu-kkO>4Z${NA@Li+ z7|=KHo;>(I{Dyq~KP*X7eX#H&^Ck%h635d^ff-&)m|KJXv}26@*ESOoRXTL|eHR>OVMYDC1?Ir&R{i_tHivJ{;OTxf{N%^tYkY3R{f9NE?3)qNn8bo%2kf04zJPxld z1)&yN)NSYyQ51ct~jf;ACz6Db8g$(-l?XR_O;VCt)(+(SILiHx`LsZXNeY z6~DLvLiOTDg1`T!Xm1zY5&r>kq-NIY1p3RYRfA#~b9hkn4$>^D3Vf}1)44!+ zHvH8G?!|vl3&EtgjkZ6AuVav`@O$%MK8nYuweM#OBdZVxE?{qFgMu*D$E$w0L+0U9gCoY(LYKD%1U2^;f4muZj6+mlu!-U$mK5n8H0 zO32S`G*h;M7$zETY(_X7t{RIbhCHngQVp2wsM7Ax)=ZESL`{pl;B4LO0XFgq8#a zYPR&y^C9@}s>{Mc(co*rF%r!c z)nNU2!!K+lX$#o>*HDlO_Nfh0(lPj6aFJ>eWr``IlnPp?7yk{b6@V%u^U@2&AFmY1 z9>ylbq`|%13k$vE?MO$Cho!uoFEAXhuO+R;S4-^yh1zQvDK$`Z4Pxr0QmH2)-aoz7z&1yb;65aQJKX8-YF)W6Gc9D{*Z>V4Xaq1k%c>fMdNpy=M!L zj@JWA6RKRt>t~gZ5d#*;NMT_vq_3|f@;m+^%Nn~G?s162|iIdHiNe1+hRY?6edivHi3gw zv-eRVa0?y@QhONs?SL_rAZxdtd|;@VU8t;Isg2YVq?H1sDc0GvmK%8$FHQEZ_Iy7?S>`_x>iZr2j5`}CXHS_pj%tU zfqEa9f?bohw+p5nf%q(}=9FgkqZV7eukkP+)axf$pw&3H49^!9Ml8|3Z6)QULaxi( z?`st@^GBdeSSp+nzGuh3g#WPAT+2py`;Pf!Bk;#H=fSj^TB91jknqmsF(eBfyKUPe zl2RHdh@El+SqvuN$Ljm@Szr=K;ez(nf$a7?)zW<$t(o-~{A-vQPMD1mLC%dr{lEKw zW;{z2_dM14xa4>?qV{5sJ=m}u9xHDzlE<}@P^tXux@(^h1)w}{Shk_=SJHJ>l4|^{ z66`SCT(mSyL0FEhJWOWMA%nAv=MvySF^!Y9CVfsgs zC8~wg4CjY5J;Ah#hmrhAK?{j#BhkA;LTRrNNGSd~2xU|mkC#Zr4D45_B7%a~N0X#F zc|_%uNI+>5%|VcY=s3I=at4pGNt0q}M0jkmUOYOqpQmG>GxeQ= zSYO*IYZ&sq}4%#H?fJ^#-$N80U$%DoHIU9b{h; zmv*yiYR8c@8!}L?SNBsgVldw_i?unf*RuFAS8koKDr6H35(D&9urJ8pF7%>YhGHI7 z?N%L-Y_0~LM%c#oF~ZXeKVxEgh#$LsCkV8SVV)tEXx;m%l6aG0!Hm(@b{gXWcUh(Ta~tBn@1nIC3}vPbg;CuGCQv+GrUfnt*}@K&P?gvxBK_U_yNcvsNl|WN0*7=|7^SMX zNEr}5Z0vJ@f&LsI_`StabX0IsXr0$8pXTI4+z951#}-6={4d2D>}GM%0=IGg4czAo z-aSODtGKhJ=;#){PAL9>=QUU(Zd!k++1zYw`7|0&P zb#)N7K7-_f#sxs)qj5oU$7uX;J?Orj`iq3hw@g=Q@V~L`)=;-7b&2qLKl!@$6rlLU zk~dT~xM^(@!nSGM&cc>*VM&vBbJ4Hh*Jxx%{1_E6YG2nW-#_64aE(B-@B1IbCYbOC zTOrE!6aKMg`QOlFE7%JEk;rB{_sQ2Is>?kA?ZUT^pHjwlec5Pk(PUW!HAm884OM?) z{72Sh;jX4rBu%mH)T;#i1%T2iSG*G#LZPOZD;oTHiW`Y#1IsfKiO+CZMUA=70GKI8 zDE99uum@dXmRz+?xmEalH^|Vs)B16+lQ@4H4cua-zK3dcKoC^Xda#taQM?&7+-AgeII4uVqVsx3*R_d1ua3eu^DI@3PeP zLV9-y|IZfY#kixKlB@`Ao!D*=afAp{B~1wKY7d+uSGY2V;$OTL+#}eGC-U--@O=m! zI%%gc9X@%Wv+ahMNNFn7+h~gUq$(DS`g*VI74NYd{*1bqgb=^YUR#5@2>Slw5Fbuv zZBe{K450r8zN#CB0ZA1c;qRu3!vZeFb%0LuDe;+Etpm&t)zSB7xor*gax*EUiW;aH zrid>KZ3_X!7UtV*@c6Mp^54Yx7*U~mFVH8UU73TTg0H>hU zAhz`RpCn;v>@!o0_=>XK+Fdn1#0dmAwPIo*=O-NH$_&=-#W(R4bO$X^lef89i!O!i zMw_VAzP0On#ZLn?+8KF=9$o4$YnPwm_uBBg5^gjYy@V6?LWMP`%8QgXyYVe@4 zmr9x`Y_CDN+F8c%F1&--P1jOa^pGw>AX5L_HUO zz0YuHg#`$5eZ8HJD@hY6Cj2Yj(TBX=e$m9)TOJ94)~R8#!_CkE_cQeZbGqM^RAz|!2DpDP}ERQQ>IzZ?^TSGGK& zZ4w<(InHKiwp8cT07J12{aHhJ4L;zJUZY|M@Kmsas3$l!E3GD23cBih#nbnzqWTc; zyCb~HpSpamfrcK9iJR4NqK*^p=F7&eA?9#On6OUF97iHye&b=a^6x54rD}7NRPm_F z5W#whjU*5eXN6Pzo=kHovO$lIdxJ=kU7-+*v5`VWHn18KXbDq#8xyxy?Xe^h45D3) z8Y(b|MYCZgu^=3jn7urYqiTc5wztpiQ5in7LVSEnO{mItc#A({y9QDQw_ba z0YD$JRPS#hUji>2wo^xb!y0gpl;jr>`_Is5d+pMdbzha;C2Qh3I2lA$R*u12LkP_>}45AP83op2Xv9ycun!sh&BHlqwLqM!w1B{o1i_zjMV?I?tZ7JbW z^i{3fY<>;ul)B&I0;5k`+bov=;gl+Ewt9#?#QBwUgX^Nc5PcnRpUwYF+n2&-OU5Gf z5Xym?Kl?+st!0n-zCjG+@)A0b5XUN-rO=#q5CN}-E5x3EOsBh(;2V}%-GgtDn&CUO z+L3l`yn3I<{n?fWuIpHKK+;@^`Svy+a}W2Z^(M~wx#4rGV)qx`uZW!3z#nZoUopx% z>^iZ$+FdW}Yp=%wL;9JX4mY|D)iuhONwwn5z0!1iTVm|1so-DqCM#in6=9N%%T9hy zPrg({bUb~vTsRs3VZgHTR1NL$yX7O9<~fH?yXKiti}*QHtF2#;)d4s-j zWyQVU*LwEB;|yvJGaq1GhL&fBgr+&=$Hq|eQfebca^qcH9Z6}$nL3*=B()U38#l~KG^KSOk;03EJFhivJ+Nc& z;j=wI9ND)v&`C#V<^6;%6Tm3AN|9V-@F;tA&warb)2Pe6N2Z>!?CEWNIYM;R%sqp) z%n<2UUG-VFQc+h{P)^&-cY|ySu^T`SG z%ZauprD0v#c2p%>n8r%TBaD%xRl~=sqtzb+E^4*CzDEoafB@y0QOx_#D>d z%(<%Y`{R(~Qhq;i{MPtd-VW>Y+vwiLoG1JJilo@-CqbJBjy-IEKL7E3JcIZ)GyMHq zSbUs~xx>oij)s=S+t8Au2j>U;r+KOSDJYuOW23*tseD6rexK$tqf? zTYGQB`|JU??5kOHnRn#xO}%CxEk0TAO?<~Vg|l*O=o9>@`#!S?v2)-O-LRnB`}4?@ z03N^UB12a4@VtR(&Mj_Zm5VmR%6|H(YqkD#`Okj6*s!UI4P_SnmEyeh(0FQ2X0zwb zqiX{692;M)&hN%mCk`}k=@kMwnAcy=+}|1zw0Zx%Cs#k&6e8CQPgZ%DoM`^sFq{`M zbX78$x9`I6q>1MR!Z2lRx}&(Yw`lT}O>VNMVEy4;CRx81-lH6B8fgnw`Is<6`3{{4H_yR6uAuE}R!ptK%H&k4Ew zY~Po?pSOv&nZ*6<%*@Eh3yqTWuMeigTwQmB)Zg?o^J<12XKAbR0v{AF54p5=WOV=Q z<`s4QArHLd&4vBu^4z5c=T`M+h91A-O<(I_(#!a;^oG;$_1O3IBYIW$i$fdcc#7Vo z4;L>x;Ptf)SFp=u5|;67_wXgBxK}_^hXX;lk=6FQ;c>Jj0h+u?-(*QNb zx9wSB$?U0XbA{K|uOhveEA824p~v})MM$d7L?YvyJ-FYYtIk+hE~hKh}cR~C!e4qbG3y)XMCZ9D@J zI?g&BaGHu%q*>)=dyUvqLmt{+ltr6YertHjO|4IS^Ds)Yt1BU*R~kFQJ~ zUPi3)&+~-EW_rcOOmB=UonMz?Q$E^#Zp7Su*m?fqEa$QdwIjiPMUUgDK#uO6{<_jt z{ei+`lcI&a$(*>dS7r{h1(2N%+6xum&CD6J@WJ_!8j@Kn+v0}6GQBF?d%#E7pHq=E z+*{S>5I5KUk9Eb|V)^Hs9BV$?F_r5X1|7|{`RqkNfKX^d*GF=Adhmz7QFS66KKY4_9DM~eyE+VrDwFcV z@DI5f&lcPYihqOP#3`K>=2I~Pm73NG+kHX-9`Sfsx1NUGdl5E zGjcZWBve#R`F%ULt0*loshZ4QUCe5qS=-HC8|4YO54)0}!lw*KTtB&JbHT7>$Vb9& z4fv)Rqs;;5R~P229z1FMVpHjlIZ2d{i9RUdm7ap>S99B$$!+gXKAfzz;G5-0*{k}$ z&juB3R`)Z4qMO=9n>vLbS(z8(;!*FyNg3xmS|Ub zeFs`8-~Vb4#FeuoE2{6;e0~rNu^jq)JG**JzO1#?uDS{Ji0gd`;SaPtro8jFj_x5o zC7%6Dc*H~Y5nUG~sh{{p_Y^eK?J%6~esPFvu8-IRvW@Sy%?=w2e z&mtthr2m6eF5Rj}}7j1i^bQ7_Q_^bC0H(Ftj&?{8RYoLG3AL0jKTG(f*~4M54D zM>8|n%*2}NEOy1On;8sNrY5z^?Z~}{XG!_#M;!2_s#igh>`Qh3WwU6k(|_wLk2SYa z37M2hZXI&C4#`I*6VvwxD$H&;_9Vt>tL#gng0-{4$bI;Pj(L=mZPSc zIQ{9NGjAxebZcyKF4hlqjgV*no6Wvfho^EDu1k9Etd{8Ctsn;o%ca;y@XyJ#M{rN!3 ztyMEK^7}^n$=xZW=vMvu?ndPIvp|kfNW)1?=ZOW=qjsfmAh}{Vs5PUu>-hLR?7>IG z9O-0>>CB2q+QAnF=A5I);_uN=*7n}kS3#KKbbiqr%GiG;DJy|Rlpni`geq+#hjZvh9yB4}*efm$55qxn@heprW(|dE?&bf}5hY2*XQRQe4E2 zb9kQ45~mVGaItu=;`=;*-bxkikQe$b@!;|}apnS|yoBL!-W-FEsCy}HeM z8W=w*gj}$Fs+8;C?h;$pWj&?mu-ql$id0kHFjB_)u$@;1Et}J-M6PzbYDgzA)Ts*& z$ZMhHFPi%s!pPT}iwKsdgzrgu(s0A2aR6xuYuV2@@vDNV^j?BJ6m6KDRC^#sUwcq8 z-yM+fHpcZzeG}%JE3Kw7(=AwO-iKr2`+N?ET;^Z9GAZ3H_@vJCKHA@GA~CG~3(r0M zGpIMR;dxpZ;}$3GH`9Ll%+iGA#p+#>(|+A3zjU{H)_=8&ra!?dj`QA49*y3LjK-bt zoIEx2Q61bcAi)@-mGQ}ByoK-^6_sw3`56c!8^w0r^@dDC5}@XqMe4a9z+3Qn?y5Sw zxPyz9YjoWk5$nhe%1qk4ENU}pZV(Cm{ZFjihdtvv4$C~kkw3Brq=@nv|tr5k~EW4 zH=q$LY^w|G#}S%};E3S{^_kWsT049A0Trd5Mckib2Kj&(!tM~L_ju)Fsk|Z-m9DwD3}GmMW8WJ}Ua?=j??s<&rHZ;H{LfuFGRHVt5}Ci#^N7W!JC4i!vnalBTYtQ8m9bCt4K!*8ao$v zpuvrG#Y1Qn5Abo#BTa?FWZ|Z4YbZy~!KHAsMII3SxTmBxy*SM_^ssyimoBc8ujeI; z4gy`a+@c|v!2m8x{9EqGD-wluMQ~e-`E7-9-qF;byt|?+9;zTN?`6jLsHj{-Y!H4V zxkJaqn|4!w@z4)(FT3fh^KTUAm!h6IO=Nn7C5fIzE6S)8>!iK6vha4!e>6OxWCy;PFOto7;jgWfR`3 z-fpUDKPsP$@eI%8J62^JlSh{ef7c6-AYbC*KOg0_RH2pDLcz$5&Gu+?6!)W0{t*)b zacX$4J>b3l^Ye-KMjNVDqQvt@G_SXGn*D-Dl7orcm6vOyYUfRPLLo}$3Bc#+6n$E8 zU;V+h05z{T-lX&JFZd>MFlL9+l@{f#I~8mJD~HyMtnMWaFpBo((fW(ymq#abB>$%z zS=O2Hi@*nQ4^3xpKSJCItN{rx#^zi=I5NoVl)ur34tnqVjJ|lVcBv`{OY##&sduH< za$&3AS?XyHcUYH2&+7+H(b~WFEmb-zVFqt47n%#RhCkcy6z{q;C$7V+Z17cWl=q=# z1J?a`o2@CIxo@*y(xy0es;la}2a6RV8)FGdlKtc;`8sF8 zK}Lh@TLCk9SyCx9k&+%lY>ao_8u|sbRQQng<88c4W4OQ5r1uj*Z3^RczO<4h^LmYe zzjYs=7O>=a_pLW`?HD$U#SRsNRK3G_G|KCEF9Mr=kW7bcskVej!mo3`OsHjbOS`po zgztKY5a`0VPW0M?-d2cT>9_3|Xx$+1)41*2WZr7|trwD-r(lbfWy-jBsQ>Wm zz!0i0G+ciCVacUx!fG)6-@Evuv%ZtQ-uBy83JaK({j_!Y@jWrLM7s5Ltzw` zwAlK@ZtYw8*{PagmxH%Ai{1{LTa$cu#Kl;`iMAyH6SoR3DwBD453WD;^_h8@n;^!b=Y6+Io;*L`Bca&2tzRUpX~GrH+_rRC;sy+%g$JsMVns`ANlrF2-%-` zU%XxXcZ6`^h`9fF)+b^;vkwJ33Q0YCQXKQ%2&P1lZXLVi!aImby)5V=9nRApx;fGL z`6ptc7id)nJbxdXbYgP|lt@zz#uvTKPCD^gje@;l!XCW+YkBTm2VW*Uzs$@!O<;)_+TvZt;kA zBwIZ)VE_m}*RXb@9T}SP{}k2}>VTSyq_@53X&@+e!GO#(q@j-vuG*&{-kwdwJw!Vc zu@Q1_cTDtZU!`u}o{NI)1sz8ZS&)ctG8QUvSCEUbHFgUCzkFH1zKQd58NT&Pz8&kG zaz1R{9nP6sf7b&4(`-fNeBchj0*Llc(hcxg~*pH1EZYNN) z>JQNwQVz$jzkl7Q@16KipJ&ih;cQvV2kJhwD%-K+Y9p5nud|6GASRB78*kC$zyz7T zOM(r2i`MlGGDSNEVwNU+=}Z+q1<15XAV@WGta8Zt$BD!Ycj~SZ+NC4!J=cq~4IgTS zd4J~=!IujgPTl^MH0FM0rB!{FDLR`nwDp@~oP^$yf4sGTWB}N`9G1#u=?MaK=^>?c zb zi?inl)#dnfFJ&~5BcYA)#OHUX?JSr)qAezW|q^{ zE>{*Ngvqc5wy|ZPFicAy;$)VZ^iRS;CBg56Ji6m>W@t-=T5%LGqHYrz+j@mv;^%G6s{Hn_GFc!6G4-8{<)_84OO4?TOC!rUDX*&CaH+p z+ozjp@oHc~O49JiYs7RFbrWI8m*%^9faa@O>?YJ0xqX9Ro)-#Eo53`{ulivfWAxMB z{UGxv?e#Bcvb)`Ii)Et4{~}rk`6#V|q<~Dzl{9TGsg|nNtfc-pjA8)dy@i&|GDX^Q z(V;+-Q`TN-e1ftbqlv()tF_9BI&D%AP4|QN|Np;FdDDCVO88qM!zFNZ$2v`PR*E zej$~_qX~o+)d<2GaqQrDNf;YN#y)bp7vp=_K;z1Zq!r>p(=3h~-YE7&HC z$UZmHz|2nd>OFmGWu$)fN6oAfqa7jA)EeY6HmtgbvlxL7aTkfaKj2gO_gd8}>|1C! z`2Yk^Jk+wHT#nraoX0Ma?7`LJrXwf>(Uev#R7l(aRZ%yRs8A0>Xv)D@z}-hq?AW3Up&6BMO*^WZq0u zG77bN8=6fl{{JBCGnmsC7dvOzI-j5C?D2m?vGDU-)Uzj4s#Dy#!g8~(HhyfuSWKhP6HH5 zgqB2{^{`Q2sAT^y4qG7`sDf%`0x(8^i74N@Kw0OiwI>XvLk_!EFut9~Li z`r7LR6}J$rYWG#hxJWstkV|6WT5tmlxH>IJDZ`d>SAZ!m_$d0WarRe)^l3#8 z{zYPg5vC*=9Xi89xc-a8j#xaNaCk%9;Es41W|=RH6rhy}$8(Wq|6|0#b>^dWE>4i7 zRGoQLy^rd+%n0BnJK<|7$rsRFNY%v=7oQ}iw*kxuoIU3%tkIloaQSFI)_4BseQ$VG zJlbT&dn#*&9Q*e+e{K1EbnMe^|c5anWdA6D|f(u?Y^0%u zCzj<41{k@f2o=@iK{gFjndte(in8*`XY2|$yjJmS3p}q1&fSMQ<}$Mli)*8zwq3Em z6}djI`8Y?qxZg?Rkj&R3Uf=IBvmVu`BSQBezEv!*N_G>>ii{dDS6pXGzX!5tc3Azq z1{v@JSVv}Pp4nec4Jj7%$tG1>H`Lu>B;V}YjQU=oH*E~eC($C#3>_yX8eNf! z+ETt&=)zCWahFYnJ|dGU1k>L$Irg@?{htKuExl)~*3Ha{4?Wn834MeS)aoSykTyFf znGsf3S8M5@nV0vCz&iiUdS)N1-kG`p1voSLeJA)LaqTSr3+Z+q3!wwHKvDB5Q70K- zFrjpHbz}VM@RA@La=q3P$Liiq>`ZYJ%!uk}dR42i$A%^a_GySuWUvut3-%AgM z&nrCZKv=3+?XWc3#WBYn?bWPpHC81=OGRI4&o5bGuK<^6wAdjw>2?Y*Hzc>LvCtHI zk%r{FRx=&D2n;F!X-PiLc&Im5dV%sFa`7`)c;#0%%bJ`;4r=ynHVSR7nGghWt}Zhd z#-bhKwyC|@=q8p0_T11Y_oq$Va~)U;Op{k4=Y(X5jd& zv0ZeR%vHx|JY!O@Tk*;yXq+@I7-o%&r-oP$E-Z-^aD>wNT0)FXQlGgtk$BmQC5*T!*g%VCJIbG^ zsE+cUfY26J6v23ym8fhmMtGs4cCkmL{GAlF`afDMzHpd-$8?3RHg!Y|Z>H%bQ3|Yn zHR>xz*(V=_d)vxm*Yjhk3$Y40n{`2*;3SJ>ioImT-40}dg;m6(XOl>@O| zzqJ20iM|Fk%!(7UzU=|e+z4X{yUvPcD347~(SZ#;_66ToXCxy1@Jf$jgpr|Tb=B7o zdR%NCMDsj|;3(u%gxAvmNFG7^{(C;u@BUV!9>}vV>O`z$qkExx%{= z>iHM#^=MGy$R!8%CGhS6YZgh7xOthUbmJhUrZ9Q*jKQ<-mPn|*o;T#4_rgh@4=0Ha z1qE33PEgMYGL%T`EWNM_WmRJ`A^MI;xoz2lZgq}oi5H+#Pl{ubEY; zQR%JSr&%D8E-lHPy~0Vlf>&!yW@pp^oD7p^y-k;3JLcHy%IbUDM+;%aDf z6(}ou-b>`My@Yv&fDH?h5Ix;P)Qc4)6r-)Qmi>>C8jS!5C?CVhLB=z+jr!WmAHrlF z1OSUBq?nX!T;!3ZJ*;v-zwIwSov&9<@~95+vO|8NCj#6Vot6!Z-=7n~ zLcc;zi#Oue%CARfXiR#sD{T>=MM#*q@t8)os47Ur6LQn3SQK!1-gR<&1U07U$680w>M7O2A4->tn8osB=NrZ^^? zm*GF^@o_aO0Vp3ry6+e%ahw=Sj?G3ee_(txSW*1a-o#EN5m?e0FTm-^t0F}0j}aL= zNf!G+I*53u$+}PVz&tpoZ!x~(?eX9WAjU~1Rgq>5X4dM*xr{MFbJSiviJ=*%9|>G1 zUA-1^0($HN1+UHlWV^ii&R+BCNw-c4uJtO_oaCJy;Iaa3M17x8qr zAMoa{#uG4>gx2!N*h7`HzE#NExnt5qU_^IGJ?UMu;?f_>UB*6)!(Q)t2{pWM+`{Wlv9} z9&}Gp(On#Gi)!M18TDVKe~;0>Pt30>m%Jlf{rKyR8518V2${@fwc@HNNe)#%@v@vd z6RC{_D}JK|>PNPnJir{MGTBvDL?+!WX_rjb(JhPXCD*|oS*oq$I!uu7eOc>RG$+D* zK!+qY^&SS4brE6qLlGwtjTswo=sNfB zeb|etg&|w0@wiTGgZ(y+pDND;9EFTI#PbGLMry%~(VBty2Um!)&+WCw1iW?Lbjw5uQg<(QvEC; zfUe@aE~tb-!LI5Yl>*T`DZLA)_S|lBn5t#cSoQ>YZwDHBQFan;_q& zy7KCR3NCZ$)OgLj!5ehj^@I}VBanX+I|YrUp#0$9d|xqX)k``hcoi>?_(OWvH%ltl ztB?l@)_ul*o1y25=AHtR3%<=nJmo|zbZ8+^x-US0{q1PMx+l&rvxoeqT0rJ6ZbXKT z8cmVc95``ad(Z1+3(Sxg+>jA3ARO{qbh_Bsp5PVCZh{%Ipz;PK^_?v43zj0h3sNqt zbP^>8K^;eolDeD!^%1Q5QKi?`5^*28Cmj)v2j59r z^GqJv?pXTqYvAl-@2#tlUj(9jZ2I@CnFEwsnfeD~cBU|C$j=Rh0ITGw`Pe@{W48;W zK~$XwZ(rhkH+Nx6qzAnGF7EA-1A>bVgpkG`#2^~N9&xxfuUBP{_gICTXk#sF$oRq; z^bx!we^3RCCne^Pz`tv_5<2vxml6Uw56}#26hnDmD)L63LclZ=8W-r0>W5KvN`&`K zUVK-wyEku9$e#+Px&`=Mz%^I;&dy5}ulK%1CyXuY|A|S0-sB~tZvw3&uoPrA&50>+ z6Ijn|0V@6a(W<2(Ked%Bj6_#|%ZB;hMhXEjORk-?Wsmbcl*^9uO4JMc9guB=1*KU= z0i0w(2UZ@GuJ?7^26n%e#Ym1nNyKftvrZ!GqES)Z-O_G{NuDoLpr&dsts{`Q8Lrw< zexNDBfmPK=29hhO`A|FZXC^F8-wXF##lzm2og3}4Ub~H8h2Ke-RR^k!g=vbnP-Q{w z>`_J=X)kFmlD9;N0hqoT3kmEk>{gYSrwjaSGV)_dD3;2bsv2wPI{ZK~7(ULe9p)+w zUa5QBx>4?u?XWClxzV;EgH~e>8L=awAUj(PT$!H$YgMF3{C!RCvGg=0@-oO*+TP?o z)I9C6Zr>)<#NGhOSKot@Z6qF@IngNy?+rh7;XdqB_qK0CRDI3vCtVU_S(OLEG2w&t zzOy*v!m6`)WCL^L4Nz9ws}z$j5N|XAEQltvSJ@!B#7{*xGpE$kL4WHlPPa+CWI(LD zr9*uIQPQJLN>ip(wOI1+cYgw3qb1`q>ZBZgifGl)TPV&@-6E(m#<`@FoP;Xjnfe)y0sp>N=NN4=mZ_nE2;WQn?XLRqj~lLZ)lyd zV$B;wS8PSCFHpiCR0jg302FwwP_tRJlZZ5d;s$j;7^^)lXNJ3{a2O5%WbvlM&Tk5~ zqyiO_tf)sciH*89?F-ijh+W4;j6ihA*)*?ok%_R-H-@rbvvsDHX)2J29}1KLK5^}W zRIH&I&%U7D7O4{RD;il`n-mCeTcDd7i#vPSo8M^rkjVvP-#cVxfQc!|(NU%8fo?Is znaDCm*2V9m`2A$J7_JfU)E7PTxdozO;lx}1dcNiGYN*;ZHQ8=iM&^u0bC~AF}n=Vv*K{sXp4&8!gX$UfnZja)rhXy*oRYg`z9O1>ND$IQ7d(_o zoS(zOyo`mjjk!T_&7eZFB697S{Yk*oiG~6zS`nx9F^x0YXUv#(Ivj=bJPa)k@}fuu zUQkIWzm4par?38Fu5B|$Kc}rN*a7V{BPD6BsCElfn&IT8g|(NtK2IPwsR3<4H-SFY zM$)f7*+$kJwLEl;C3%$O-7_Kbnt}cK1s>3h1Ph6J+O^Ol^CHG#`x0A#!tJaeu6-zE zttMaagMRJ5xgMnXnvz&`^{)@Gmw7kkd8kv6_qYS(k!I*EATmzq%Vs^T2aZt}Ndm7B z7OH}8OhQMBLLt{dkN3k5HhQEc6d4F|$g)%%mv3ed^VqD@`1d%o$x19#bmaqgE$!P^ z=gHb}m8DGDlh`T;cFw?rK5Y>FUSfmXTj63qX*aSm47fofH^Xb?elz?Gn5i%#ittRn zuZQM_Sd`$@{R`5P^er#1&33&!yIKBslafLamVDsmib@6<`-p{zaP2O62s;<1t9>Ky zhiC%pQvg+X||i3^g$th)I1-I(3AP2jDnc-;qNKxkKh2gyLZ zf!AW&S!Dh!MEfniIGFtM|CI6V(NJ!0{7NNCD!RyJI!V%PTthLF6i3P=wt7~LnhroG^Ycii6)^cXxXf&Jr8ZqB%0xLK zyiyZM<6mX_tGaHN#thuCrg@%o$YwhQa0UxwD=Sudn10}yH9Y%E9^2v7_(B3rY}m_@ zloT#Ppz~uRo?sg*#Sf4Nm?aLPQO88*TLRGY)3+8gmjD&>3`?saxEzG{Pxs?DW68qaSk{dmR7^>9rb}(L}Gs`<; zXs36lkOgUe%m%i}4glWk>)gVM9ZU;b?D`oySh>;=7#&JR!0{uTRe7-t;$SFH?-m9Q z)SRFLmL439hfJk$a}87`?R??u0aN-ZWX4l3*$7jB$CDlu* z4fA>!|Kf!KuvF9)aZ{mz@CQcEivrMItmq zAMWvs&O-L2SiVb`!WQj-UFbD=15#-uwSwf|KG12TNs>UBJAMr9FWU?}<%mVzE6jP^ z<5#F<1Gp7V579wMmgrsiM3Kq=MoFTeT|OAO5WT-JiYb_<-LR+82n$_HlEg_8Rc7qO z%$7LCVyCqSU}w?%LFD=ZEZ}W#i^j|mF$rS7KV=Sfot!pTW{m_nL7D1?Ef9W6#)vgm z3Od>H1^!Q&8{i`N67w3*QZ|f#8_=I@)@q$N5Z;-Q!$Ar3(jiN}M|L1rc|24HbCB{0 z)k3y0#rKaP>$;B0!7fjwnNNc$Fs3Q)Zl{wp2fSM`jx*GL$ncikZ|jgnPcWh+|LDLj z5akfDrQkhYeo+v6OJtd9QqIugnu?i9RHASFAMPwWaw>^5Dr71{1*bzY-xnoRi*RA{dJOzOup~0H z&ez^iHaTFk1Wl9v#!rNGGZy4rT`qr+=q}^UBM9~n$gxv<2$EqZd6MGA)e?+Ns&s9U z07lA6a8eh=Yq+8xI?n?5N@RNtjbQF4iZMi^P;zS`&P384}{VhrQS?45TCW_&5i$VLn^j>X%`E|dhcLdGt zo6d!`_y-p+o4b%e*xAMw$&ob8A7So9er)Pu!M!a8Mi>&Gc%_th&-LT4JM^)D9whN? zcl7BO5hjgew1qrSH)*@iu=r}&&Nol%NB0HpLLbl8@p;nc^YGL@tuErioaO$8K;eAx z;OOoKRR#9cDZlskSk)GW`v;;beS+_<)DL;}2FIHCIv?r(HONW2eQu5(^nt6%Y+r!B zy;fd*efuS=2kW<`+yY{^@43kxbOMaL_R^hB9kr)Zi%=m}V8`JfHj9Br-R%wTRb(30 zJgoe&%bd=Z}y=@$2@O5#rWkm4KIzkP!I?LM(`Uy zbJQ(%*6ke)u??5>(`3@yu0Pgc`1FIJ0p zrjF@aZJEL3@y?XYjYD^<_lRoqf#5Ax#eXy_ngdHR7!jb8M?A-h?|K6Cv+}+*A=+l<|S#A+l zIYYmlSbR9{mvg-{!@%CFJ2)@*szFwAxt`apg7TK(PK!UcntW#-4M#VnZ=1UxE$|MC zvY^c^jc86z$JVG;6n1Y9F$6wm?;23Ny35Oj*Zj@7d-_|?@N};gVyJox5RbbD9lqPA z<+?`K+;F{c$*_-T<4bzCXtOH+owsK>s!of($uep5I#xye%jtmih>3-Hb^l$u1WseD z3SV!$^5wm(hkOD$M1|tp(3{J68#AseqqO(q-8nWCepLG-Tmz}ja_e&f$te1@cVX(d zQ4e-zHOlkt)&rmb3zl=`jODVd{2}fxFC*2C7)_d0hpK5mb4S;`l?FcfIM@^L~(XeI4%DSjx@?5V7x zVMGXueVxK7rWNgvo&C$AyokWj5}&sQ;`XD~&Uai5rVEg}&4rFpn5z7>6&8C_3*o zPqp6|$>+AOYJJIH!<8nO0zMPQqMZ!d@-LabP2_moGlV1Z7VDX z80N2hraC$$aEDfn?$=fl)u>m2wsxi|lt9J@hqU!$+wbb~9hae+@QK;d;$7}CkM%Zl zX*po}^RM>Uf6Vnn-4m(SN*B-hFFGj^2ca~?BM@E0!yjZNimw&D5#J;CK?S&em3muz}$s)~W z8%D>}+AVe<7*OrSx+2CQna?)#gSUC>u1f9R?BzK)gjSzAz@UZ?lx{=6)1x_~@9V|m zA``Gyb9Y&KXQ}4LL_ndlW$xmmmLno7)2qD2D!l!Sny|83Gowpo`kH2WP#ZVJ@>15= z$G{Mp!V>~77u7{9&?6WZ9`>Klc% zc)Vpa#gY7TDeg55y4?n53TD)!56Z9_*|1iIt+f&;jeRn0~6q(FBB7~~;F zcK`cJ#{@M+hA8UZf=s&St4oh>6<@+v*%5?{&5F?KeH*WG)g!)qk{y@CxG$1J#TOUJ zyX0dBnP&@sg}@BERga16a|2#US0we}-(u2n-77$ftK&tWZbPZ`*iGP!nL0zcL}e6N z0GAH|t)xy-txPF zS0(GOKXoM6wHtaxDT^@}$qG!b`SH;=zf*+6zv=Defa1!f*e^i_mM#k`0k$G>pY(T7 z#WC?PG{A3Vn}m4bNJNXz#4#*be(gNJrrEYp!%c2D`nO^ipB>B`&B<*FGxe zz`rZ#93vSfcIvm`v#o=FmnY$RUsM1`#CGzxIX_gPZsdw1Vs9d1mu$494z*w$-ly!5 znx+JY1Fv4<*N|(j%C_qB5H`9@H&e6O7+AYgQOFg;Ut2?i)^(4c^<$Mv{}kLy@_oM# z_3o5D*JLeMZwL2+@bmn0yn}R)u|7z$E;!#?*sh_JSj_5;pY)^h`n@4Qd~-l&5#tyo z`YX&A@#xvdvfX|cLFVH&>pgL$KPbt;x7(`sc+cbpcdA>CH}-&?Ryqat;K`JBPEd2d z?xoFH&J}CDzvZsy!ka&1FE;+QkcREAChDZD@FJjfwDi|6p|61g?|4SVHVV(y~!n@tjT zDfV3dSO9qD2d}2^+h+=*rDS&Td;G?AlHxsEG=`ighYyhWCu{=2xEtwN9B{s^F5(7$ z{2(r9N5*Pbr}Qo(8Sba!Wr(|#p4tV%r5(s4fv!&dHu|`{3l9J{J${W9>Lpu9TemKt z{t+Gxe}_?#B>^dCBnGzEAX{%BNXFnt8Plq>zGXB#w{$G0>R zDkoaEiG%T_z;UeVXeH1TUXN3iRhR$#hw_NT0e>t;zR`-~tUkiMkHx-*#gPP@onikD zh*5Qgt0LH(O$)cp=g^bc4l4R=S9I}cnCY9-;{Aiu%z?&qJ;uukj-R>-+_~iFvlu+Bi)7UhF75Fsmy!wYmMC z;P7R9!+5K^_`SQM@b>$$JJpB}$YJ5)PT+|{>pAEi_0NHumXz!QRcBod-trQ5S&6P)EQz=`r~SKG;KPo(88!YHbw{vSHuhx7Ou% znas6wxeb2LPp${gwa+QH8VBH|mmH(h0lXu_u+k*8%gPwoi&jvoT4tYuh;q^q0TK3- z??5`lH4WIDVs7p8GdW%vNc#B${xo|Tt*=m#-3BJX#xqQ|-3(je3dN7VL2aXTue>t8 zj(y4J5lJo6Ap9`;H5jhKEAGLOktvNUYaVRyyy) zkRJ9s*E&t!D@J7%s6*iq&DFn-Z)F~Pn4#R(o*Seg>;>=k=pZ+q=q!S_iqt#r#4&4) zIwOC98Zkbiz`zvezBG#)DYiP?nQTf{iZHuNn>hYmxw&?h#4)YRI?La1&=bF4L5y~k zjo>{8`SHtMe)shIZQ|m?qtC#RjdGl)jAyLeNOnmq>4Uh4dt;hK(q5oazWofy^dn3rp};K(|CT|eF&t7VHNXrIz`usW^&v?(<4|bl z+<6a}XQ8<82N1E3r6VgqdbVs^Z7`pUf#tt^#c;TkEI5-1!r`)-{cynZ=3&V36n;5n zuHLP2?bIAX;U%~@J(N*4u5LTecrweP>U)9DKV-)e4q)&u;`e&4f~#$-wPxQbg!tEm zTh4N&+NRsW*n{EN3t}>Iy zSZ(%)kLyRA47J45E29)t7iM11k-V_>X5Qc?J{o{(@sG~g^%Sg>Ne2)Xf!@bJIXUqT zesDofEfg_v2wf+_^${DB&j||e=5g0X;VU+S$={*Pgvi0P5PzLp&HV(+H-^@Ezb7P0 z-z1?6ZSNVfIh-t1k;X!1ZK#prU9*YON?6+2=AfolC&73W8+%G;*2W;s zqs6w}z6z`+3xhZdX14Nlm0A@*;FSfoj8aX#D7|m%J8Q<3^mlJ9co?=)qoKQ3j&pCm z+r{%j!i5g^9{=^TnnT2zdHB@BYP=kPVfGuAOLLGU%0ZwyFqLXmw`HIlI&kT-C)OSK z7`pIAM60@*14qUX{CC~nM+v=bI|j8gEO$=K8;Xx;!f=U$Z!Wz8W+Zn!7hb&{7UJ`c z-9AC2<~}Ni*?gZ{p)szgxwH^U%086L$ZMk}Xq&lU4+u=^4G%%iE!F6j<(7Fzl24C9NWq!0&+{U|eZ0mIu1~sXE zIsDFcrEStv@dPOJze_1+5CTPI5B5(;*dEF=p~t)DT-$1tk<{Py;mi6B;t!0NYudBE z`cMxe?PXb=Ss(ES{ED8Ak~Hrl+P0P-Dicd%HPIDHJ4Q{GM4BBa_9o&ktL_Qs%|VOc z5!ja_alYrd(J5gfgAQQXJQHUhtpR@RW;+sqo z{<%`agSx$9pl|`iEy^#0av+SD+8isu?-M@a5XaJS?J`GKexyT2)4CO-$zcNh8E&;U z^SHQ&08Z!UQ$igA)(T6V#D0phv>hn1`~W6j+m$Th-+qo;dgrMXe4GdmIw;cz0X}Lo zzBIH#EZ_Zuz#b6d6gy>0L{7C`CWBMj!ym*5nCy;~gQjAd_+9if_Tq}tqM_dU11Bg9 zG%CPA=U8$b!MEp(QMLR(;A3;74&vZq(>q00!~%KVXTqb7%$c)HXdI;?4#JzdXctAb z%2Tlh;z=-{3W$X9#xv6ZX5k@irTesVtB3>-=*^^r{XPSuuCk(Z`@jr1<<=H!zGEw) z-D*CWen?Y1MjaZ&Ysw-4&AoiLZUWlu*;bjd79m3*R-4x((ioT7w)|;fbRWmAGBJT9 z5L4zaCPG?Y&i?6pkEZ>m#y~8UH*uf0k_p89g%D<81Fr&ir@_V0am1$tT+n08ZQi#r~ZJ$TOk1BV(oGI{M3Po^>m%7zXzj zk(t@65!C0L*v-6UC+Mc!R-jwiQQa?>zam#U{N(~FL)ySg06`T3HLM^?v3)Y}?yI2b zyR`sbA!HKeT|itUpQdE@5ESA_dqLtTV<7z>2@v&oc3CyNd0!S&l$?86Q8>M}-(kgN zOU7HXgu(C^=5HCe4f^*TS}_YM=y-KT`Q)B2_?_TW%N!?%f%jBcdc(}j!I$@JQjr;nL3x4)GExfj_sy(?ikEy9LK2cJaRMzPnim95KrMj<4-cqh zD$EU+oA8dWq;wz~5XnF|rlKMA$-%v+GWC!Uq*T^L#H}uZp>i4^<$??DRWZ;AELnTk zHa(8C11@WCV92_%8Nlian+)0A*#(!;IS@Go3@E@1IhzBOqh0?|98(COSgDi%JYUAk z!L;}L8~&RIZ@J&B#{Y(!mV>pD`uEx&wqN7_63SBc)+hgincJED2eYuqm_*tG=KsSm zGqba@_#e#N!rI#AKiEl2yZ^x~tWH|~?^p{f8*{7w?Pp_WZu|c*>;FGHQh`>glc{XgC_pWQlEd-gqhRIFpY*D~dGz*e9+ z^)&j|-oNN9wfXEClQ>Z}XZ!HBnG35QUiR5`;zEz^ipj-0yViWdsr`aqx$+k09_2RV zHC3v#a-6vUy~9svgH_HadI@|nB#()x(#_z`~ESB8%hH7 z!=8E`h|Xvu1-HdsewP(0eL7OfA_&uy4Oh1-ER)?Y{EL2$Ziy)*^zYz1+jx`8IX5Jh zyQj~O*t<=iUsayysl)+3*@v@!p+9Ljq4Plo5{wYc-B`&RQ_#J#CzVWT3( zRZ4~h&=5q*Sy3%@F0^+18bBSy{zoqAf07TZ>@Qtfs9E^Y678MBV>^47kx;OXv-`o3<*?p>;We7fJ><{sHCUxRBl z41UrYZL{82CmEv2s=0XdV$0(fj6u2hqc>hH5hGEW4?@GM zJ9$!+Mx~@_5=j)iSe=f2@}8-rv9wi? zHp(D_rqaUHTycs+Jw@S?Nq&jAg-^Y8Si<~g@@*wx*)m_|7F(xHwA5rSqEa-!S+x)6 zzw>UaoBVO&IpuHHZXnFK4*OiAP;hHuq0fnqq;>zH&3^twNT_CusiZCm9oWfO{zba( zmfBE#;Gpc*mpth!-80>ClJo#ARq#DAW1RQ6awi_cST?pX9{VW{wo!&3u=q_I9@s!7 zmHu+M@TYa{&UJ)YzvV3C3wbb{2%nqzFHPwJmHtZ@@m!+9ku3PZaI9Pu+u*q zn?u9|Q^vzSlkP+Bx>-8=-!Y8xAD!YX;dEklkNj%wq;G18w*rRf@mph{{KPD*YSp{< zNzAP|*;zPjwjft*89o>L7rVS8jBDRDPxb&J2cE_S>xva)AQYg69692Of zE!s*~j$V!UAje6F-&>pMW-UHyxRU;L6hk6d4v$Ft1+VHYX-mBhMeYPOl#es(6Bp4j z-wrome$WB;u_Jr7$=siRuCaeegx@aYX6vG)ioUd%`GtcP{0SYqSnPN+6E}8aHC(Y^ zk3BGQu=k^|NtMy18yV3s-t?J|&A~ilW1#C43r;#!2U0qjnJkU!O&JJ7Ceyym9HStW(@5;-PL-ypDj1+T4*34Vz*{Q z6wqLas*e0w0Z)@p=u~Hb#*r!ezw=%a9e2|SiDY6#_-Z1W(|m^`H)%E+tw!m+%P9^W zV_#?$P;KU4=imY$qOCFQjY0TonY3^ITh1qy@_bt-=X^#MPV)t`@|p+5tVRgP4dBO0 z=S7h%m^vzS&plrpC5RHH76?AKzFp!B)iY=(z&kceV`T1DrM+((y9kX^>_nq4Lk?n% zOak1vghKl$_!m0X6uyRZ&#*{b)m6PHoVwwpRq3!aaH7#zfYk%l*zTYI9ccr3TowFM z7k#}7RPlR+=M23V547mw?26=(HeMjfE8)i0@xT_$F8{ZNib%U0)N8mqj{|B6?4*HR zpAVwmwyp6onGZ~RLA;Q+*39Q1|`R+jrQ+08M?To6^P`&Uv1V}3qPw$-% z+>Ia~1#$IyN^2X90AIx?5ne+$-;jGK?3{!@7!zdsVvRv#<#zLdSrLagyq|=|3uFj5 zW&-}!RAb-_f_zI*T=>0(N~F?J9`ImA;4ch;VMut7K@25qbJ`;|ZZ~1=zTf@>+1Q zeG`ZPln)^0V}usUDn~El3YOpBS)&`40+B?5a4`i24A+AgXaL{CO9#(J7G- zn`l7-DfrE~@IETs9cPzyw`%mV#uM{mB+!4v#2xpKB_ccxzTVvld6ikpDjx>JN^``zii9ZB`EQ z3*Dg8Eao2EF+AK)@$^ehAqazLOVCkzpx59Ccib@!olVE`p3*7{ISsK#j(j35DWU5L}a{XRY}4faaPouK^Bj zCr2#hc+UJfO4~0axF3#Ew2eOrYm_c2h@s8EB~T{K)|4z)y%%_qXx9Gyg{|u^QC=cW zXl~~HdX(5wC@%S9hSZZ|VO@Qh(k%Q8lFC35;G^lni1c!j^VIp@*QoEu#E);qVK`aC z+%^(^eBUq`1}HY7pTP%}=G}yNnlO(vz6DUk`u{=9L1g%dVs8*|*%O2`v@lP(45(}) z%}Q7=Rg+=(R#FRAD99D)LBZueFDMb5Y=;{0g<@x@70ahi5fJO605Y+fG|d&<)f0-n z!F88~d9@tT3C(=Z_zu8G;os5Cdcg-dIN%RJ2=mHJGQx^b=@r8}+I=oSKa#5|;4RA8 zZUR?o3MfiwCp1>QypS}<{!Bt)uZYWv2l^WPl;lb4^K)UKIIU`gq2kc`t1jLNWeI@Wq{$I<(R1+O-h%a^Q13K)r_;+NSCLLYfU^SS__D$yOM0HvdLdnrZ`O zir+|LAKFLuW83shr7`M}IOcalf<9vbOgPVRQHY$o})tQRFuG1kZB#pSMdA>F><8j`w=r!a)RY?GtwI<`!5l%i$Z$GOpH^ty^Ej~U)RfO>Xik(v0 zMVQ2%K>pNi%8b?eY3AqgJzOL-&56ZDB5C|RK-{-Jz<#qq3q1x*WcJ#hTqHwZc>>hN zyn&n084+7!sB{qoaE7KaGf&d>2KVejy^;=bkz`}h+w@Ezzo7&1yp#bZ?=&^%qzjnY z^V>=O*3%83=JO=b>YgBcwERb5v!;LJr2u#f7ztik{jCd%bN*Ok^cw<#h&qZk zdg+@NsILv4^86RDRsY?oC4yq2EufeXz;|(xFtRlJq5UYmfJ}Ri$TWe@FO=J~r66#2 zV`SCkQJ$m`=fyC~VUgq=I_!A$jXL;dS`ViF0?T?BP9QUs3+dum8jk8j7CF|xbz~GG zM5&B*gY6;mtmhndERCPrt( z?}8~m7zwdiqXPNg%U+vZ!MLtWXCNtLGwZp zBI~;1Ja#P}Ac-pxq6}UX>k0C5-BMd9f_FexLSG^@CiUPKTth3DU@m|KkApv1y>yU5 zItaa`yhxj>MJV;b*eDAgIWn%<^uurK=>z@`c82ArXzqCl2w=ijh(H{nR49G z_+^6;WMkX{0`1E~yX5hNX^Y4*+^rop)dO(njpx6vAw6}{9j?@#I+FH?WH56uvR1;` z0cl5P7)v(JbUt$gLWp+@x!i-ZmB`Zz(h5krGk$ZGYKo;l-@81$p~uld{)-o!4CK|I zg_XD=%i&pR;o8bkY{fJ#vd_mizk%t`NOV*yvswovz!yy2a4U2n3b?rX`bWUZtcE6c zVDk`3?*8y$%i&4s31Ee>Wbf+Br4m3M3^Gy=&%zM~!cs)4(Scc_=5LvTxKcQ*qSv%B zfvgN;4P^7eNNefIYhyM*+mNp*+E;7)ERXDhJ4XcEaOZNsXcrKFP$N6$Fl{F1u`!7G z@iqzn17tvvSMas5XWRo?$xPUZ>!X6HyH8?fNL&zuA}LBYvkJNi5!%tQa-wrv0{_!}ta*dE~wJ<$zKit)ksVyfur zUme0G8#j>!77V!sQN0J@1|6RDZideaWveW7Xn^k7BOl2W1m@62etcT9;!s*{BEnEAT2O znk4~M_L@zKQ=5@`xCb&>C44=d;0!G%65!kaAjzD@6N=i+v}+e2m1J*cz=C`X$m51i z@^7l_FV}4}0lMS?19P$XD2*%(Pp*raba8+HtOpAUkoz%Ej= zx^yWhI1ZLFt5^^nX^573h0;%hfn_E*jYIOsTX2!Md|@m*6F~aCXP2g@i5df8L#pe2Znm^RQ?#9SAcVLYMgD5Sqne0cpND47zdk@?y@QlUJ>Z#= zED^_t0~CKF!IG+EvY(hl(YQ0G^BW(MBpu_`j@FIy8f;N0>*1RsZMjPoH?`Eof~Yv< z1Qg6Sh>sT7{#_L?)HPlr+KP&#KFZ4nzAX=))HdA5xBF{L{gNcR130R)9)$px9U<^<^{UESml2;{F)!^TUQ_3_KC!JR-UB$aOz2w5- zd(#SiCBy_JgY=ld7ummo8D7#g2ZQ&JyTUa=HcW zGNoVa?8)g&Ls<<^D_$1~E;1pZNw&&3&|$p7%9c!DRE5=WK5M93mkxw^qL18CGoJ#L zeyAh3NOj6oO=}wRe@H{ijM=F=Lv>wM+`e+IX`4?;*nrS1o!6hHYU3(Cn*;l%Ow&PU zxD^n#)#6Lsh9lL!KQMHky3PH}LL+pH+V?vKjq~sQ1Ckx(yaHXU3e~gnF0UB+6I003 z&1>=T18A3kU*e?S$aEi8zF=5E&lT)wCxgBN-odML^8Z6*`)Q3dpLm3`L{~st?4L3n z*J-K+WY{U^C*n8%A!cL;!O{gG&hZo6^CACQkE#|I{299UR{!5|XQvvA^B`blgSVV( z#1N|8pjku@CkxKD654zCcBc1z#0;RwUaR@U*;O#U#0}xeU#X}5eKbkdAryZ-qiPWv zqRO?oalv!Nu#=B;hvLSHF{h%O#Ah7;sWFM0t->Tvy>1W{`@a;JLHS98yJ_ZlPd~s< zpZ2wcR`Q4tK|-yEWR_^ZJtC}T`sAIwR)S)A2tJumY!1}*WNy<``l8_a0BzIQYnfks zVVJJL4WCES^Zss+b@!mi8?OH}yRk)=jvYO;N>4p<=#ebAD8_dg3G;4B-Y}$IR+yZ~ zAZ2F_lJ>5LX3s(cC>8gCT)a-CK;YFXl~$M!f4g#RqS2%z+c(Nkba6T3wE2J=a2ePJ5}N`4gPedDyFgWt)=01~`{ zC~`oYpV0j_n|=pgG^xJxFgkJqqEBO~o+(2AA`|6Lp97_c*Z55>${~^t?p!6mhpa;t z77Ca23%6=Zcf*8`PPJVZp(jVPV?sw5J`20>6@3Xfg;;9`D72MA$P-clX}K>J5A7fy z)Am4`v5DNOR(dsJg`)HtZPH8AAU`S&zQY&)2+@pB@_&uy(RS?zZYmkCAgzSYh}DX{ z-qXk_GoCG}0KY%7ChG<3!o>+4MdF_uhBjfMFszZu_)5~h@#||VoL*Goy;Bui$+c{N_ce@yBZ-0mi$)i5QpC_R9e4=R8bG9 zbJUWz`Ox)QSoLFpJ*o(QCKAuHo#6lyZ8vGQiuA9#Kn-qqQUxn6LR>3(=7gF!C-or~ z;rpM&u?G;DtwU;Z7vWhEX+AHWCdz)Kw+B2EO|&c8k6M?G1W)PC=I0qTbP;@rAMhi? z1f{_wQtCs>$A?4;qb8hV;0M(m>}gIHVHR&Jc1}nH>%Y%xSFuMzApoLrv5K145cMBR zmYPY!LEem$I6eW5u$@16`KaamYnI#{_-soj^`P}iz-}+i!aQj9)iuCoMb{uK7cYJC zDMiVB4T*@ufB!zwDKHiPNC?K8BZjg_rxEb#&j_fZD+_W4fvA?pbrHre^|op)il!nn zk3{Sz4hB9mqs0%o+SiFnz9T}zum{(lyLgw4wm6jUUJNhkgVNu#knN= z#Z>Ey%d@gQ0wQ;#GC!Be^nJ5A_Cm?6hwFl42R2T%{9L=L!{?OO?aAK6%~?o$f#Wc) za7#@RKXWjs^oA58{Te!3bq4m}I`{Bf*Z4WtPe3p8mhXGZdP-sAuOeDMw@qjhbR$__ zbiXviSEu5^zVG(ZFEe_JPjF|J)PC$me!rBlh_jgrk^KWIlwcWVE(;fU3lyni1Y zx4dCbbGmCYWo-B0bn{P3*W8pt=T1C4z0P_3cfhe+^qmzi^z9xrRzElmI|#(u50UK^ zm(lq@97uZ97_U3^l^^beT|Y_q>Ts{)H=z-E%-vuQ5Od+wkLaJ-oQ!MRW_+P}-WC*H4&$k||_D|CNvJArH$m-~!jt${v zFEJ0|%oE7h?bjA=cr$qv7rniXgLjc27sdyePOUC*xN5({(T;U7(Zanh=40~}+ba*w z96FLsWXya71lKu|*5gf)^$`$+xj zZyR|%udba8yR-D8?1!uH;I?HRH+6HwA)mPyyGCDrTh@59@NANC-@_kf(9>PzCe;tm zWHsztXa_Z)8*IOR^=9cm0X07(N(tQE>lRic(r=_Cv>kaqom#(NHnU=?!E49*(WLrqR~+#6)jOs)C>;7@q6-suG{WN@ zT^L5y5pM4APcvSx+J4U73eCG9s|inI+gG>SK=}!^rMvZ?_vy|4ic3r}wI1ZkkL^G# zTvGV&Gt<<;FOPtyW(4Hg+uj{Zf#u*13QfWtn!jb|AB&*s`%kAM5t zzzc#0i{xi9xwmatf z*l3U&M!3$druql;iter4n0=w?iL@-s{_^C)U70(4j$V(>n*>5a_U#JXexEW0d<56(_OwIvZ-|B z*Rn#rJvM#WK`9nDYih19*&`2JKbR5N99<|02|-BLUhE8cwT`UukfrbgrEKI(LNO01GqP;8R|IfyaDGG2+pUlE*F1ZLuodicZ|(i(kBF zi9y+Pe&cG#AgV>$?hw`0rLeNzm8q>4U(gOzKOkfXeO=29T^EHr`K>z0ojIlo8eL&5 zB3c}hzx_-Z+2NThmxHe&e2T<=rnG=bUaolQlX0Nom@Y<%{d(g)`&x1p zmgXn)I_zcR_POCDI@8MtBU70_kv1nk!R|N9G<^|UyU*3Mk$rVGXmY?LxvG)svE1^U zviYLvs&k6Z85ug^RP@mmZM!g!Dzv{3KB&l9chDVR4ySqJ97jE&kXWm^f-^w@!w8w9p>Wk~P>Ghkaqw=;+}8c33r!B# zjV5g{7FJWeQvWdsQ`CnA>LArQ=0D!FKMJ2)HroJpdH_|wg~&sl=n;CY)QbfV*z~Ty z8SIUeyHyxBxX)_`|5?bl%| z9>z!N<&b2{cJT0*TJeqIbyBvGBSN6L&rA)gq-?<2RH8BX5%uc*_2Sh`=7jb5I$i!;1Ufv6-ZdEj?kHix+c9^5SE%{n{I~!j7~&CEweB`5y5J?LN2JN4u8S zzoky@+P6l0g0vs;T|NQ}q%i`}=UOZAi&5 z6A|+HZdhb23i0gxk2ve&bAQEX@k@AebRR$*HS)dd5!o6JM~qsNpu$h6fA(MSy}WSG z1=SGETFhSKn~-kj=70b7(Q6G`U~){W8(c5`XSP+qulUm&{oS6`8D!sOugyxx zJjZv*(A1I=oO5!}AJX#FC1=}zX!4IQ{k6^kvv{0s`b3E?hraDL@@=}j*u(#`I-8|FbC0F}HQ3(ly7 zauh!#!H%TMN=pX2)Fp-JF+3+u8*NVCT(FZ?FTkfw*U7q3p6@r>jSVEL4J9`%d?dkV z^==np=Gl9m2+(aSm76Ke^TeGy)xe4wb!bUYzk_uf{;~Ud%%jVsyH)3;sE?yZOpkkV zx1Uq^eFPQ%F4$=;dIBrH6O&4N%hT_R59kj|H9ucEUn5-S4^~#s4NxtM(Uvp6FBFa* z2{o*K#EC5pxv$A&kb;U`QlpMVKErzLXh8hby!jejIvf*fy8eY`1Lo0<)xdp91(w#a zIDr!z?T#%SMsdxIeluFc_8aa`pFB0U!HvdyN|_vY*p8NI?t_N7)q7}eTJqq^C$Nom$t+LCZCsW488`d9_q~C1t8;qY@PZy1UX_VcaC6T(8(nYLf zZ)}}KtZ6U*vnecIgKh&v*vmzaJ2l;VsvlXSj5a@<{uVp8o~O1pG6|TmwQ%zn^7nH) zC#Dz1l;xmC!|>K~~ePCFH*U!9p(tx#E9q>+;b_wWxf4(9-ex8||8=8Wy=)fk+g zJbv`6NSKe*2RXBVoxqPOSg#zx^1ZkcE@@9NPm<{DdWMd9kS7hS>~d zNW-Q0VcV5Qod8tYIVE9i<;p{>v2`oq%KfZ!?3x^T>mr7#!+S$yS{VFez(=?!dr8L3 zcijyE;04OL$jj=tl#y{aT=1RIxcO&Go~gfkZ*UxQA-M_Sw&21V-iT?5g+%WtNz_-j zVN&&Q>A#T(-kOyQSiOb+%+>NXu9Q&xL(9ghdYv~6a)GeAkcHVy<{lG&RAF@-?g;j8 z7gO&d=I&Gbk!91a#nRn>@86d5l+%8;3{}bwcTz52S>5s6Ap2Aqw2VC!K?_>gGk|Zo z`(b`MX7}xV$g;6aiQ9(V8Rlg0IH*oras;ikTzUW3N7Tm!`3PmK0QKmBI%kQ4gIyvg z&Qn-U0)SM?Rb|0(e~J3L$-;1ItH1dHCrQ@^I`(y)j$ZsD$zOKG0WAYtsRoj>QD$L% zKJ1wIqUw}=(-p~SSq)0j*NRN`E7#sSiFRB7*~ejfWbD=g|Gt+Y7~FVubH z!1;n&Kv2U?VLKOoVJ)C3x3RBjL$6-{sO@g*83%K&wcI(N4v@PY39Khe@*X)fd|~a> zbRU!jv**leN00P)%WgDhpZ(3(k2!P?ac;0?y(TCs@3bt19in|M+=^JQxfJDjURKBk zE&~~B5GH9RkbdLMK2b~jo3*Nz1mJpy0X-4Cgj|FC8c4Joe!iKJk(MXrU_5V11~Fbw z#4UmnV?IMr+DabAoFC0YJ9bJaoQMO%Rh*~;(+B9hH~&tzh_;eqI5@%k-w<0dERwD)z+vvgjn_gxE^u8Zmub{z!Ad62#{XRgTu=0foVB+PZhI%DCd z94EmbdlSFTSip9?J7BhlWQS|CHL<|Ojr630k9<{ABa8tiyC`*&e3L(Ev=}MuHH)={ zMjikfZAq_`IN|DarCB9qV;1dqCcoNP{#PB%40Zj0&|L08HMJ93<{-W?G1LPjQOg#j z=!g7o25LOfkp*NU%i(sc5L^`et6~(JD(;jQ6)oz?J3?@v|81-w+$xJ+Qk&+Y_-Nqbut)?_15{aPH60^NNhE(2Q)-V{GUx{J!%! z(`&{Z-sZ5!oa=@iP%pFjN!S9~^dvU?N!;G?;Gs!?jV-3F;$uq+Uu5&6>(WIH8aL~2 z2DPV7%0p&J+-Ol}NLyw(bZGJoHo7Le6_gV1FCgC%s4MLE>(XK|d~ zulAW}o>Pg_X7S(bF;U7nv1@@@jU@hOW*0T4&UKxOlS`U<0!AvxS)#c&6@Qk0<{IwJ zRA4=LRh2skv-psc`))>>8p4;iA1MInceL-~{_5d%v5u2{7&^lx+B@nO!F3Pg{w|#w zKMPw7oDx4MFbmQ-h8v38W|LJ7|L~xeK~TT&J6;f%dx>2mJ~>bDgY`6>rrDS>P&-aL z>2LZpAK&X!7I<|jL?DUki;r~@Unqrs zDz;fs5NFkL!^*$6&=PKyK)}>$R!f2>PhTpIv$MIKX^w`AZ#}PiV{$}%OZ9KPkCO=^ zvg>Xje^O^P%8x;ZxL2F}B?*ome>b&|lK@5e!CE0{9RXiOs0(N6wjlO$6i52vd$Nxf zc#rDd_S!p5pW36-;~>;I3D*OZWpY^5KYKeN*DC^A!}sKQ)tP1j!&8>>g*BU3#Ur6z z%Bi@Mx7wxy`iPy`R||BLo;)X)7+6$&bQ8au)D-9- zEpa*PbhBo)ML~Lt6|aPN-!cErY`VDjRPA(J#8**zp(lBBdxtmkM)3JxXz}p5qa0kR ziE;aUe4SlY()v}g1AsYjRy@|XAtggNXkHu_eNBWGPjfZzPxjO{?*6VR`0^``cKAb_ z{EGI#*GZnb{6WDCPu>0Gk$|UO`}T^Mr@sE?xSXdxle$xVd8}3lMV&#;I(m~93_c&F z-q|^P&GXjo?Ss0xV_;7<`bNgf1HUsIleAwb5_2GzZ%Rb(eW}WiK*`Lj= zu08TTkrO)!n|VHP?=4Kr)6qu_&D|~ayj9h)z7T$9W?zM@ef3oC!M@#@AFFGk!V(D% zF}de>5A9Du4Y~-+1DP$8MZg%4v%#v%GUT(@_=wSWZu9c3|6T zF*L7)AGTohTRq!wps&tr3oHj&?%0pI(m5gpoTWI zjB`1%^4O^Kf|pR7sF6n9^MhRPikfccwhq^mkfETQKM-lUCIdhD30m$r|{+-~=a>!*r~q_8`M;e6rLRVg3 zbUd#+E|&JrscC4isZC9m?E^{}=@p=rk4tEapGF`*th4=c5jB*bD8|-WSDh8cXToIo z3?m?RN9}j-6JOT!DyB?=?w4JOTtiQfKYz@h52WwxdDXG^Ea;KXCpF0re`OYmc4^u- zNZTJ?d9u_%>c@fU4#FS>6-Gt!{BOKCT2o%j)%y`xW8lC%e)2@b5L@Q9_3YJYy1+DG zr;U`i*0-sdZVsy{^p(%e;EP%bXf1bdbaA>5iCl6k(wB5!cV?v%irXliOGl?dSVI8n zx+fePLv#VfX^x5$22^{|nmZq0{{+Opb}y$ygQm!b8E zf_S0DaS4ZXCs7IZf+xSJgUo#~} z0zx1%kEIo(RHNcTKJC@P*OXyxPeNAQUz`EmgB@-aAvYh+0mgVK51|o)_n;6Z4Q(Q& z#p^rHd>k1r97PCrQbULMNSgBQl4a?!8XVxpsBI&IpXC~NGH9jK$Gw&GU4Yb_%MU z)v$(IojrbW&x@SvG~+Z$kr6q-nd2ktp4x! z*f=-x9(weUO7#f+&+40l;u52fty?Vr{`ctawR`_MsB5apm3a5_ftFfgsJ>07poFuREg^-b$BhwVk8dD^=}gQZ@vEV zh3iKEJV4+XGvdja!m2@0ypeQ41O4{(3Tef++qW4Dqw5%02=EtH?;N+TD*iS*xIXM= zmu?$V!w4smYiLzhI|N3j!y1yp4hK9rr>;vAwccigGFNKj(kx`gdwEK0{=&NqPwpMJ zFweV;h1=?g${)JeRrw$4s_;t}t7LwfRUUj>)8JvCVQDNv`-}A|Rq84DQtxKR@4xM_ zZqJHO#fu+ee}{hO1uQ+10{l>|2yTx4EVZ8@W7K zhKGA$LPkD6HW18 z$Y2=z#4&b;&iJ7s@xoy2y#@M9wb!y7p|ThbcF9uhqIm44FS{biP0bv-c;genwz;1& zV0ygx-MCWCT;HpHK95BO&%t`D9*gVdoov4;{GD|7DEuon^5VH0tFNfI@|P2qxXr5C zD5;9gN~%EEnB23ARZ&`@?aT(6(~vX)u-#*X%wBHH>l|ZcEXq4R`1r=wZ@Lhnb{}ZS zpwk~iUU~1(&ZQyO9@#BpGY)BngvW&eoUO4;N7OUQF6+o18GAfPUaObpxH$)^S}CtxL_F;4#Cnm; z#A|-au2QVvn~Ma=jC-4s6hVtmx@Z^Ur$}c^fnvaB4^pi|vC`@T)%P_5mBl=fF6}~4 zhpnj!%^C%rzuy`!!{t)F=e(N}Man6K*Wqrn23V46rJ|_bm6M;5Y~7M^}Rq)X8=E(Z7##b?;hBz zy8&l=-^tFSRW~3h9seKUt>6N`R1mH&L9?*u1qt9DpwCgeLIYGqcRzkhu~L9BOG^%R!1xS3qdvW5?v zr2q{;dYOzTRF~=!xSsC=7Czypx6w9v(~wNrG>;9i$`0k|z-Gyl@;phfRMbFglx!$% z38(!8SG&?j!ftU@`U!zx+5S!A3A)^e64ZOP&XaL{A#C)ttx2-C2?U%QmG1=O>Tb2y zIoTa!o$yXpYiLh$g*;*@A15CC5!LR*8_Qh#!rtz89LuGZ{$@G#BlncI|Ra z#3*YPQ{VEN(P~n1LgTI%TN#x`Gn@y>cuDX!t)yMmGs^G{)b{QGvaVeX=yx9CJIBuz zRkcB(8tMs6^~}d#qeP%esqr+CM9&E65x^qIFY*?ogS@I=({HF6IMXMsPmt})Mcj~t z=Q*8=KrP@4q7}+$A$oHz(o8$6O{rV$_|O@8Q?XyxkpoUdJ&$+o7S3AM*O=s4YmX=@aDzwZw+q+ENbtyl! zhod&)xQ159@JWL0i}M)j0h0MHg3~fb8;JR+-b0lAFis+fp_2cn!hDDbsHjg zzFms2ZAtn1&q00*=FA=KX&p4M^z`(Uz~^*&pf-x59tq_^`w>a^&-E)@R09Cm0w;_2 z*qf%okUE#WuZmY~xVl7fnBRywGbxDy?S4Pbrv1?bd|t%g)cz1ygdCYx+HQMZd5CJ$ zlComNMHd;=4QufzWZdNi3cfm6OEX)rSmQzdQRi~2TH!Q zD4o~9IIjgxKxD6ML~w+YK+4aT!MDv_bf=QX!ylegyt2&sPmBWNU4-Zl*fk0SJATN* z47GsHGczgv_pb>LHI>aeL1QCV9PhI2Mbay>WLH7$sx7$t_2BY6rrq|`RI?(kJ5oox znrNo=kKFbj_@frmM&u$|yJo4$Ahe(PtP|My&DN?VrIeOT2V?UpZ7n!$0VJL}->?*T zkGxaCdQbcd^NUC7oDXqaF5D`6CfNSf09-&r!n`T#d_hF+$RO@p-eNKXsPH0YFet?OT&x`&fo=DXmiR-_1j#`+P=TRf~fMh8UTm*kYD*;HS)}WH)?>Z zyNs2nBj75~H)n3BiY%1-!!)l|hv(Xp5y4t;u!}3<=QPAoS8vD3QP}7u&xrlMi^O3l z#s%8EpSRO8$%*y^u&`W{_eBX;%IMi{EL+uwKjvp5fxiA74XO;^4k4+qG5kPS7#35lX3c zy4_Bp83>{QBpW-#+9Z2v>-0-v>jlszmpF<}AlYzqoKBT4Vu68(k>Av>8nV{J&H1?X z8jQHGHEeJ8*f=w>TBbSkAcd4}tr3bOhX$HUXZw-$VVXMDZ85F_dZ$pT9VXXrfz*)h zqzJlXA@aRenpTnp>=zfCKf1h(+qh$U8P_EON=`RK$9i)xVDniL0Qe>&(sSt7WfW-YHt_(VO z{WBSTkXecQ0YpHjle)^e&!+FCZ6>8SeHRLsNzS?<$w0i ziRx*`G6$%Ov^!^%ZIUz2FRw2i-t>A)@vUic9lp<@l3Y(a?uIR|>%H=M1~!Lw(pb*a zc@@LnZUmDos?0ssu2g6PiH&ZyQ7OYmH2D&0dtt*W#Mx1`U zA^`xH0w6zs;JTWqDyy8DK}0lnr}0cj85J|+W16q5@Nflg5>i=l6`8z7$(5C<+NkP` z%UVckS~@TAunb>AJ*K%r`-aa$43IA>UudqeG$--0#(L3jn3UHiozGLI<)y2Cr#;Bz zlT$JMq&0=IphehKD;FeZY z$C1tT%!Uj;3o2O49>ut4t4ta>;agAQN!>E_%cvaHa69M5R=DW>am87BOvyn-G>iDs zuZt;o@9C=LV)~4mm=6(tqv^ag3gGe+c+NfUWfL28OFDUWIEJ z_;tN5btI|}+Alh&DWNN`YMM1K@tU4$?Mn_}rLqcmU=T#xiORzSWztDOlxNBzj&6&Y zXcH&(sX@C4ee_bJT))}D9OVd|Zit$Pa^9~NFL_dxmMEN}p!YlvYnGzvxvK}l9x*Ga z(~uu~Utw>m3<05md>1`EZ*-E25q2b3J#pbDEs7mPs$$8suyNf7-c!xZ;D~ob72aSTd7Vmx+}GI47xpEH zM}spUmwLokzZ!hH0-TjEo}j0Y`W`P+#|BTZ;)npj>48=8xQ^CqfcG`#Uj#|IP}|GH zvlgqVS*&3A5(U)`SQP(P=zb#x*4;VdsmK8=h=dg#VtqsO)Nv7a;G=EIKm5x?4gfp7i> zy$e)1#S0(KhZ!SJ!`DgBmiL`roYZ7)f=K4ukpAV2bP=u4QDw>X31oPW$25{AO_0FY zwCz&=J1O8Xh#WPJZlHO`0P15Yh#%SL2w~Vs_@_2f6z-kydjyZj6@Vw)kDy=tD~Zqf z*GkQ@(0Pbpc`-J&_9WF^J|7xuB$sd%Z(HeT^}fezpyxy-2LGzS{|f+bGU(k4d@u>9 z(O66^>1{(62QasiccZ z2uVjtQi*aI#>^a2^tww?i4kQ&8lsEa9HkItFfq9f1~X%1W|%SNjG6h(`~Uyf@4wdf zd)7K@?e#o+pJ$)FpZ%O?@AG+{&Zb_G)*Fv^_3K32LhU9Um)wrPFCFy=eMTv-aS9f-9^b({{5`C1-oEU zvt9~7Kq(^#vQx01U8!24d())b^&`bbY1pqDV7e%gG-NfoIfr|)aHyi1==&5jq9!b? zn5T=9S{MyT5$-56hl{9e(qO(6aEFtmdnHjVM%oIKNqf?)my1!2AWX*o1rpFyEG>m9 z^O4p^+fJ^V;OL!^UoxcU>_Z*MMWteYS5LEI!L+(W?D-ZGIy8}vAt1-ef2gnNK5v0! z?9hI+5nU|~96?=(b%ZOTArHuvuNhD1DTdC;uRSFeUX03xBfCwM*r?Z- zc!FjDnn=M9SPg{W6}Uc&5>T8VTPNF=sHZzL3=@=FMI#_M zk(cENse}kUDiE;(pEO6?rE)nZ_=$$}lx$T)o8?|Y-XJ67=)wRmbnlQMKA*6>DHtK! zRHZS9IOvZ$2O!=d9CRueYLTCy!W#=XVbZ`H33hrC%#KYO{mki1J3J+*wwqbM?B?29 zU?(`9MmI`9PcvdsnObC5)Jh_ckXtMwJEGdP$>*R|v)a=P&KAZ1gVwZTsEn|UfD&`v z$`muRNo>KC#diF$VJ$%f3iTZc>Bvs~2)M$WdB7?~Bnm(=kO7jo;>vltz*FR%fxy=% zA;@vxphnr1F~%jyqi+z2xGf0;gMeaSg%aDmT>hi`C*ZZf#X5)zRWN>hUeow+()=Ao ze-{BU4fzo!TF_0IF>!zc_w{?)ixs@ z-oQjF{F5=z&Uvx5EZ#tCL^>Z8WjFqQ)F~2Fs(c84?Z(s6($yyemZZurf6FwaHx?tdNG)m^HGz-?v&G1n zxgGxy2aat-gArMBD_*uKp_*asM7#p=cF9}^LCQ{Eyp*~F+QbO;l*F;4RYEsOa4`;fP`4E80 zUe!W=w8>BIY^^%~`?-P6@$4Jy<#=IOinreB;ag9Sj661Mu3Dy{eq3+jqUWiMZLV+p zO!)qrUF>JWPhE{S{%{{-ZE2ydn}2C>?b7lo>KC2kWPeO~2(T^s3fGTJ0IQ$omnVk3 zs?82vZgsB;*=#EpHm(>5YaI)%Z%7a(WrY(^S1(M_7z_m4;0-fU&ebT zxm7%X`5A`Uc`MbiL1udVR;1lj=fak&m?z}L<-g&9&{c@tXFR)2CI;K8Ii{(TS4tw1 zYh6Q<$~%J_2~Y#OXPp``4E%JetQ@$bRT>s^jA^1N#3o>jtM+(P})dktFqF1|biD~q2di>7UbD!HYW+CJB?SJ^w75idn`)}vF zh~w7U7LBO#PMi$?lvma@wAs+&^`o4|!B3B`N<%6Luau$v_WK?$Y{Ha!ZNEU8{i8Yy ztojjqY<4>C#=UQsj61Fkxq#+>3M1}f2OJyiB9cO^cP`!1TTo`Y*!i%w%CRq_k=w6T zO^58g7FKim$&%U}!Ecdq(2X4t*m`G9$Sba~w$-7XSs{51LGhcc@Bul;F#X$UV zJ~_u|r~4m|hUGn1!fBMk#@5e=P#ro_qWbXn6?yk-f_qHOZ)D0qW#}5=(h_x zNq{I`8bmMjyG~=9CU5(ESmc(A?lnC>+ldUffB!R6=HxxQ%kkvqKlxW}2VI^%tF)?h z%m0U)Ygf`$*V|jz^ZKb2{C>;q`gEH(_^f@UMF7dXtg8a4U-shO?7V@LUm$mXkhovpxl1p2)^xM26)u=vxn7YD(z=hthRd9lS; zJ%Y=Vjk>V`&FlDu(dxp+0C8l826<#!yF}-^U1Y2Cv`vG~d-QL{t}8F}O6i=rH@U;ZCWbKB`KQSbJR5vzd;T+(NZXq5Ik;y0 zN@0H%T>SQhcUDt4rY!~}Fx^}$-k?Up?$T1!F7T!|b_7?|daDb!4)Pkyz3;Y&=U3g- z?RXO8xo5?o6*CUVUwW0a30_vQJJd`^_mW!W3D1QS6hoZ1o=yb=8ZVLK3y?VpP(gh z839uoL*Tm6O!}VsE)b_Pk}OLct|81YFbE4Cz;fR`js#3TJ5rPr% zix2`%)^X*tTCMoS<~^`8=4dw@Uz(7^4|{(bq{Br##Cb-TnP|n7B&)~5$Bqz)r*;2! z;(AT%w8Z-S^r@nR*KB;7_DMa$3_gyy-N75qXWS9VMVnW_+FC;I>D_yXBT7oR@|~*t zD4=KXL0M5P;x55uCGj-Gj%fKk|J5p_11&`d;VkOUoLm2C@hbSAA4+ zySNL5KcAd>bXaZ}dy*!%cq`LISg+qTp1|Whv^bbyLz{er+Q)x{%fP=Atj)5W(i}(+ z_^kLbbY|#r!%j^C~HNxfVA- z_-O84a~y%z?Q6Yc(Oh%&D)@AOcjs>JK&S;`3!`E)i65`2X0w&4TD4fYz=`o%W3rk=Us z(j9TV-+%c8t!yq>djnjl6Ss5#uPtm0452{FJ*XORxJTiNbL2H?4P3RWeeq%-)*iYQ zeNNzjBrj2lD6Q2zxG0YTDHHoB9*D5EY3k6*dJ8$`S2fFb2(FkpC>a+2L{Vt@0<(ZZ!C3*@z+?H_2&}UCU^IW zwkA!bdu22j4p91KfgQNV#J}P*h5e2@|0VZIg7T) zU`2R(d3rWWzF|hVEIG1S73B+N-=thlNdj5qfD zv$)O{$dspp_cUnZh^-nUCW=R(f#PhCdc%n~>?d}<9PgzOzTX?aPpiUos}N9W_i(40 ziw5D4vl*%ZejULzTRb}5hbJY+QN&c^O!mjiiX97m_fHNR7)T2I#1BG!yQtj)%Xnu? z36>5&+W*8!7%FZ)m5BLK?s0X4?848Yzl|W)ic@g)mgCK>12BQ9FC)v!%#D3Hl~1*K ztHGZJa1<)Y_LYK(uVe>+(&dho%Wr@Vok^&1Mf-NxN^a~mnB!dTwbyKTRB{~Bnp?aH zu2sTIW{%}l*rhZ^}9Eq&@P&~F27RvneB5f&$U zq@-^=n7j>qN!+bgk?!%(Lcjl|tV4Cj(CsPpM?o8UHB5}z7AHWq2QG5N6TL$gU+be+ z+8jd-Z>5GYpnI=)n)8?I$V-YAsvtA3q9t#}s8fj+eLA^5f@1-WS3T-(56vE}tB??y zwrZnt+!i`8?-XC3G<;5TdtO|-)%P8|+H>Ya@=Dxh4~~K(I1S5x(C_CEEeNGMtIRv2SExkUkO-q>OBwf^YxNi`cCuqsM;(vY&T>OH{PC+Ewxkz#{cxt zJfKau;*ue5)-)VKlJMN?l9bed6aEcR!-c7j-`fo7-{1jqO?$mJ7eL_A5gofvsSr|Gdh-YTSfNHg9|6| zD$haX(mwgY;$?759HKZahUNf_j{#}vNVQuO)N-Yq4x?P&Y5Y@H0bIN%CPyUEG^DJ8 zclCRB(HpWwp^GElKM~sTg-S`)*4}f6k%MR7={c@+?oHYxLK0PPDeiM6~d)Vqu?LwN-Zff0$#nm*p!*3eCNLiKyU`&fY} zVNt&*S7aM<<%v1lkO5Sc;w;-yqV^lQ6ifsDx;3Y92%4h#jR*gQk?zZ9fE=AikgBbY ze8t%5573>ZXsKU5@PY6UT4W&E4kjMGF-JTE;T90EpTXqG*wQiXxYQn+ihQ+Tx4}zk z+&rG6)1j7vv(oH*prT&4Ipt{8E`GZFZ}5R!Kk1&*n2h2`3xj*G@++WaVhO4jxT=!V z40I*5T6||*b?RGT3%=551dEv8-qCamEp4>N9Ir_dK1Ms!Yo~c@5 z*rfn7(B~f#8zsw@F8HwT90wg)HA~-z-&7z zhCq6DKDGl_op)5SYqxh?=e~JR$iLWhV;U{6Eup(yGsV9 zXn(c??8zY3y21TpIOa%miz95R#@eUN2tk@y0b3>QW4!f}GtC=wjqFIpN0Datap9D1 z(UJD%JlSeVUnV&b_ZHnBL7|gS)`%$}Cxl`dg*t*%+5}dP?8fqTM?Kb!9l#&hgjGqy z*8p^EJ16wCrH#S|d{3F8u@ZZ-b2X^Mk(Z+JP-~y4(H0hp|KtC;nxFreXes-uyF}K` zNeV>LL!T19iPkGM8~aZ_AmH5GQI3vV0k5Z@T%@cJWX9%5+A{OrvMfpXWAzD=&!TM; z8|g{UX+MfMbZX$OSZ}5G$`|D`tb(fkMgtlk{4?}?PR~ZsSr;QeJR7@(cxx296*F6` zlOUywkUsOz^Nzu}aH-OBqcs-K&v4q$)}NuE^|llcxq9xwMmIR{97- z?TROJ_ZayYxjW=-$s{0UXGLz0n%{!kWzRJ{>^AU)f&MR*q94cWi5!*=IDod`Ym&Q1C+`;|!YF}#be@%m!AyyEs(n?$aLJrw3l^0&K8 zL9a7N#3XBolFK1ynJ&oA?(w1i4Ucng_szeG%s|sG*R?{^udye8XMPhsUPHDfkv)1# zyXnfMM00fGZ9eV##YxT-icZQ$@5uJDF_U=r;MSt{HI}0(%;!yyb>t5wy?Py(ut(F@ z1P)O%IE>;dde^D1n;hx>p5k5~L_xb=y`-5m;KN9YbPT71$&*oD=l;OH9b{|7YpgFN zUg=v3d+*yrv)u3!i-`}jvIm%|S?*>7+^bCycoVj|*%y@bLSsB-S#TczD+|G(^p#!C=z$u~FmE3QeCvMj(!AU+~ z5jHd)l<0`qYbF-Pu;KAhKIm1-Y1Vb)r5Z~^?CeIhxeI;ExZ|_dq}*qef9=s2pngtWozIu$5OzsS*NDJJJE*^ORBe+bH`-W2Eo({x>HA_5mpET} z-Zh4_)r)^)-FMQil8VKjq{5vGefE?AqZh#P7COGqWdjm_a9$KU1@99L{I$_y%3uVv z^hwaRX`pG_<}+km!zr3A2W_G%GuL5N6*%K28p)5xLbq)7&S({gyNefH7}#!h zzjSNPA?QM#Ldad;JLBeawt+?eT*9A`O|M*VcN7<6{Yu+cnhH zNRo=O^!!%i#DU>}V6X3gZW-?T0``)m>4cx8Kn@+f8XYBkuOfEr-^%9)+P(y8(KKC| zbK~0ei$mtZyU2`{*p~ogBYejX?A#%P2MZnH>LHiGm|gutE|tFDnaHvP;Z|{=-Al$N zG&k@I%T&87cmOgy4yQ=zD=PV6<^BYrO>TpM*8b}y^05skFM}3W9uhPLs`5l^ zMl!-lF*euUJWFC@2>9$2$MN49liK6h!LFJr%INN%V$n`aMe0dLKQoRnXjfiDM3eS@ z$>!&G(>)DtQld@a_;?1_wKUK~i-LL5U~>Rb+1?i0yX)i)tOnaD`Hy)~~3G zsnDDDut`7S+}+6TJ9gRD{V%Ar!dY#YU~A311ZrF%Qk0Bho_hcu39Wk7F15u!n@(`X zg|iCXU(~NoC^K5Pb}bsOL`1^8U)_Z zd?fPALYgjLC&-254;&JZwJa6@y>OOc?G$qUPXt3g4gJC3@G|MfdJ%U)_bZ6*Jwk0A zx(=$ghnhs#CP$>`(*-0T4=z}LF1pr)f{7g_qQM2yLkUZuy)sYhiL%on0V zM`%}c-X#=zc5!Px8Vp%X8dvmKOB|K98kaiXti)w^D}0Op7x-+k^Xx2F;sM-YpA@D6 zhl5VL{yhGu6P#boz)P7tIm=v=!Uho(F$inQeK~c+v(S_-YwyK(J<=rd(6-zBCsJPK z35y_Qj_crmf@e!VzV<#c@1K;-8!B+N<&!>=Z3~>6bg2&-HVIx;N%}9ktz7c-)+i=^ zRHtr{2x~)EL%waae<}|DEXD3Q1Zd2E(uvv&Y^(#xOji2pgjB8yj|;Ahlix3EMLOJz z-uA1(8g&DxX)BKEXZCQDc*)NaLs=V9>#m2|0Ty_HgFt#|>_;efBWgX$|J(DJ;Gc9> z&P1Dk3-G83PDzpqzIc6ue*Eq7Og@zmEEmc?qHyxH;I}xPO2w4mjQi(9y}^E3Y4G}; z;CCWY)|RFa1cEtxWQzF}x_fKRzYs)7%Lq99tn4EcEhB@{K?dLKkwIM^aYRt}xhGau z$5gdH2V+sfrDn22mgy^Zf^P?w1cMR=Kopmps9uFf7P?FGPV5Aww4Et&Hpb=_y70U d(!}_`8e5tgXI6zATV`f#vTWC`qv&JH{s+F#KhgjI From bd128ddb18a967912b63d49e2d9c35a5bb662d6d Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 21 Aug 2025 14:04:10 +0200 Subject: [PATCH 059/161] Changes to snapshots --- .../generate_qc_output/test_barplot.new.png | Bin 46638 -> 0 bytes .../test_excel_dIEM.xlsx | Bin 6752 -> 6752 bytes .../violin_pdf_P2025M1.pdf | Bin 28932 -> 28932 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 DIMS/tests/testthat/_snaps/generate_qc_output/test_barplot.new.png diff --git a/DIMS/tests/testthat/_snaps/generate_qc_output/test_barplot.new.png b/DIMS/tests/testthat/_snaps/generate_qc_output/test_barplot.new.png deleted file mode 100644 index 58f016c18244ebec324251a250268171c93e9633..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46638 zcmdqJ2Uu0ttg-BLF5F}U#sDPm43@TZYmbYME}NpW&C5)71^_rq!r>n&r9O`@slmq=QM0dB<{n+|JFIh zh&zx-2TA88|5A1e8}4@1mvdZR`B|^M^9jXy$*=Ot_llZJ2IbSUbyVH$gHu&`5`z*? zjihEK#&D;YrSKHecADjMxi1wc64LN@#WMBW> z>`)kAr0sXUtHSR6CnA5%nS3zH!nLfvPBaJ?ZTS7?i`^8`>(>4pzrFnE@1M3X@7=ie z$LGR-ME(Au@?HJiYk#~kNb``mWxOsPxOr*qPo%^%$B31|>$Ls9(uWrQ{ra^;ePVNh z6032UWIuzjUHsx5yP;3D?cLX}26O8udK8w<4{{jK;927Je7B3SBT0abn>)48n}O<1 zVo`q4QEu*_UW1;;)yG0>t9_HI3~a6#ul_Qr`t`j_`Tp_oN698{BR%G;+C^`2e6?L$ zoazev-7fEjdq!F9_rw3L`L{n`dpEIWgPY8n<1go4kd*YAcstYMS|614wXZLHs`2-) z#2wu09&N%;PfnGOvsG32{ZlpR|K?&RHNNpH*FWhz@vYQ-;n98}zsHXsM?~o6S$01< zaBBNLUW3{3FBToy8TprcrRV16?ou;}IgNkjDO=vI(D-@M@!v^i?;vMSQ1x z*vTDx_BhUr>V(Vh*}uOlQ6DG1tElMCzy8LAU%McfJ1j;!NO5K8I>@ z`q1RpuV3wlKF!U~YtFsmx9G5|d~`rMs$sh3JDFS?Dwx*g`*qA?riek%GC*YMTa`cK z%8Qoe$m4aIDXJdtJ3Bj5_guS-rI|3qYH%J9@mzN6uM1C^EYK-(C|jKH9;sJUR?aqU ziY>Gs3RE-$d{uQryFlzbm&H`1XZ z{4_N5l>MMGht5`N>XoThy_%863hzCE#_m5W_K+j|o<24C*}#>6^}s{RwRti)7q%J- zH_mR#YwTMoa{gsB^eIKpb7^XL*qozQQBhIam$AFVb*8ylS&N%k;5*wGN)cb=+^!-= zt6$%15fqveFTSoE3z4htJK~?9ULLgE_TF58N2f6RYULw5iJV4GF|pEXt*MlIxcNhC z$+~>Z%*=oOY0wybo|;kQXx`5+Z|*Kk4*VLh{!sNM_5!f$m`&}+nz(T;HL)o(5!dM`**L|k@fLmlLZoHGODS#buqmU~RGp!=jH^aQ zu_K}($1Y147(|bZBuV`BS2>*{+yuO_q zInnLdn`6;AjceULu(lsB^4+|2@7uR;c-Y!Pe%!~7S@*s3!2Itbk)C@We)^_*%(*_vcxAXF5G7W36VO1|)Jg6hU%E~Gx=Fy&M*wfQfZHtY#YuB!; zSL^$}e%-W<-r(xh^ENg%Go#&{w*d?5EuzD&D#tbrl&&lnaA9ctbE zf2`h|2=CtGi4L+soafJ(NB3jg@zWLL)3 zyA$uRqE@}777ZT^c$dcCP*03{ZP_*0nr5|>Fw~sDcm1oRbE^rznA_~fcb1gQ4r4u{ zMj!Tec2Y9yx{jRizMM!m>9XY5>mDbvbKgGGzU*ig_t`IZt4{=c{}9rYq9r&KH#;|{ zLB;F7G}UVH!Fw&a$Po8{lECJ;GL`1p&RdwSk!2Kh);Gv;3=1_kVdyO&XxVjfRP)1o zjYLLK7xe;(6GI6xFO&1hG_R=c%r za+>C0Vn~&NV(OmeWR0xxachS99Chk`lrp+)oR(g_c~*J_t;v_7?$`v~{n=O9*%p$R z?)38t3bAa>+^>EP$2{})D-8$v_>>AdOWj?yxUXgW;Xk69qG>qBhT^$>>(ySsVzg%kf_4peYm^O-uuA=8}IYXw{G4H7+O^C z9~dyrFuJ6vsmXFmpPydPyzS!OyO2u;wuY&EdM3*Xnlkqlr=lzd3g!dHK8arzh)cHjl)jp%6?!kg!RVl zQwmlY(oLw*!!D_>U%y7BcFqGVa=1YMyupO?&JR_wJDXPTelXh6Ve@DHJ>1$CM{_N^ zJAwj(4Js-s=!zH_8LJO-sHHA0FN_h}Y;#j%qfuR$5O!X;yx^6$_Zp(k&W!bz;rnuX zEA+M{^GNF{o;!DsNbyBqLPxopI@qWfz9&bTz-E3gWE4;XrG2OH#KG9 zMsxE#hchYziX6v!a)t&tjkI&z(-S{j;l2qZ0_^p%$9pY85V2}*ZpAj(qN+x_jW1t* zA$4qUY|L|dxV6kvbL} z9IU2V6BZVRzc?!`{qF5s<#bzBm4kYgUHKV0BOBJQ4=4h3bqKdS{<`^)QI6Eo{8V6( zkdV-{?%G2;ZOu_)7E)_eDPL!D{DyKP7CRsyK(NQ@&3r+7dwY^v+96}1KW6J|#pW!*U*czf^R(H$NdTKMYC)8Y4^L#)b_W#i_}oh?0AoDxb_)ais` zrK(LSOO{s_!}{p5KCNN>J(9-~6@l|J_Hc4U0`z_Y)aB#j6JJ^U1-PMEb`Ey}I2kHv z72)UiwQaP$q9?ibilGB}@U-8;%-G%Ax0gn&JhN<`pbBGGV%1LonQ|OG`c%j^R>wX5 z7!wmyQHNHsy7$F=?c29+SL?X)nOwT^FtOyo+TuUPUCRU(_Y0poRfJuxGHHi^b6%b! zu*2NLhYx}1q@<((AGz=g&F>sXyMVT8T9VX|Se2xfGE$peS5m+Q{~L094YFzN@jUGPXY(P}94UYXL*caxIe7VP zybLgao}M0ghbyah@?KBRl^utr0n)#qRJ>a_DI4vrzD85Oa4q%Lla%;>J`kG4k+J?BIbNKabIxz-v` zG%dfAe)sO^t_$bRr4QNpSJ^EuI^$mBeski-#6|#)2`r-9@H0^fE2-=B<#u%RpPrtc zYCjqWn&L*PS!PB$S*;dkXOBh^sDMpNT2k^MgJ50IHPz0o%D760?s|{$&wr6qD-KPO zGiOGjd8VL0LP^@5ASszYCkL7=ijyl|ePB0SSXdnYa)w+>6uUh2!=>6M%$iLa zUfwPaee&e`%CdW>!pcBgB&x(O@s+EsS!=q%dkX26B&37t?zGbR>gkrYU%!5d(i(ku z`oe7zys-Lg*~+pj15am`u>#rh`t=MSR^cu@dkgad&lL~y=+om@1CV_lAnMy~E@ZVA z8`m>3oVw@?0+*fg@X z&fNRRxT0)%D(%Wf?;9Ub7Utqdf=}$Fs_q8g>2P}ahD)cg`CZXKuj`m7IaR95oS&X! zd>A>X=KcHlSNCbNu2gK*(oJqRkHFSkMX?;?cJ*Ql$u=`-o=ZtSR`!~j9lqkrN5=qt zwLbV$u3m)xshgKnTxYr+1B-0Gf5>2YSzS}3)U$K6ukY8}T?$7T0O$Y}-mlhHi~Zg% zU*B#aTM|$wFRhUF0r>sZt5<6*90r>cvW)Aii_%J_pH(Cd`HD_IwP_8?xdTW~$y2Ov zt8n1pi4*q7)ntKFc0WFnYl6w$xbxe0?x=nq5Ox@D1AKNC6cTEG$DD*JpghX0Tdbax zGr*ACp_;iCjh0^e$}R7F7}WACb)0A3-rp7JJ{>ZpBzL4K|9W4*P(_+XR_r%Pf0XXb z;w5YlWZmG-5LBa$lyvWl0Fsrs$G9`pZx+k2e%&wXG6{&D!vDUeCTs8FNw8mU0mO~k zD7RMb2bXiKF)yprb6ge&6g)GqUM&oOs`nbB!Z+{E2jwMxJH(#CveHrlzKp z$f=>UGWN~=^Ru&Z&-fFP-?hyB=6jY8aLquGoQG;qb}f2}3zDZbG&HbJx^u1tyG0|5 z#ca0%U`V#%@5OB-8%N=ii762B?VdpBzq7O<^C}2S!w6KSZ~0pT@+C#0bk1NSL4$ z2zRcj1_%S$*IKy+1=YkFGp<%5uPM(CNI%^BV!u*YU?44sFE)N=M`dND)(7z-lwH8= zpq5f$an{wid39mO!^Cb#D@OGZDI&b4jnN2`{bFt>fz#-F)Ya9UppiKCIJ2HSc~V$d z({cdt|GYtU8TXxX7~Z8IvV)e^zQ68((03Gxe7k{0EvpjRy?dR$ zz28xQeYeL zH=~(jX8--ez61^EHUY*XZ5eOO1ww1TC(6$F`T3!+W*b!bf%VApd@|ub!opG;DK7r! zpWl!uY-Gdb^KAR&8LY1I1WHHUfAC;pV#3ciIy#ykSqP}Tzdo|QzCL(6-R|9C;-1SO zn_x8VC_Y_IZ}06uP0O)a`iBxi%8R``U2?W6{qmat@8~r3j4QZca)hBkXq;R~rpJ;? zQG{$#%ktu{dLk5({;o1`nW$p*WfHe+HM~vp_uqeGMF3-C#DuSX;`A;!!h)iQk~u9F zY!1Qc`0?Y7)4eOpGsVTlinr<%=VoXBJaR<9+0R49#!JhOIS0JcS08S0xj-D9c4u^rp ziXP7#JP?Eh&{va77W@1^b)Li0%>4AVtdVY+Aby<+rno%`iJO7S$`mhJ%%Y;AT&D&N z)r>WB&7Gl?WGY@_ehZFKqZ5$em#sO{7$e*NA>^uTpR^CXPJvBKGT!!OWCR><0NbeoQJ^|e2uXe20nr1^k7uu~)@CDpAoAlxa4FYzBg-f*R4O3~TXb!D;%Ut{5C zC7;*hgl7Q_cI)=-xjnOgSUawvLdLze|K?qJ^`$QjDf;W zFXrZScl!Z$J_|!yD9Vo>J?cT$0MUxs=%xu31w1n@!o>`#Y-wq!gASLF(403<=}F{z zXfHOlwqP?I*`|E@uWr0Ja~IJOV?YI-I03XY)>XjA!I5;m?;Ws|2P#p@=E~gk{6O91 zoe=bZ9Xr8Q3QOg|36v*M7ttu;rDlt zS|059-@OU(8yfy++9eIAiZ&e8!w%0X!a{s@gBpGzRKVVWC+vPE4Vo;Cpc&PAUjK(L z&1BsudNS2Zz!Zh9GpRIJ0No&6){V-m91pDp4ipBJL6Pmqv*KY|0p00^;A`rN3Md?& zn#>U$69Y|+lbf5HnfVJUj!Q}Dr6_5XMTi^5PEPrV`2^zwC$078Cdg3!_KSNIU)9?| zBHef+RKg3*8#N&#BLm+lD=*K@&JK>Di+l!c13Z!kue&0*m8vEFGNj07KqJg)5@K#t z!;fLzMJ9h%g;JL(Gf7DThwQhnh4_FBz|GH}jb1@@pHWspS8!x_c!>?U8VU-z9Afa> z)fYs(EzE}w9ny85YPoTPlp%oY)W%DisBRmt9=u7w3NM z+PO1ev(oB|djHAiO#?k13nLy&^C}Y>VJdW6+0VS{w&oBe9Ai4o3Q=0~2FN7ZPQqMMd z(r%tRlz{Xg8NC#sH;{AtR$DgVrg^H=c8{c7dYHWS_72D85Rn!0=Rm(CwSmG}ass(fCwOnw_1+ z#R*M61IAgHA4PF6z+0o-D?NYlz2Mch(v-P$-{ zhvl!VGzQ+#^O!SC(aee7#8Sh>AXLJ+TlvlnEut-}T(Bw?H9vg{X#g^haLbogJrs1B zLkAB&N7Zo%N3#$`i`{|;s%&b7tQjFQIWXo@(7pdCDZIxz1dmvZ4aGC|t<)ow)$M z$mQGnCy3d=JqaJ`>Y#?L9AIE@1#!TpM8#q`bm%3n>XYZqy4qTwM~?_af=9R5|N*4MB9tnz2|-7mD0iOCbx6Jk6;cJAKAudwp-HwQ7jv$eHFfYaa$ z$klZt8ITc(-!SwV?8PJUVL$r&jeBeGtm;&y3}3`&R;N$k zAF0(J%>|f5U6eZ*h3!l^w1Di07ZIhUZgUfgLsDSM zfV?21>dwc+h?mEkJKOL^S8P{zclSVFG+O$gCSPK7o*#&d@j=Vh#nrW@64Kb2f?@&JDMlB6w+sjb=?6fKa=&O2JjxIr-MduN(HgE%|_roJp z*pEgkI&@nXINl6$Qc9gry@O3Xk;RWanQVj|FB~p!77`R>G?tYwzZz+RM5HNK&N6|v z@fm%%7*v*t1~fh|;^g%!J;ly8b4k1R?8yV1^Q#0111fj)?ob z{!(W&IW9YLgrAmLz7RntDTSUgFlV+S3u-wbU$L#QQBREel41dGxqTycEw0^P4ul zt*McvK&Mtu#D($*i8THrp)vEKay)NxF8*O2>eH1D82+FjrzK>e_x`yK@~LNH%u>JG)Q%2L{?hWc3xBKZ>5}^XK~20c_YLGvnjw zC3)Dz6W>OO02;|@r5ChJZXLpMg5mRM=ck*sBqk>(BVA4nwbnH8X*~9jvg#?GW3VdT zK}Yv}U?3zo*m3g5rGXS0iQ>8Mp_eXQQrxCyKO9cc`v4jG90S^JX|D#L>ORg>OIH60 z$g1`|^|+68Q=U~XTJ^hk@BWj8g-|kP;iGvjtRkgxG%=ai?i~J+$4f;;_3-|Ew!C)q zdtKbVR`-?Q<~q$Aun)|A_MD5a!UH_~lyl{-dR&JYY5b5Sh= zPmRiw&DJXJIyiY$IOdenb<;p_f*px&`SIdQaZOca5^>SdN16Qggu6jql?j&-KX_2T z{N~2ST;-!2Y?1Nbo>oJsG`;%TYfH?d{A9hdQe`a$pLZxZe%rigu){Ud_5Z5UD+@hw zZMspG=X@torwj+n%#NsqR5Q(u@LKhV%+)_c>|(nv1GLc#*?dC+^gtsH_tR-Vqy&x; zvyI%#MN` z-o5G*PFQg}HDweYnefhYVjNIT#MHI5IaNfEKLL|Cht9)maNV>I@>4-UL2kV#zYQwX z;j@~i`n(|%zfcG>2V2p;&PjH?8G-JysoiN^-5%u1;idaMY*4bJE6&`enQ+?XinwqC zRKW$KpclFw5g>PeFl`8Aka6CVC)?&*zOiga6+rvvYOpP=Qz~yWP%6Gw ze`Gd%VI5O_OjMNMh!gQ>#VvFcx%g48!Hvy;8_#t`U8gyx@-{y`#VZSe&1z^hXpTP*e;#f)gPC7F)O%00?{*-mgWs zwO{2;JxZE_RC_6*k}NFu z>_D$HVQ_f|t$q_Vqj^k>c3gFmZAS%Kx!C-W&(EPN4p)PmT=Dh3J3d6iP2HJaL`1ug zQm%kMk)x)rE=XVyK8GqADacU(hVa6*xj zEqRoA1IQw0`3`Pi&zCl&TQx)Vkj#turwyH;j7xdbRb|oqn3_ksbp}nD^}#mfnHMka zTR(b3A9*CO=yj}4nE(fCQ0Db{X4e`$sQKfc%N4}t{sg=J79|{BH~-5lN3*1-qqS6o zNhzV#@vjN$F$A|m^9jHxZ#pjU_~U3p-ztBM4({UYPl9pJMI`H%6e5@nefDEL9yK2? zIK3JEzd9l;=Bv~F5Ec%Gc;2&kFlfV9odxvfbDcIvU+jU-#}hTHltKiQVvyXX;entW z7OhAf=!B~S;?cJ)1BQ*6B;4IlZ&}$?du%n=5AxTx`N5dfGn&3C6fd-ofBJjUr9^p~ z_)M2AC*4y`f`qpOMHKI5JtCO`Ffi;JLhbtfNiA{cB z`V?x3<8T|Zg)f8PXrP*IhNFbb`M>`9+h)5H<-~`1!wXqtvu}Y~90DOrs7#Trqe81B zk*xYnINTDti0c0*rjR&n_r)UPX{okuU}#k)d3Z{2WDwh zK)qNj9t&z1_yEiv$w=Ln{hdHop9s-6$bRDQe>LVH2RAoh=?@6`ge{}~wlcSKaybyB z?kAqMiL`lDaM9T0yN!*Fyu7?#G>C4Un+<4tnH~l@Sl=W7b*_eX5GdS2NXpq(6$+M& zz)?=lfV=}IXT(u{cz&-)i|#&4P`BsLPg4+HKwuVO2VJ#2oU8D6JK*oD<2P^J8fr;m zwIH~p;fYRo&MsYg(qIDK9TymQy4EPmM$M88V~R8>I4L;AUzDk1+z_@zj&3nEf_~crVVObamP#EPa=9f#@Ahbjh zi+2se`ci!;AD`MN4S(6>P>El=`J;yqMIh)-oJ$auQ&7+=a{wtDcA3z!HNq1z$MrnAU^p+=rlao0)d=NkjXwu7e5@x5ITHu-(A1H9j&#|nZ zpIi#5PN%PyxKt8&*~TW5|7|98P#c3`^;Fe=bJ@^i5@*hE$)g94hv%0Y%%vp&7=mzsJ8BCv{*D~Ttp~g}JL@#izl3fG zoRb$KM4VCNfWwzF=JR?zICAa^hePc!1J*{~ew_L^G_(ke3`JAL?3mlEHQDWaY8oVg z#yf$o<;fHj6i-1PvfTki#23F%qvFWO%Lm=vve)MwfB^DIJ$kN4=8fWd*4E>E^&(1x z8(}z$_CYmN{jU}P7J}^Sz)a=21=e3~>L`5q2dE)7+8dLcjuY2D70-V4MpxsAzbSIs zwNJ@x?a5dfs6p&gTOka9ZiN+d^mauz4FDuB)SVimV1{v&@K<{8nsQl!m<*V;N>d1v zpBD|9z&RDv6L4+;-;^W%Wy4P;U`!m zUf3}pEU0pfFbJ6EgZ6{8Dl&=M{di0xk<I3~OaOSqgcl9E44^ZqZBj?%o}xo?=ka5j zq1yH9*P{_tVpv0mx}(Ujh&tUUr-&wxU6CW0=}B!v_BV;#lu}cwchI+gRH&?>jdTbl zBJ^Y1!eOe!$celYq|rk{Le2kJmCIk8LUT)H&YR|8;W~DG zQu-+RHYXiNwb%5Y8ZO18b5K(5?Kn)e=?$1Kc}pVcBwhJYLuQhC|JKz4k$C&e%}bUs zn@Rbi30=q?;`W09S@YKD?cXYgQWq;1@-NzykibV-GMt?a8FoZ!3aNghAf~-+u!@5(`FpY+U=!B8u}w0m1^w#xgaZi>=l;^V{V##eBO)Rib7AwUt+luG zB;6gKnhMNAy9k!Fu(o||@^MWsX*>+2mYJ#SPu4cPVc^h{Xl z>P9q}*j*vs3w3sU{BiD_>)G2B(%qyl&x$?{*j0M_TFcAljgyAk{hV=kyRYU?g%D1V4$7e@}$qYa~r5 zr9=qIC@3JXNOxfc3djR)gfS$OeHtD<(zQ$qH2%o~Z_t~sU$3xEZ??XNL|C=bBuMWARjzJ?+B}YPfJ08| zur%xvFJo^!H5wW!4!+S+dDRL($h1errwd@5k^tcx%k0bW2ecf32eB`^HM{~ z9fz~o?rV#{ycB@=J=C6wv zALr%e1#YrFKu383&wlRA#(bV|`M55!lFwOcy;jTUZ)PF1My`e?0-@*tA!I$U`j|05 zzNe?U1Ff5iK_D3t(!_@sfjpGx8{5J+&9M#s8dx(7I)`rkVlxhGG0SnQ|GY#05?yuH zkM))pG<48KC{1mVZqYN?bn6QM6T0EOw`tJT3NqW{U-c*L+n7(`aFvt-qw&`VjBuq8 zPQ|J7lX`AHE6{;&oA>)Hr*l!_2GgO&w71zL%{sqBHceNY*fvl!z?Rs$xs;*OIhv7VHZ8YyOx)ial>>H z?*(&%UG#1cvmEcHgGgcV)}z@I4C`8A-Yqa8(UUke5~yy|pe?yWf{LEs1mv>um?Cf? z1SwCH&$QwR83~EkFlm!p22nssEpoQA@o4fc1{ai(vn9CYrM!9AeD?7jVP_u%pqs-k zaGCt^Bs?59W*iB*6L2UEG&fy%C(bd(3t*#MmGl^Y<1$02s+FdgxmoQU622Zv5OY5X}4vW{{W|eA%-s8W1`R5_& z#B^;e1S5U*r@NVlI)z4d8MRFP%;OB-}6 zG<4{9?g{hvf1~b1qD#zDy%aP_8yln#56N|yn<2h%7pzZ^j@pN<2-OWOa+=p~-b4Wc z!3K<$9fw94y0jN48qg*z($u`{eqC`3pJD-}w+g+F^x@m0-j;cHwCV2C zR8v;|(+gK_dro8T{{7mPPBI!lVrL`D58p3yBn5nP(4#af2<++Y?KR;yGB7}b4scVZ z|I|M=#xo-J^X)E)dsuHRem;61P$F6h_>JH71mTfb??ASN+6zL0ou=&I;7}EWq9Jh) z9j2|-68H_l=pg-U0OUr|I#z{fVt?}(5~E{Ci;b3TmcmoH!5bn7x^oZxk%mDo#lr>(6G zoX0@!tq>3yBnxAv{=1i;GCf^RUrJouy9sWHX-JU?PGx0TvIX4{*F2 zmU6gw5}|^zq?x+bJb1C*Vb32?srU7FbKll)mI8NR+q@}iXD7!Sz;JwUq=+*w7Z=yD zW9cA9XbOOh+6eQWL0BQ{*k@8K_%kE=j7cK}{C9m-J8jpId1uB(hPhw) z=_*8!UMbptV&7iAXCFTGGvlfeWaBHL+3}+F=zfvOEMxG6Ax8r5-JndYOS=Y@4HlzS zDrQY>aONVH*kbDGv{sH=4d6p-!0-m8O_6>;MKE!pc>QOl$kW+N7~;`>KpDXMBv!ELd>py-nzO>&!yK`TYw`5t{y>7LCXi-j5N(0%mB!G#G}U!4g{*x zXo-5dX5siJkGrY{HIS4~v3h7iy!@3bF9RMdlX*M`P!*tLqRoZr>&cb2qM7l;>Qj23 z$Lh8p-!fBknkM}DHZ`f_Jv95c`0lL>6#tQ-nNum|yS*{aBx$>~W}~<8OFEG^LLWW~ zosG;m`~7*2(pjn4tK`_{F;dS>%unvzsBn7Y-!~;b*c(TRPs_<4DW^Gj%C&FaZP}%@ zqiiX)*CR7hYSt*lap@bYIE-=iSJ6@kA9ITy0N0MQkvIj!1-4h zBxWWinqfdXL){##4Vx3N?WdF5ho{HkqNQdQS)T2?FZr^M_*iNpr9Juej={={2CLj^ zwgNs7PS4NJAH4~scN;UIeG%4t4nW+{&`>Rm9cgH^|Kuf+GVf{)tRvkEgT6TNbb*+3 zmiU2XM&nq~6|88t#8bFQlBywrqi|#J6)5oGexb};h9lXNccEj4JNB5y9VBMAInvC> zl*;$0)fjqK1*lTT;+DKC`$Eh%y_b*c9?&FCVtA2}@v{Vm8gPg+6utAG@)YZ3%*@PW zq+-t8p)y?v!k|Dva4-mR5k6mV93QzjY%G2)P9N^2R&Co0cg1PkUci!kxQXkdCR{kN zTBOW6Q^!I&VpUBETN0Y+&-T!Ui+e5`2I~aTw>CFNZ^9s$Mej-I`!ui7mljt4)b6u^ zWG$0?d@Jd6G>d=LcV&LB@jTCkyxP2MK>>l-O$7y&`4I=`SL{59DSWw?s0S7i)rXCk1z}k4oXlEz(`-?+5BeG=WlRJ zlWxR&S6p#+c7~=)T%71kR~BqeKzlvbZ?@G^)5NEyS{507f@*$-;?|~ZgZdf^qFvW3 z59D<_Xf0e|6md#_6S<}IqT5{7Ux0L2p-@5MT!}k(?odr{FnEzFq66f$(#%TujA^(6 z0|TMqLh4rQG;XTAk^!lnV6=Gn%w7P}Q$HH|(7quI0G$TE@IWY;G5cqip1hO_l^$|L z-t@Ickd=$6H|xo9wH6&z(p=iDnQ~@5jr^OYoYi;2d3 z+`PFHY)QFRy*=>9LQ`rt3=Rwmg7Y@ZuzfR=n>92nM%z1RWyE}XU>0=lPdwcj=t5&1 zIZ5g1d1N|uCh>IQ?vQxRtyT#wc$(mXGakCb3=9ls!H{7tf@+tiEno-J z2eTIjAab7_V`Vjl;!P%#F=`F2!jJE7T{H?nNNTL8Bj&1-z#(+*o? zQJUAVsXeI{Ra)!LeZnN+3?_5rHzr)TZ~;EdEzDH=c%w(MapVVVaSACa9gi@JM$Ctf z?C!Fu;X)78b;18}IDFnA19TM3Uh{H-B(ABkUU4ElqpaZ-TlD-4r{W)^)2s*r7r80} z3=>6mK`;EZZHn1TSrNy0q~(8o&}mte0u=@~-hrLI}~1-Fe1eSjV9*KTVV$17_k%VnCnrZibgKDRy*QHunEWjKLKp^VT^xjyQ#Dz=VOj_2 z2h(6=SWi5Eh4wx?a7uu+zxN6`Ha0f50^5Yy3o2I}d6l-1;ttxBoA8FX!HozDrX~Ov zc2Uu2;WgyH17%#-Q1H>eQV^}g2`S<^S%rm#kSSBxaVXJ#2|{LF*hP%!wY0R1?AEqx zkiz7m#^%n$S!jGeE2CxhDl8P1ePMXZDVE_zP*`J@WR0OUJYvt7UY!dn5ynX9Sn!GV{oGX}H#jtOh5`v}3o}lGO2Jp-zr;Z>otvKHtQK90 z7tpGfwpf)+ed=SJ%%=?WWBK~iDM_Dy>Nazh$OiA^Lwjp?a@}f}pP&D3|E9_YW-|iT z3CntY+Is=#=_qF-L*t;8Rvb=oT#5KK!p!>gcptWdCZ;hdE+bMCYHN7PM7+|GAXE+S zOI|^cFnb{}+KA3QKzVddW#2v-^X50(d~f$8Ssi> zS(JJkb3jng9U9_U4LZ!?$zyB~XNJt?=QXZA6CECj@*lE1CA%f^fmW32y{#&VMyrGq z8mt7O)6!y)IQQpHL=DO;B{L4|NkQX3p&AMyVp2drASf^pS%$~!V_VxSbR}->{A2dX zkJ5BkkN&a?QRfZ$U`MUP93n@Qe*^@eUF05B`KQO2Y2EM8;B1;@oMn=oQt-`B7Sp?$ zJ-76fl!)y~#>&|HE+)gm zjS>{ZK}@xMgM((*9!7A({-Y#Ya#mGsS#y@vs;tSd@@8L$=kh(dywgMe(2lhUOQBBcAhKoTM8xn2UI`Nf7 z!eN2Om15HZt_P7G-D-!HL>0gh6oqHUF29C3<1W=czf#}TWe$4~|3?(i8T|eyU*sX# z5W|ziu!^N2VvyGmRa8kn_Z0DjpUX;__^h6xc4PONMnfaCN)Jc}ZPw0SJN<2qpWS%> zd)Dd+;d{dC(Al*^z$727HB0a9|JV6|f)Gnw;g83dLczm&Po=la*^J$9z#B5aePh_B zBRrKDC$n~WUv<4~&kk};R=0YdrmtDjmWnWOK?&Y2k!jY#C6(ecEb1;Djjl* zpcOGaa$oY$ttFC1JcmLnYN5!~d19rkUE-6HYV0To^yV z2Ix=n7rmk-*6wrjmQ}&*f44=w(MM1Z%fGjqPWe|?RsGD`pm|;My@Y&g+>@~Vp3R#{ z&zmTu6*fRvthJ*I!B z|H>pkoC)RFG0Bcca5_wwa0g!LbwiGZUsR}qVfL_|t9027uibdSN47w=rZ z3OCy_;Jbxdo-0S3E){z2&qqS%(YO1R59EgC2FP|oEh-@x4glwT;ZSO*kS*|}Y<)k1 z_D>>c?}Zug3ZX#?in5JKY;m+`ObkPvS2NncTu^7{zlT~OOT#hDiL$Cfv{z|fgVSLk zprbAfUBT4EM0UJ5I8H4LXMv7ONmXp17D>k-8X93}c5}~a1H6TuSp6!(eAHc(hr~=w z+G-ew2Le8NSD+-}drR?+pFVv`g@#w)1VG5_h4ajE_%5rc5JwK+ieTNufhx8(Ho*8e z_2GMcB$I^G;?GL)v1dvo60a1&o4$b<-=%>Ikf3Kl3w=LlQJ~LISQY=2{#6IavKyKt z+JAgR6Y>lN8j#2Wjx|d#$k*1^rlh5%B_|KRU0z2zX-~Lp4%#Sk{LT0)<6HtM`^5|9 z9KbN~+0VDoU{_IACX7Sq?>St(+J~WlSTr7?-h2-{Blu4_pdH`>?HZL;rSCY5;|`jd zXjEBwE_6CF9LB(xhsL36bq<46EG|7!l_$++p7C^~{2Tc?ywYwV}|V&i}F+ z0OeKo{RB{eZ6t@>6m;q(GOyQ1VtnJj)HX-2ipbz7O zE>Rmy5k!`P93$JU`dh1CycBvvl)ztP#n3$Q(7eA9c)F4ty4${K8BiTr}P>P5F zTcAwhoF*J^0mtKypaL_TT?z338MO$+7C0Hw!-c2EF2hg0VPp_by8luK;hhmV{kfv_@M_7 zLU_?|CW;YqOKzgvpF0t_u83G1%%Fl(QlP96M@ZmQAdCKCCl&&|Jl^(__8}uUYY_w< zcnzM-AmfK%Y1a+D27%r z*T9ougm8qagU+%Ny2Sr(N&RkmN|$hHkPPlqq#Q3qe1ft;%K8OWLTo6RptOL$$Zyb( zTA>_ubPz7*X!d_sx`{aVr+oHmqv<{4j;+#Arq577KEY~f)ht0%hmV@s3um7l&LIxG zT+PqOlSPJPtPSj8LqkIx92^S&{Di_lu^g%&$0P46U%W^oVPax(&H}>`Y+i(mRMik% z4w4=FY7W>7)o+W6M+9uw2as99Z*sub@_-}NWXAsqmnIjR*2Rm~@AerRB_Q1+qdu1KLlSkX|Cc!63Okl};Tso_A`y(t3nl#VDZ(>e2DYYHK^7lvEr#I>_5i%KkRSqHz>sC&ye0 z)6eZ7k;0EktGDFPVN+O$T4&D%MXv#R)r-Emd4hzsBkN+ z;&WBNYY=2%R(M)if>QvFPYoXDVXUgDxt2QcJLXgl(LV$L5}RsP!VxKM6qj+52YMJ| za9aOgF4CRD`<6O=N8!)>rzg}7~c=_ zY&JJD6aFqvHh2Mo8nn{9p7bi=o;D#h>N%4GK|hJZfCgKJQj110wy9lX{QVo$#Z5?R z$R8$C(-`5rOM?j>JBh%NSq$O1bVYRgdKUg8GbX2|rlzIU>x)M_HEz>jnM&KRPY^li z`*keKuZfce1+V}$Ul0O{9mJl37>=``%zKK37=7zur$b0t@Cg4vgu!tQ6f^&`k}1#U z1P<6Lp^-R$zG{og@0FCG)|2^v=pg;w5A6S0cg#EwJ!#9e(4x2uiH}utZGZSuY>a}| zhk=1nAkV|TB7QD)js0v~T-%tF)iYiqLz9>PDQo1<&42v;;Mx!WTVcbuh+hSlg1#G) z38vVQRgi(r&D~%Qf#m)4*$d|jjsJWn`?ZHh{Hri2lE>`D1fTVqU~##p*qPZ&oznrs z@x93S5Zj;!8`V5R(kJ54Ijb*_*wRqWWR7Y6S6QC>zbk{M^tAl^N4jCz%f-p5opGgN zO9>03ufl5HW(!weTaR1F7pvOJ<3s;Z(r?4pjm!GTjL7;2cUBd@t+NdI?~qYCFo)p9tbjfM~o9Rno3fYMe!L@%OK7`sq|mR(Xk&rA*DuKj@2} zrW>qovWFtA;J%7b3Qbi^8eul__HvRV9TT%k4N?G zpyHzIq1(AMH8mxUBT*{R5d}}g&U1uth}~;C0+Sy!B@LHb4OZ?$T7;Pqc}6L(F^_8& z$8^JPyNGsk0q|(=LeI?d_+9ZoG3A-N`mT?(d5zS2kA02}wS+#|4ZUxfthY>tIm=4R z7Okep#c!+xOaUnX`uPHbZy6;$_%n(0GC{U=??^*b<%w49UkWRe3Ro#9yDOjaN`aYs z6TjmvNQ>hAVYa1{F+8HvZC42}tmXhWG@$=q!JDFLL2Oc?mzS+9l!*fbp}_{Wh!|Z_ zJwfce6W3JHK(Z_xhy_S4s$eAE_=7WKi*#s3-%r$eZuNA19Qp!54jT9mq5iO@#P9v$ zD|0cyy7O_K^Kn1{X;6pGMG|j(wdeeuTMb6g-=m|yr7aJp!9e_fwf7~^RJLu~TRowu zG>DY3K~ZQxkqD(IN=jsGLZ%eTxFs|vNm2<(l2B5ahbC0!B!r6C2xT7k{*UX{^ghq~ zeDAx~xBl<@zx99CyVkp&*!RBg`csM*Jrup(X`bY&`36Nojdy^tD_8Oc- z;T+OlcRAt3t~8jy8ce`Zyc@wCd>&Zs9n5xsDAwD`_D?Cz8mZ1gB=`Y!KdJXNkuUnd zB^H>&z~B*+5rXn}R6b}T@AtIn@Qt=@H~5SW`t$k2{pmtwUfFeQZiqfPc( zT7sGBghDMX-GRDT_obs|-7IDu3njy)U4@GFpc)>7>cFX8`e+}#)L30~W)XstOw{s| z>}qT9=^1={%7YB#Izl7I>HWv`R#g=je|@*x-^qbRu9@8|_1Jn+8O~{hGmr-K(ue(7 z-%2!uJxnU`{wYwmC)QMO>7Kl%a-2_f&egWa+1kTc17XK`i1d~?Y*|?ekc;(uYud}; z`uC7;M8hiQr1*6Z$ISFZQ8mB!2#QRHsw#jaD6Z(@)i(+dAinJ#KBEBM0xZQUtOCSL zMQ%2vMy=KcH zFF2>z-rp#n<$`qlx~ovtT|_;Q$G6srp!yDbbsKF5_bIHY8 zvu^YV$YR_$JuLopIwCN@q{X5L(6Eo#*WQhZv9g)|a<7TW4{ikW`D!j!B{q)Hq(`SM z#@f;n0rXSrgp5ZRL2yEVe_lzY-AzhL`Ybb>rGD#U0KPsht&1lwF2Gr4(n(u3&a)0NgYT^HCmXm^N9h zkX^fCPY?tHkB5VpH>3Jam5>%&d@N}@gOzYAKuiY;3c2L>3n}n>Gzm)XMW8V79Wev! zBWRx!t;6eox5cj=|B-wR{r>ab9Du=l~r@M3=Jg6<=gLH|U2=L7f zt(v{+$bsnP2wp^e{ux2bPZHrO7{hX!Lk(biTH_2qlKxB}RFf_}tScqE2H7WpZ-MRt zRcu3_+J9re(_*=qB`u-{F}wR@EW(4z+8t}Gau|RYt6W6jQgd2+Zzl#J^kp?NC3Ffwhvj;FfPsn-j#s6{Acw>g%DhX6 zIM~>v!JA8J{fKivu-z9RFRy@wKLAs|+;l;_X>H@bAz2smmArx_?!-H>v2I|mTH1bM zRz!ug4C$?8eDaM@Z+vA%MTIa6BBuapy4;E9yB6DDu(h#S)o6OmYe6fbyp~(nw!Xm| zhq$J?{Y-9s%(Tzr_>77TH~?{M4PHMNr8Q*hKC)^xFbI?mJ{nQ;B#34=!vS9mi9xb$ z;y|rihc6EtARtuoR3^0O!wQpX&jJTN318_u#3Qn7G0*p%A>9BlTj8;FUY3Y4dqBcz zdt8`yM0XX2VhJJ^Jqbfy-9($pL~LHYU3T`Vh|bhFv2?8-D*py%jZXE_S!bB_%FKF# zF232S6V(Gr*#T8dY4pr^k7Qdz$YpNNwWy-4hK2&Funk>{ed^T+Nb|c132+2B&@#1T z(j~I^aG_cvgs5>W+azCe@ht-6?C~xrAXeoBncCr?A1(18!Bxv{zMb1yi5B{>S>*BPN>RrhY9|E6%FwPiNtH7&3A)ww@1|&Dm(Ij^*>a$Zrr=2+`AbSTH zt!(_=yQ>Rgujc*QzC1x4S=2~_0w;il7C5-xnrV)e0@_N=xng23kQNwm!Y zQqzSpfQ--AA(Lt*O-Wz*8W>J?m%HS<){cSicV#`@zo9AXqXZhkom z=Pm}8^}Z}d$$SsEfP$+S4507u)*__?WTPZlGV5W2N89DT57g(c`7CFTL?qXb06j#Y z4(A^7d8`V@*ehrbvfRHTy{nMOJfJ2)B8CXwlr>m!ReHJyQUa)?u4itif}8alc2k*2 zG&@SJSskpAEO}FAC4?sXo}c0^xJtr)LtcpDhJ#t1rn>bA+~jOW)juT{UQkhm&O{+s zM#PRqZy%XqG8WY1v58#9wtzg`Eq20Zl@`w-L(a3-ihCU#0xk;>eMasg*qrlS$vq5t z-x6!kO;-f4LZg;lr5;ag(b*i!uBQyR)H=QPMp=oxKf!T#zlCS&Eq0>Vi*}~wiMqbP zv69B7JmId@gRtrV`y?;h{x+^U7^y?AxRv&LDj(9`g1>?);Y~QejJ8-z6kDO9%S+}6 z`3oPqK4$x#{dfe$X-@H&`Gmr0nVL zT1BN$V&eE9#BdM}%~aiy<>>*F0?=!C$rzzbY%~qn!0n{c5M=ugINrh)(B%Zu%hs3z zm^(x#Y$G=-qFdUp9=WV=MLnY6e)!JJP~CwH-38y-7S00yhkAZ{!owjP(LNkVzEb7pO(p(K&9xarh`Zi*hG@0z z02u+q2KYh<7DxLF>c%)*ZkToZxR11&A`~Y7lFesORWS;Mt#7uZ`e&R7kLO$$nBRb5Ce?|EmdI{_7q&WOgF?mEHP1C33uNxGaFl-S>A4PC{&sfa)B6 z&+3$B9R!vBW(8cMB904P!72iji9tQnEl5?le@HInKg0aQm$iN(s18#wA85bOE!aAD z?$nc4?NY4}n?tr_In?q)?AUb{82L^>*|9O?EB~q1u~eSi!GV|L(l}W*O@!j+x^0Xl zC&AZ2vH*a{b$5bf@1**t%hC@GXNUg>p!h9bYUPS>MJ?eU`EYEq?lFW5VjdbRw;+um z+8iL@a3U|!t^}m4#tEWtCM*$$ron|i86hodP(jTCb zAthCqpMNs99^_VtGToE&BTz_I`YUZ}59HR|Ekmf?H{qawJv251j?&0N@}-dN{ECZH z=%5H7+rrZTH?aCb;pi(CTBQduN%U5p;K{4nj|J-QrshgmK*FQx)f#fAZ1Ta^=@FE+Aju!8~Xj4A<135j**W>;!! z0V4q@VIBS98WPTS-`G7TuU%t(4fX@uXRv36<>xcY3iBMfk}jbrvgu&z*U6Fu&BhLc z)G4bk1HlyhN;>7~UGmL}#ral2<_{eaZ2g`1(Rf4E@0hywQmkfV5N`~l+Kw5E0#oYQh zIfw{uU13WsgJ4~IcN!3b>rg~N0f0^_?ueJ<>I}k0CJC+LS6x3pP~XVmIlcvqY8Eu! zh;EgDF#zF#aSS~fohP7X1^;|Un()v02-ej&+m2tyqH;hGlRql%F}Mv(BE*g@PXY3K zg|pUY{CKcUr{Jym^1!z<8Jt;?Q0z3~Rq-zw=?O$OeXGTP=5ZM0JiJStEr}Yg(gy_5 zrH^=VLzb#8<^bFnP#*!Apx}!sma!&u91c|5K0aPvV}SAm?kceNT!u`m@#?p$SFTje zh~7R4P91KBJE81Wcz;%X&j3_RNW(5wczb|~h^`O3bTsK#c-ze$iS-Wg9h7}YhY%DF z!f*|tjG!v$d1YudAjRbK&s9u5!9>`5>Fst!%TWbo!ks%4Lt0N7W|%3N`&CzWmmpc#8{Y8SY7*$asL}?#X zG!S>Pbu!UmiqoLAB1*0-q+%Z@c(!xag(`Q6CV8 zJWmkxA@SKsPRje4ndqjHlbeBp1^h5vnUW!guI^HZL4c7!%ni7}mq%dMEQEID5c2r(w1bSF6dz6@!;Vqi@k?C6 zb=`DgE+;BSyYz1_ph#%E4@Wz+Q8LVM%oRMm+#bpPpCbJ2g`*L^@f+$yIv|F0<9eoD z%I8b4qKZRrCV&hG5BY(mz(bdg83~vvwR~+jc1{Qvu(P$n3K_BBAa9e7pV@;?2VBn>JkFj1tqvgB`4#Qlos-DYGfW)92CaF1x_Jl2P-&)Zs(++{zRhGqMrpb2ovnJ z4yR#t5m0SmcR9ySoyQRFLD;z!%sXPiO zm=N#d97Pb`mPQ2137$Q%2&@MR4--87=0mDSD}p$;=cLd%8T{s(BRCN7;eP0+$`eJz z#iQHiG(+>=gToR0YO|#2fTpdjE$2r9y`qS_tV)7BI**`L`mSz-LD}k3yU4aqsk?;b_@1;}gLe}EV^ch3c$tG%BbaEDc? z$D>(ITnDC1`4&5zaKx21kgBR<;4d71{hG*O1yJyOt$S`k(Ou;rM@n}Y9);itXQR$U zz)X&%fshYz#N?2&BW(u7c!|VO=iAm}itWjfsG(_??}BBcdkp#CtwU8*LME0V=}l$` zae8|St=*tBSwnTga46WHj(rCKsdmWH&!{aRhM!BMMIa|97gZj9s>}^5 zBm@PB1x0<32ohnIRU1VUoVfTtsjwCqETRSS@^QdzY%@RTJBQ}i46&)FZ*jl%(S_yAE>YW)l=wJn*qHN zaAHmC${sHtdT+uDyqfN4}E4}3MUh6k>2t>xguEWR`cn1<|=x@l8L?$)t^X|mO z73AkPg8>S$xLyg;25%$Yh8oWk;ZhUV?L_fCoQY8+qqeM^T#&|N0D=tCAi|%n@8FFD zY_ws1ps3Feuok~ax=?H82=@%?_390bvPKW9@G_?=-pJf;e^$SLj3mW*&=8>TTy&n^!o4Wfuu3Pmy)-`j?epd@>vKy(Lu z-q*SmH(~mvM|GcyeV^ajH+*S~K=y)_YaU*hlC-AR?VAa=VHhA+FkKtdw5G#(r;m_2N%Hr*i)=^#yq`bbA=Y!XM%pv%Id z2}G?bCk7^1m?&Gogr#k^lFmdbGXk(?tMphz@BJek^sK&PDtd^pBZ}u0*MRff%9!e9 zZt={+c(S?7pgpVC9*W39PpQR=J#aLj<_F0p@mO&bN60$+rMBg=kk>fSGeLC{i43|R z5m@boe5M2BImd-Pk02L@3M6lmlr+LjPVAx+MB!q8r*9hOc?R5yGr0L>_g{45I_~7mVkaE|g&}e20$O0Gj0h~{7)-rur*n|8QBwhwI z$1s`iz>ZCVHR+7F%;e*(Qjh-nGv&3FT<`-srw1`o_1X-?)RZqHYj#9bBLmx3(YKLdgX#8ThF1CLBpv*7Ly#r!MRZdh;!MrI{QQqes05!Ya zrOn--zg0OgiPcP`@W@N>$byI82aJ5Ju46hg3K1}<-2|hVn7A}lTN%fGjWY@QK$3B? zrky0^OQe+PUE8{oBzkdlglzKj9e)%{owrcqKZ6F4bBD|kDU9BdLfFuI7{>#Y?FIr< zT~(#Z+3c7K@}HcqoaQL$1EkDJp1I=f+grLZ;2*E8a1ir3uNj4A#TgJT^E^R}o@kF? z)vTZYiR?O({a{Q<5et+cghhxFZ)KyI-Asl^l;jn{!yh=Hbi^^Z^|3Luwx+#@!~%98 z)(4aV04p5`Yy4Bv@*7EW)+sg`<0=-Z8Yih3hm0Ppbp`aaM7aTwL(MhycIt4=EpDBf z_CIE#{S&5Q#3?pEant6fr1bze>xkHJ9dsaHQ#cf($f z7Dj>L+9QorgGlNi21iPS&B)|Xfh-2vAaTzItnoE3*=ga2PexofWkPM9hCMnl467~~ zX*x{xG*5e_Td__{li6-!nAp(4p#wFZT5p zV+O_xq)B6kSkZrsyG<|>L5@~*8GsuCNUL%x4^Tyx%eQVo7MIC{cH~Qj*^KkireM~o^&CK zdSKd zRNQc0lREO6BLHMv6a_)q%lX)kDy#Uf69E zS&A)fdg>nwxC?Z;8T-Yop2GA`NJkJ(u#0_j$!BC{Ka=MtaUXVD-55Q8H~jy0bfzgSy)be=Bg5l+4S)KP zZvdm=lt5cYrHD_}zj&9R-5!uvGzIEF32FP@cLb|Ovugu`6xa)bq(&>2#>PenmPq|C z;(cK2AAf#6{Vl%-APVJgCv_H?W~TS)B}GN;MM8<9TZ9kzYoNH0x5Db=b*3N_M#QPz zcfMu%Qv>Yhe`H*wM-noBo+l`wCNe#zAxQIyf=NLENa(w|0P=9zU#BE!G|xf=BOs6% zMz{&2i`^@1a8^+Hic_@-rXNldq|xu+8|~a#oe-d3T3cJ|Rsa!*YSf&ykCkK8#Xc5I zV!WsrQxaGd-%AJIwQ_obSyJqc8`nE7wSD=r3Kj>P5qIvibY=jEfzSZpXi>5>a3vRqvCn!;;286%aKQn8yXr`2weR*N8h-HZ^ptgG?C-0 zH0x@BdnF13%9gHEguszURqo}Nq4ngE%yyjY5RBBnpc$1YeCo7Yzdu-YSF4S68s|jX z=-;ALgJ~?=Hx=?TQpt-FNqoPgswPU~i=v{Ublp|ssg5`fM+B(6kz2Bzzs(NYXXWI$ zxH#|!HNBZ&ZAK;R)_bH$i8G?#S6R=vpHWi?Tlcq46=-G&bql);mA~?@{v%QeIqH9KB$s<>d|w>y$4Zb6%gV|1 zE)@yc2sJr=OnWwa2#-Dm$J^%t&wzjc`&W$Ko!kukKQx1OR#y4UceS;dq)-HX z0!`A705Zp$H+_l-bUxY5pXq66B(a%#xJhPR6lZ5p@6GxO*>n2fETAGOLKwa!UC^`H!W;T#nL`2BhkBR!g zNKhhFyUzn2WKWNilP2C0;V+e4ptD7MYcC3XAQ3z4=m??Jy$*7ltI77NGFuj=NXwV+ zQO?3sB9CV{yhGfk#t90)^-%`6IPm1CMIbG49zF7Z>KX$gxE7YPs?uX|`fLfP7C5o+ zAI>piF`&s4z^K4&B_VzyhEYzC2i^Lo4p~9jbwyf&BXeY8|^5>1o>R?MU}qFq#x8 zis#(cK(*zEQ)o3XOBEoY>EpI$$3ak6#G)cKbMjck`o!=in-??VpY046GS=dSl941Y zzwIFTPu~HViZUAo zA^rgmtkix50WDl{((E=wmmxi`0w)3L03i~p1skF3-4wQ3Njiu+e@Eu?w^gEYHqB9? zram5tSG@A0hYx|Orbb208`|yWHbf31U@UOogE0)aGtmT!JYk6ESQb%*N5)T+ye*%r5X=qjis^;6p@vC0Awe6tC#JSYZ0fz}>xL;qJ#d51l)iDCYfVvMsVwO}hi2E*z(~Js$ zNgp~m+qmG~4P4X6$;lM`J^ck_CG4YNKv#uXYmC$usx>vWr{Joj>e3MH@jL-SnWzj4 zuqZw;Mm{}6XzXST^og+n+adDfXbg>jxI($+m7%!23z@J;x2c6)sioHkV z$W3`k-g{v=nGpLZ-0I^{?$3cp2oT>J+X_^=9B$4NzAS^RLd*i2TGqvl;9QlzByV#j?O0&is!9)!zN!qH|&|1o9P;DjI`Y<;ASbw5s=gg3Rbm0Xwm+`~2ln!qE2o3sKlG&F?H zq2jK=GCcOi`DO3l55puMz@igyQk0Oq-U?j4`z#e4%Hy8KQ{iCn66}+3f7>v3DpoGK zQW&CX>CzB@HH}2arFx_#COY1+Xd#W}OKL5>YSSUigcBBCW5yZiuq`;vnZpABgIiNQ zP-qX|Qow!?_0F!60PZSI0K077y-P#zdVmw4`J@XT1^RSDi6BIg1OT;5YkQ<6d7Ri7 z`N#m5waZHXQ(zuEjiA#&LB8x&fn)*8UOBmL;u*bp=#?tPc5ve_Wa_|LEUn3+>7aot z8Oy9GvZaGaNlCvWY|%JYkklZ6y>mxOSrutNDpZ{#N=>j!*wv<|1hZkYBE<>poo-$M z3Dp_zc-RmU3pku@BVb?h4UFw^9u#U5A_Wm4>|}-+8p;p`9|ykbF-oMY2+TK1J|L=? zjFhW$4ve!F`W%4L!Tl2Gz-`2;=EJE(|Hsg3&_Ndh8pm#l@E(UjEes+!KYnLMfLuDG z`SawW-SBtxYc214m8!6}VOFgQ@!NY|Md7`Q+;LkwJqGAJranLq}@mERMB*XQ4k zr>W4%SDJ}niVxt=0QwW7i?k@j(kK+Kz6Ty%k}}7u^>@a?gP1Ekm<^*?skVmOY83SY zQU@YqGLyM2MxkM5Cg0xC-|12r$L&6a_viWQ_ zHa6b5^QvYPrt=}R2`1-D6&OIoF>0LX4uj0Xci0vw$xPxelFeI#I#!!f_MJPa&`uC7M(uH=(5P<0&#>M&TDI8^w4%b#FZ}a= zZkYL>j#K|~fm2+@G<|unIq9LXe~n3i+;vi^R1w+i&u{|_DTcuYk6=5$ARDoLO%Jtp ztS$UQKQOkiqMTb&;3^{@RLAYxmIzpXgaLwV&;Vb4d}!b*0g@F=ze8zZ(}F(jI;4-7 z=S5^Ub@=@2%u(JAGcQjDB0gY;z7Yc_g+`GD;*6Y~NBV#Kv&@AkeSZ7<&kybce&C?< z5MP@!+{_u9Y`(AfIa2&Ros@xyi1UAWHv5;M`TyV!qLYiqsjpE8g>PB~a-?%mVVanD zBK(y`BV8EW_{JkcPhYzD4;ufPKO&D26K04_kg0qjWCfKL?al?G1qc+TzY}(X=om0J zFU!&*w4bEMO!EA_6!0aBA=3OVoxA_-yHMXL%(gh2WM-^UdpWOfxBOgNM zST(dJ6Lcw>Tipp}gK9Js6t!8)m*8Fkb`W>cl`GH1@7q!GPF;uaKYndxjFllXGEpD( z6X|0z{F#G@OWmUUUPurIu5BI&ArOg z`TD+w-N(t?dO2X%C=Ea%g;qBF+r?Hce*M1O!+>^zhU|#3pP&2inMp=lO6aVL)-PYu z-R*%o@E?85GnQ6t`tz!PM-X_PCz_x;;+-GL*9X9f7tTUj%7Ee@MMg$?^{8l5;)cP> z(9lp;k11>0J692LFv~}p4gu9UMg4cKW?xf1#v@UzMv4eR`TR6sy20`FIM^Y2@Qpj3*{TpFEJL0K z{iU2gxe<7=50-!nd=8ZJz2xi3**qNpZv5pLV`yj8_vs&73QC>l2}FXaw&=z`hAIFHRY1{$T;>{u^g}{1lp(s0CR> zpXP%7kFe&i1W`{M0f{L}%_E3xSGm<^e=yifzxlOuo>=~?;1hGDO$8$S+Fk`GZscly ziGL81HWj;&?rzoa62DFl^1KK1=sG-i&{>`I^Tf)4(R~2|IutGFLLa~=h2l(@tf+vs zGFMR5oQMXf_$kK1+`OI|VG7BbIWnC;I~tH-Tfy?5;=YHY!6mXJg`feuJe190>9~<8R;YN04v$ z&Xtjd<*&2E2g(7Cc|1+uxvnLTk@%AP}z)?{<6ralZQXcVbQt1JQn1+9*6 z9LK>=_`NfFOM7}kM6kL)XY!0kLX0_OPo^}$8qpkiNN*P&meeyoc{}#lVZa6P^tzOf zezTAVZzp$z?6*f66;zc)9d30)t9$SJ^FBiVx_>)n_*Wn!!97o0gxwrtsz@$K8xqzr6V~<8Oy7{dyeW!lm#jRNN z`GE@^C(7tXl{J&Hr}xQAhU%d{I&~^Oan*BZq?U9$<)AwV4vn#D1%2EE&zXC6lYTqD z?@_J!fpACY11v*oVR@cF>ACHLw+P=~zC=>m)H^2?yexz@4cCFAIm*hsaWCT8AaE78 zv1j}`^Lx;g8R0$m{rR)=UqyDy?7-<8Ht^bFg7LV9%YXT}%K$}f@+b<8N@sTc6A?^9 zK2c&1kJPa|B4WK}m%Rff&F*)lV9Vdh6+l3&8)zSEqv0|7KeH15OtoFh@OY-Thg74ohx0ij^Dqgsd9n_$O1V5J3$ z0nJyogX*`v`FQzKUB-SIKaFoQZsE{L&`kYwz@Vgq15ig)p1mi9(#%Te$kX6@rzg>a znzbHs=)_NASJfYzO-uHgilK(Y)unO702i8S&D3>wHNg=9$$6~Tie*YM}!G?hzWXv+&*MhScwKSjM2o9rcOQcBpdrB zJZy)6vZ7{uBj-oR-6RgIInM=|1mI;lVp90;rM3-rM=OsIg)AyHAc6S`K9oyWh}-KQ zFW6ahS!M?y25T@QMuR8>nXTGi4O<^*$DFItzn%e`M&#g#dQCF<{1zKeY&dQuI;H;S z-I$TR9=wTE4N@0kjzG#vh@y=ctqueBcB~l+@ON*3JCiHmVDhTuWo2Eg=FIVqXYYKP zbpC_`+dOOy#`A_}KK(F)ZG3{L`=@DP3miqy&w|8opffnj9g)Wue?SMueV7PY%+{=0 zX!WBO*RW^H+>AXsEVFp-F6u=W%tONLT3lI4hP0FTQi6^kWzUY)SM&D}=LRAvL}c5> zV90DSQ!g&3$H#WBZR(4fFM*WxIBPkUjX<+#t-y8ELQ<$2j)Xx>cay{NZ!8I2=~tzbM(Z4 zp1BybDZBTrmnR#q8_Fe72CZvoh(aIF9yQ^?%}Ybl-8U+PdZO+ka{wiVSRX0hUd)2Q z=LMERgegA!0P;%!CK=j~2S zFs7X`gWZY|=SPU5i7Bb;wRx0fFcAhI_M_ZQOj?hzAscv2sc*?Pm3XL*=LrP94{Fjd z{;J#A)lZDRf1zb+sd)d{z-^>`4&XUVoEiWgqPGBX{7awDzx?c`XC%%TFPaW0w=H!^ z+9X53zvlbLh~<_Cl2gk+M6`x;o%sinz4NGUDJf?uqtJJfDD<(;dDh-*u(@w^{`dgY zLi{XzGQ(|po~XVKMd0r9bDTroA1U2YB`q|zXN9OuohAg zPN4?)_R?hyQ|Vu24xr|eQ*;#^$tv}$jPp?d&;^StUmlo`g+;$K<5@08>kg(K@MvDw zc31vnEWtUX^9_^(y~N)Ia#*~3kG`}zV-ph-6h9*I0!&=TV)hY^3R4IA5!n5=!&^c% zm|H*kK3jN}krKB5iRvb!AqIaAj*rZ5XYIehX}ixHGJ~g3lfYi;de|Ma^ULM7lol7C z*N&E^>$C?w8q0m==X_6f$X&qw5{?zZz-5UY1k&Rp*nLS=TD%(&4;0uvK>5d&D+1Ol zmwDr?w*W|AbLpIBbeWP}(ZM3FV$=BRWYu1H&7Ja0F!H<>k`@gAhA3TBp<*K8c=nCw zJgBmJ*^RxhlE}(>IWi_&Wb_pV6k-~^Uos)(;>P)q&+KGKkSJtM6)BO!VWO(f_Us&W zt23Klb2q;ra1zl9bl-ENSWu#396eHhs7UKSYsnlSl1Pc5mI`SD@RvNiV^ePY_QJ0J z;{jr2Lt{u&6JTsWQ(U*qfo4ZnV5}&Pd(9+Oz_15hqyw4!VQvp&nFRu~XoIh)iKfz! zvU@r^)WNkNTp-3FK2WNh;P~{9a(NpSze9#ey%u({qF2FY_lUhSz@(Jlhwd`VWv zLFILJk5!>l?_|~q8EhJ6z_C3WmxV2)B8gw$>hrhpj5gc?I;FXc$DXl4N{V^I`sPnx z1+F5#I;;P=G!u4bVJJHQ={sTDEbhxM*u9tssf~lj{4Kxlnw6+^Cxv`~PpCy(VT?$; z*-^nq2A3e-agb_DZai5`|Lqkdkyp@8l)tExB?}JxEv#(o;D}+|r+rlp1n(gr+beKs ztbNoK5vc99ZbZlLE8-C5-)4nS6ZCt+UPuI{IOVX{-N`ja+aOm|XOR67ZU*^TYBIyW z4VS|QjI|2NKoubvhcXhqd!3>T(9;7bH0s&u%Rs_`DbAucvO#|Sg~u%Om6SrUJwUDhHw&Gi2C&5DDar+4k3>Z4@r8s zZ8ynS{`txT;^a|vaBwhXN5!`v4?q)zB-xzs-)~-o-5G&(SuQ=RPJq)5*PQ=uWoV^Tm^l`lqSRnJIT(h2M9kt)1N^T_GW%)t#M=3X7w_ zGb2$e}&M?In4w8V-RTe z$=lo8qvfX7!VHrRt#j-Tmf6s5ziQh+zBjz3Z>Y|Ly*~i%`TgKRgCR)1oH14WuLb0qwoyP};X`+hQpxxZC2% z>}_oHw)NC-_!#iZ(cXOSy&1qg%TmI8lo_ZORw3Jw*NP{jk|vE&#p5T=(Oqix_dlBh zlo>w`KG4HJuX>K?U!Vz`GP-m9?}uss`@(1^y149wx~nEG4OmJh$A-1q>hY>(-v0-s C+t(xj diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx b/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx index e1a75d59aa1f5b86eb270ba6265b0a264a876978..e9ce8ca1f1818b9e66940f6917ebdec53e0065be 100644 GIT binary patch delta 502 zcmaE0^1y^Az?+#xgn@&DgCQVcBab2@Q$WIIea4j_dh#u%mmumWvub^;_hAEx*8g57 zYiHLj=$TNqV3Rg`lkm~q6ZYJ0$`zVaUiSC{#D8v(+VzlNt_bT#RBil~kCJdU)yA4^P%I_e(hmDXLvb>3<=y)bZUy zVa4uEou{t-@=LJ!C0puL*CBhssQ!F@CcDNI2S@j)S7|bqA7_+=yK7l0-#xfp*uj%a zwzir4xc%#6rPll`lnL@My}=G(&I~$*K+LWv(7PHvHM}pZ|#ro7cI9~p>i$JV#kha{hy-l+UJBV(fs_$ zSnaHMW@Jv+q^GK@SAGBZw7cf;%cpzJiU)YJb7HC&Q4>g_Z)w2cjyZINZ-DFdfBa=FP2U(IQP@yVaL;r*_-(}E?Ptj zhfPsoUOmUGEPV3wvs&>JbQW!i+IG&#*H7T&Cc#sz6+Gvh9heQh1*Ee#xqq2+X~nk- zCnHpDs-?bmQa6-6+*h^o-c!De(E4fSTbQRrEn32?`(3ziPG!u`diN|-#lMPMlpVCW zbW0KpgTf4|J}R~7{gafKda|-7t>|lQ{Fluar%M_B+ErH}b^7@+KKYdmCQjPxm^?}r zatiX_vT&@w!}+vQTY24OzG&uO-%2+AopvPrVzErgv{wZ-Ket@l{>f>SaWsSW^qOa1~n!I2ISD(EX}G74o(lY zE--zCT^LNiR2xDXP$^rm>L Date: Thu, 21 Aug 2025 14:20:54 +0200 Subject: [PATCH 060/161] Fixed snapshot issues, second try --- .../generate_violin_plots/test_excel_dIEM.xlsx | Bin 6752 -> 0 bytes .../violin_pdf_P2025M1.pdf | Bin 28932 -> 0 bytes .../tests/testthat/test_generate_violin_plots.R | 10 ++++++---- 3 files changed, 6 insertions(+), 4 deletions(-) delete mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx delete mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.pdf diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx b/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx deleted file mode 100644 index e9ce8ca1f1818b9e66940f6917ebdec53e0065be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6752 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 M%!qtqB)vKPAMFfoo&W#< diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.pdf b/DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.pdf deleted file mode 100644 index 799d6504f3363a2fa7a32ff102881aa2cd4cbaba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28932 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

Q*wRoZ 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

ck} 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 Date: Thu, 21 Aug 2025 14:25:49 +0200 Subject: [PATCH 061/161] Fixing snapshot issue, third try --- .../generate_violin_plots/test_excel_dIEM.xlsx | Bin 0 -> 6753 bytes .../violin_pdf_P2025M1.pdf | Bin 0 -> 28932 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx create mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.pdf diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx b/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..1f0e614f3b852b516ef4227f531a9ba209f3b2fc GIT binary patch literal 6753 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 NV@2c>3;Ffw{{Wb>Y9#;w literal 0 HcmV?d00001 diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.pdf b/DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e285f615d4af8007ef593d0be114b08a93dce122 GIT binary patch literal 28932 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

`OU!;?OU&?5Z~#p;cHR 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 I=0<$~3u7WaRsaA1 literal 0 HcmV?d00001 From 936f292fc5fa1bca1797f41ace7783db0108cd7a Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 21 Aug 2025 14:50:47 +0200 Subject: [PATCH 062/161] Fixed snapshot issue, fourth try --- .github/workflows/dims_test.yml | 2 +- DIMS/tests/testthat.R | 1 + .../testthat/_snaps/generate_violin_plots.md | 21 ++++++++++++++++++ .../test_excel_dIEM.xlsx | Bin 6753 -> 0 bytes .../violin_pdf_P2025M1.pdf | Bin 28932 -> 0 bytes .../testthat/test_generate_violin_plots.R | 6 +++-- 6 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots.md delete mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx delete mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.pdf diff --git a/.github/workflows/dims_test.yml b/.github/workflows/dims_test.yml index b547d126..e8c4078c 100644 --- a/.github/workflows/dims_test.yml +++ b/.github/workflows/dims_test.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 - name: Install dependencies - run: Rscript -e "install.packages(c('testthat', 'withr', 'vdiffr'))" + run: Rscript -e "install.packages(c('testthat', 'withr', 'vdiffr', 'pdftools'))" - name: Run tests run: Rscript tests/testthat.R diff --git a/DIMS/tests/testthat.R b/DIMS/tests/testthat.R index 636acf7d..6e5a4962 100644 --- a/DIMS/tests/testthat.R +++ b/DIMS/tests/testthat.R @@ -2,6 +2,7 @@ library(testthat) library(withr) library(vdiffr) +library(pdftools) # enable snapshots local_edition(3) diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots.md b/DIMS/tests/testthat/_snaps/generate_violin_plots.md new file mode 100644 index 00000000..5df67e7a --- /dev/null +++ b/DIMS/tests/testthat/_snaps/generate_violin_plots.md @@ -0,0 +1,21 @@ +# Create a pdf with a table of top metabolites and violin plots + + Code + content_pdf_violinplots + Output + [1] "Top deviating metabolites for patient: P2025M1\n Metabolite Z.score\n Increased\n metab1 2.45\n Decreased\n metab11 −1.51\n" + [2] " Results for patient P2025M1\n test acyl carnitines\n metab1 Z=2.34\nMetabolites\n metab3 Z=0.31\n −5 0 5 10 15 20\n Z−scores\n" + [3] " Results for patient P2025M1\n test crea gua\n metab4 Z=−0.46\nMetabolites\n metab11 Z=0.84\n −5 0 5 10 15 20\n Z−scores\n" + [4] " Unit test Generate Violin Plots\nUnit test Generate Violin Plots\n" + +# Saving the probability score dataframe as an Excel file + + Disease P2025M1 P2025M2 P2025M3 P2025M4 + 1 Disease A 10.900 -10.9 49.90 -49.9 + 2 Disease B 0.953 0.0 2.29 0.0 + 3 Disease C 12.100 0.0 0.00 12.1 + 4 Disease D 0.000 -12.5 0.00 18.2 + 5 Disease E 44.300 0.0 0.00 28.1 + 6 Disease F 0.000 -77.4 -77.40 0.0 + 7 Disease G -38.700 38.7 38.70 -38.7 + diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx b/DIMS/tests/testthat/_snaps/generate_violin_plots/test_excel_dIEM.xlsx deleted file mode 100644 index 1f0e614f3b852b516ef4227f531a9ba209f3b2fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6753 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 NV@2c>3;Ffw{{Wb>Y9#;w diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.pdf b/DIMS/tests/testthat/_snaps/generate_violin_plots/violin_pdf_P2025M1.pdf deleted file mode 100644 index e285f615d4af8007ef593d0be114b08a93dce122..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28932 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

`OU!;?OU&?5Z~#p;cHR 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 I=0<$~3u7WaRsaA1 diff --git a/DIMS/tests/testthat/test_generate_violin_plots.R b/DIMS/tests/testthat/test_generate_violin_plots.R index e80df979..55a2dac1 100644 --- a/DIMS/tests/testthat/test_generate_violin_plots.R +++ b/DIMS/tests/testthat/test_generate_violin_plots.R @@ -325,7 +325,8 @@ testthat::test_that("Create a pdf with a table of top metabolites and violin plo out_pdf_violinplots <- file.path(test_pdf_dir, "R_P2025M1.pdf") expect_true(file.exists(out_pdf_violinplots)) - expect_snapshot_file(out_pdf_violinplots, "violin_pdf_P2025M1.pdf") + content_pdf_violinplots <- pdftools::pdf_text(out_pdf_violinplots) + expect_snapshot(content_pdf_violinplots) unlink(test_pdf_dir, recursive = TRUE) }) @@ -393,7 +394,8 @@ testthat::test_that("Saving the probability score dataframe as an Excel file", { expect_silent(save_prob_scores_to_excel(test_probability_score_df, test_output_dir, test_run_name)) expect_true(file.exists(out_excel_file)) - expect_snapshot_file(out_excel_file, "test_excel_dIEM.xlsx") + content_excel_file <- openxlsx::read.xlsx(out_excel_file, sheet = 1) + expect_snapshot_output(content_excel_file) unlink(test_output_dir, recursive = TRUE) }) From 926ad6d0c0985b677501eed5da072229e808444d Mon Sep 17 00:00:00 2001 From: ALuesink Date: Mon, 8 Sep 2025 13:14:48 +0200 Subject: [PATCH 063/161] Fixed issue if P1001 is present but no Z-scores --- DIMS/GenerateQCOutput.R | 1 + 1 file changed, 1 insertion(+) diff --git a/DIMS/GenerateQCOutput.R b/DIMS/GenerateQCOutput.R index 321c6ec0..5a439d5c 100644 --- a/DIMS/GenerateQCOutput.R +++ b/DIMS/GenerateQCOutput.R @@ -328,6 +328,7 @@ if (length(sst_colnrs) > 0) { } else { sst_list_intensities <- sst_list[, intensity_col_ids] } +sst_list_intensities <- as.data.frame(sst_list_intensities) for (col_nr in seq_len(ncol(sst_list_intensities))) { sst_list_intensities[, col_nr] <- as.numeric(sst_list_intensities[, col_nr]) if (grepl("Zscore", colnames(sst_list_intensities)[col_nr])) { From b1a38581172cc113cd2986e38ddd23cfa86157fa Mon Sep 17 00:00:00 2001 From: ALuesink Date: Mon, 8 Sep 2025 16:32:47 +0200 Subject: [PATCH 064/161] print statement for testing --- DIMS/GenerateViolinPlots.R | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 847c4cb5..801f61bb 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -101,6 +101,8 @@ zscore_controls_df <- intensities_zscore_ratios_df %>% select(HMDB_code, HMDB_na colnames(zscore_patients_df) <- gsub("_Zscore", "", colnames(zscore_patients_df)) colnames(zscore_controls_df) <- gsub("_Zscore", "", colnames(zscore_controls_df)) +cat(colnames(expected_biomarkers_df)) + expected_biomarkers_df <- expected_biomarkers_df %>% rename(HMDB_code = HMDB.code, HMDB_name = Metabolite) expected_biomarkers_info <- expected_biomarkers_df %>% From ba47db588619c69b0852a736163c6c832dc91169 Mon Sep 17 00:00:00 2001 From: Anne Luesink Date: Thu, 11 Sep 2025 09:29:05 +0200 Subject: [PATCH 065/161] Removed duplicated line & print statement --- DIMS/GenerateViolinPlots.R | 4 ---- 1 file changed, 4 deletions(-) diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 801f61bb..224b1f70 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -101,8 +101,6 @@ zscore_controls_df <- intensities_zscore_ratios_df %>% select(HMDB_code, HMDB_na colnames(zscore_patients_df) <- gsub("_Zscore", "", colnames(zscore_patients_df)) colnames(zscore_controls_df) <- gsub("_Zscore", "", colnames(zscore_controls_df)) -cat(colnames(expected_biomarkers_df)) - expected_biomarkers_df <- expected_biomarkers_df %>% rename(HMDB_code = HMDB.code, HMDB_name = Metabolite) expected_biomarkers_info <- expected_biomarkers_df %>% @@ -155,8 +153,6 @@ for (metabolite_dir in metabolite_dirs) { } #### Run the IEM algorithm ######### -expected_biomarkers_df <- expected_biomarkers_df %>% rename(HMDB_code = HMDB.code, HMDB_name = Metabolite) - diem_probability_score <- run_diem_algorithm(expected_biomarkers_df, zscore_patients_df, patients) save_prob_scores_to_excel(diem_probability_score, output_dir, run_name) From c1e79a557eeae1f81c240569ac3721188cd049f7 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 30 Sep 2025 12:57:27 +0200 Subject: [PATCH 066/161] rewritten selection of segment size for HMDB parts --- DIMS/HMDBparts.R | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/DIMS/HMDBparts.R b/DIMS/HMDBparts.R index a7768c72..8c2234f4 100755 --- a/DIMS/HMDBparts.R +++ b/DIMS/HMDBparts.R @@ -30,14 +30,13 @@ if (standard_run == "yes" & min_mz > 68 & min_mz < 71 & max_mz > 599 & max_mz < segment_end <- min_mz + 5 while (segment_end < max_mz) { if (segment_start < 100) { - mz_segments <- c(mz_segments, segment_start) - segment_start <- segment_start + 5 - segment_end <- segment_end + 5 + segment_size = 5 } else { - mz_segments <- c(mz_segments, segment_start) - segment_start <- segment_start + 10 - segment_end <- segment_end + 10 + segment_size = 10 } + mz_segments <- c(mz_segments, segment_start) + segment_start <- segment_start + segment_size + segment_end <- segment_end + segment_size } #last segment mz_segments <- c(mz_segments, max_mz) From 109d664f1e1923a0b95e1cfadc67c78bd991310d Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Thu, 2 Oct 2025 11:48:14 +0200 Subject: [PATCH 067/161] created function for averaging peaks in DIMS/AveragePeaks.R --- DIMS/AveragePeaks.R | 40 ++++---------------- DIMS/AveragePeaks.nf | 2 +- DIMS/preprocessing/average_peaks_functions.R | 39 +++++++++++++++++++ 3 files changed, 47 insertions(+), 34 deletions(-) create mode 100644 DIMS/preprocessing/average_peaks_functions.R diff --git a/DIMS/AveragePeaks.R b/DIMS/AveragePeaks.R index eef2d8a2..7e8d9fe1 100644 --- a/DIMS/AveragePeaks.R +++ b/DIMS/AveragePeaks.R @@ -6,61 +6,35 @@ cmd_args <- commandArgs(trailingOnly = TRUE) sample_name <- cmd_args[1] techreps <- cmd_args[2] scanmode <- cmd_args[3] +preprocessing_scripts_dir <- cmd_args[4] tech_reps <- strsplit(techreps, ";")[[1]] -print(sample_name) -print(techreps) -print(scanmode) + +# load in function scripts +source(paste0(preprocessing_scripts_dir, "average_peaks_functions.R")) # set ppm as fixed value, not the same ppm as in peak grouping ppm_peak <- 2 -# average peaks in technical replicates # Initialize per sample peaklist_allrepl <- NULL nr_repl_persample <- 0 averaged_peaks <- matrix(0, nrow = 0, ncol = 6) colnames(averaged_peaks) <- c("samplenr", "mzmed.pkt", "fq", "mzmin.pkt", "mzmax.pkt", "height.pkt") + # load RData files of technical replicates belonging to biological sample for (file_nr in 1:length(tech_reps)) { tech_repl_file <- paste0(tech_reps[file_nr], "_", scanmode, ".RData") tech_repl <- get(load(tech_repl_file)) # combine data for all technical replicates peaklist_allrepl <- rbind(peaklist_allrepl, tech_repl) - # count number of replicates for each biological sample - nr_repl_persample <- nr_repl_persample + 1 } # sort on mass peaklist_allrepl_df <- as.data.frame(peaklist_allrepl) peaklist_allrepl_df$mzmed.pkt <- as.numeric(peaklist_allrepl_df$mzmed.pkt) peaklist_allrepl_df$height.pkt <- as.numeric(peaklist_allrepl_df$height.pkt) -# peaklist_allrepl_sorted <- peaklist_allrepl_df %>% arrange(desc(height.pkt)) peaklist_allrepl_sorted <- peaklist_allrepl_df %>% arrange(mzmed.pkt) + # average over technical replicates -while (nrow(peaklist_allrepl_sorted) > 1) { - # store row numbers - peaklist_allrepl_sorted$rownr <- 1:nrow(peaklist_allrepl_sorted) - # find the peaks in the dataset with corresponding m/z plus or minus tolerance - reference_mass <- peaklist_allrepl_sorted$mzmed.pkt[1] - mz_tolerance <- (reference_mass * ppm_peak) / 10^6 - minmz_ref <- reference_mass - mz_tolerance - maxmz_ref <- reference_mass + mz_tolerance - select_peak_indices <- which((peaklist_allrepl_sorted$mzmed.pkt > minmz_ref) & (peaklist_allrepl_sorted$mzmed.pkt < maxmz_ref)) - select_peaks <- peaklist_allrepl_sorted[select_peak_indices, ] - nrsamples <- length(select_peak_indices) - # put averaged intensities into a new row and append to averaged_peaks - averaged_1peak <- matrix(0, nrow = 1, ncol = 6) - colnames(averaged_1peak) <- c("samplenr", "mzmed.pkt", "fq", "mzmin.pkt", "mzmax.pkt", "height.pkt") - # calculate m/z values for peak group - averaged_1peak[1, "mzmed.pkt"] <- mean(select_peaks$mzmed.pkt) - averaged_1peak[1, "mzmin.pkt"] <- min(select_peaks$mzmed.pkt) - averaged_1peak[1, "mzmax.pkt"] <- max(select_peaks$mzmed.pkt) - averaged_1peak[1, "fq"] <- nrsamples - averaged_1peak[1, "height.pkt"] <- mean(select_peaks$height.pkt) - # put intensities into proper columns - peaklist_allrepl_sorted <- peaklist_allrepl_sorted[-select_peaks$rownr, ] - averaged_peaks <- rbind(averaged_peaks, averaged_1peak) -} -# add sample name to first column -averaged_peaks[ , "samplenr"] <- sample_name +averaged_peaks <- average_peaks_per_sample(peaklist_allrepl_sorted) save(averaged_peaks, file = paste0("AvgPeaks_", sample_name, "_", scanmode, ".RData")) diff --git a/DIMS/AveragePeaks.nf b/DIMS/AveragePeaks.nf index de4b21b0..f50bd874 100644 --- a/DIMS/AveragePeaks.nf +++ b/DIMS/AveragePeaks.nf @@ -13,6 +13,6 @@ process AveragePeaks { script: """ - Rscript ${baseDir}/CustomModules/DIMS/AveragePeaks.R $sample_id $tech_reps $scanmode + Rscript ${baseDir}/CustomModules/DIMS/AveragePeaks.R $sample_id $tech_reps $scanmode $params.preprocessing_scripts_dir """ } diff --git a/DIMS/preprocessing/average_peaks_functions.R b/DIMS/preprocessing/average_peaks_functions.R new file mode 100644 index 00000000..2545b757 --- /dev/null +++ b/DIMS/preprocessing/average_peaks_functions.R @@ -0,0 +1,39 @@ +average_peaks_per_sample <- function(peaklist_allrepl_sorted) { + #' Average the intensity of peaks that occur in different technical replicates of a biological sample + #' + #' @param peaklist_allrepl_sorted: Dataframe with peaks sorted on median m/z (float) + #' + #' @return averaged_peaks: matrix of averaged peaks (float) + + # initialize + averaged_peaks <- peaklist_allrepl_sorted[0, ] + + while (nrow(peaklist_allrepl_sorted) > 1) { + # store row numbers + peaklist_allrepl_sorted$rownr <- 1:nrow(peaklist_allrepl_sorted) + # find the peaks in the dataset with corresponding m/z plus or minus tolerance + reference_mass <- peaklist_allrepl_sorted$mzmed.pkt[1] + mz_tolerance <- (reference_mass * ppm_peak) / 10^6 + minmz_ref <- reference_mass - mz_tolerance + maxmz_ref <- reference_mass + mz_tolerance + select_peak_indices <- which((peaklist_allrepl_sorted$mzmed.pkt > minmz_ref) & (peaklist_allrepl_sorted$mzmed.pkt < maxmz_ref)) + select_peaks <- peaklist_allrepl_sorted[select_peak_indices, ] + nrsamples <- length(select_peak_indices) + # put averaged intensities into a new row + averaged_1peak <- matrix(0, nrow = 1, ncol = 6) + colnames(averaged_1peak) <- c("samplenr", "mzmed.pkt", "fq", "mzmin.pkt", "mzmax.pkt", "height.pkt") + # calculate m/z values for peak group + averaged_1peak[1, "mzmed.pkt"] <- mean(select_peaks$mzmed.pkt) + averaged_1peak[1, "mzmin.pkt"] <- min(select_peaks$mzmed.pkt) + averaged_1peak[1, "mzmax.pkt"] <- max(select_peaks$mzmed.pkt) + averaged_1peak[1, "fq"] <- nrsamples + averaged_1peak[1, "height.pkt"] <- mean(select_peaks$height.pkt) + # remove rownr column and append to averaged_peaks + peaklist_allrepl_sorted <- peaklist_allrepl_sorted[-select_peaks$rownr, ] + averaged_peaks <- rbind(averaged_peaks, averaged_1peak) + } + # add sample name to first column + averaged_peaks[ , "samplenr"] <- sample_name + + return(averaged_peaks) +} From cb33aba206ce326a79347404a6b76769af3e38d4 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Thu, 2 Oct 2025 11:49:30 +0200 Subject: [PATCH 068/161] added unit tests for average_peaks_functions --- DIMS/tests/testthat/test_average_peaks.R | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 DIMS/tests/testthat/test_average_peaks.R diff --git a/DIMS/tests/testthat/test_average_peaks.R b/DIMS/tests/testthat/test_average_peaks.R new file mode 100644 index 00000000..994de48c --- /dev/null +++ b/DIMS/tests/testthat/test_average_peaks.R @@ -0,0 +1,26 @@ +# unit test for AveragePeaks + +# source all functions for PeakGrouping +source("../../preprocessing/average_peaks_functions.R") + +# test find_peak_groups +testthat::test_that("peaks are correctly averaged", { + # create peak list to test on: + samplenrs <- rep(c("repl_001", "repl_002", "repl_003"), 3) + test_peaklist_sorted_num <- matrix(0, nrow = 9, ncol = 5) + colnames(test_peaklist_sorted_num) <- c("mzmed.pkt", "fq", "mzmin.pkt", "mzmax.pkt", "height.pkt") + test_peaklist_sorted_num[, 1] <- 300 + (1:9) / 10000 + test_peaklist_sorted_num[, 5] <- 100 * (9:1) + test_peaklist_sorted <- as.data.frame(cbind(samplenr = samplenrs, test_peaklist_sorted_num)) + test_peaklist_sorted[, 2] <- as.numeric(test_peaklist_sorted[, 2]) + test_peaklist_sorted[, 6] <- as.numeric(test_peaklist_sorted[, 6]) + + # test that first peak is correctly averaged + expect_equal(as.numeric(average_peaks_per_sample(test_peaklist_sorted)[1, 6]), 600, tolerance = 0.001, TRUE) + # test number of rows + expect_equal(nrow(average_peaks_per_sample(test_peaklist_sorted)), 2) + # test column names + expect_equal(colnames(average_peaks_per_sample(test_peaklist_sorted)), + c("samplenr", "mzmed.pkt", "fq", "mzmin.pkt", "mzmax.pkt", "height.pkt"), TRUE) +}) + From db8633e61e15b27c1c052ec4462f7e0b6cfa5520 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 3 Oct 2025 13:49:00 +0200 Subject: [PATCH 069/161] moved parameters matrix and nr_replicates from workflow into params --- DIMS/EvaluateTics.nf | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/DIMS/EvaluateTics.nf b/DIMS/EvaluateTics.nf index 831c9500..fdf7f713 100644 --- a/DIMS/EvaluateTics.nf +++ b/DIMS/EvaluateTics.nf @@ -8,9 +8,7 @@ process EvaluateTics { path(rdata_file) path(tic_txt_files) path(init_file) - val(nr_replicates) val(analysis_id) - val(matrix) path(highest_mz_file) path(trim_params_file) @@ -19,14 +17,14 @@ process EvaluateTics { path('replicates_per_sample.txt'), emit: sample_techreps path('miss_infusions_negative.txt') path('miss_infusions_positive.txt') - path('*_TICplots.pdf') + path('*_TICplots.pdf'), emit: tic_plots_pdf script: """ Rscript ${baseDir}/CustomModules/DIMS/EvaluateTics.R $init_file \ $params.nr_replicates \ $analysis_id \ - $matrix \ + $params.matrix \ $highest_mz_file \ $trim_params_file """ From 004e3e9aad7b92472d52d4083bdf193ccefef11e Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 3 Oct 2025 13:51:15 +0200 Subject: [PATCH 070/161] refactored DIMS/EvaluateTics --- DIMS/EvaluateTics.R | 109 +++++++------------------------------------- 1 file changed, 16 insertions(+), 93 deletions(-) diff --git a/DIMS/EvaluateTics.R b/DIMS/EvaluateTics.R index fb73576a..a61e910f 100644 --- a/DIMS/EvaluateTics.R +++ b/DIMS/EvaluateTics.R @@ -14,29 +14,6 @@ highest_mz <- get(load(highest_mz_file)) trim_params_filepath <- cmd_args[6] thresh2remove <- 1000000000 -remove_from_repl_pattern <- function(bad_samples, repl_pattern, nr_replicates) { - # collect list of samples to remove from replication pattern - remove_from_group <- NULL - for (sample_nr in 1:length(repl_pattern)){ - repl_pattern_1sample <- repl_pattern[[sample_nr]] - remove <- NULL - for (file_nr in 1:length(repl_pattern_1sample)) { - if (repl_pattern_1sample[file_nr] %in% bad_samples) { - remove <- c(remove, file_nr) - } - } - if (length(remove) == nr_replicates) { - remove_from_group <- c(remove_from_group, sample_nr) - } - if (!is.null(remove)) { - repl_pattern[[sample_nr]] <- repl_pattern[[sample_nr]][-remove] - } - } - if (length(remove_from_group) != 0) { - repl_pattern <- repl_pattern[-remove_from_group] - } - return(list("pattern" = repl_pattern)) -} # load init_file: contains repl_pattern load(init_file) @@ -52,86 +29,31 @@ if (highest_mz > 700) { thresh2remove <- 1000000 } -# remove technical replicates which are below the threshold and average intensity over time -remove_neg <- NULL -remove_pos <- NULL -cat("Pklist sum threshold to remove technical replicate:", thresh2remove, "\n") -for (sample_nr in 1:length(repl_pattern)) { - tech_reps <- as.vector(unlist(repl_pattern[sample_nr])) - tech_reps_array_pos <- NULL - tech_reps_array_neg <- NULL - for (file_nr in 1:length(tech_reps)) { - load(paste(tech_reps[file_nr], ".RData", sep = "")) - cat("\n\nParsing", tech_reps[file_nr]) - # positive scan mode: determine whether sum of intensities is above threshold - cat("\n\tPositive peak_list sum", sum(peak_list$pos[, 1])) - if (sum(peak_list$pos[, 1]) < thresh2remove) { - cat(" ... Removed") - remove_pos <- c(remove_pos, tech_reps[file_nr]) - } - tech_reps_array_pos <- cbind(tech_reps_array_pos, peak_list$pos) - # negative scan mode: determine whether sum of intensities is above threshold - cat("\n\tNegative peak_list sum", sum(peak_list$neg[, 1])) - if (sum(peak_list$neg[, 1]) < thresh2remove) { - cat(" ... Removed") - remove_neg <- c(remove_neg, tech_reps[file_nr]) - } - tech_reps_array_neg <- cbind(tech_reps_array_neg, peak_list$neg) - } -} +# find out which technical replicates are below the threshold +remove_tech_reps <- find_bad_replicates(repl_pattern, thresh2remove) +print(remove_tech_reps) # negative scan mode -print("neg") -pattern_list <- remove_from_repl_pattern(remove_neg, repl_pattern, nr_replicates) -repl_pattern_filtered <- pattern_list$pattern -print(repl_pattern_filtered) -print(length(repl_pattern_filtered)) +remove_neg <- remove_tech_reps$neg +repl_pattern_filtered <- remove_from_repl_pattern(remove_neg, repl_pattern, nr_replicates) save(repl_pattern_filtered, file = "negative_repl_pattern.RData") -# save replication pattern in txt file for use in Nextflow -allsamples_techreps_neg <- matrix("", ncol = 3, nrow = length(repl_pattern_filtered)) -for (sample_nr in 1:length(repl_pattern_filtered)) { - allsamples_techreps_neg[sample_nr, 1] <- names(repl_pattern_filtered)[sample_nr] - allsamples_techreps_neg[sample_nr, 2] <- paste0(repl_pattern_filtered[[sample_nr]], collapse = ";") -} -allsamples_techreps_neg[, 3] <- "negative" - -# write information on miss_infusions -write.table(remove_neg, - file = "miss_infusions_negative.txt", - row.names = FALSE, - col.names = FALSE, - sep = "\t" -) # positive scan mode -pattern_list <- remove_from_repl_pattern(remove_pos, repl_pattern, nr_replicates) -print(pattern_list) -repl_pattern_filtered <- pattern_list$pattern +remove_pos <- remove_tech_reps$pos +repl_pattern_filtered <- remove_from_repl_pattern(remove_pos, repl_pattern, nr_replicates) save(repl_pattern_filtered, file = "positive_repl_pattern.RData") -# save replication pattern in txt file for use in Nextflow -allsamples_techreps_pos <- matrix("", ncol = 3, nrow = length(repl_pattern_filtered)) -for (sample_nr in 1:length(repl_pattern_filtered)) { - allsamples_techreps_pos[sample_nr, 1] <- names(repl_pattern_filtered)[sample_nr] - allsamples_techreps_pos[sample_nr, 2] <- paste0(repl_pattern_filtered[[sample_nr]], collapse = ";") -} -allsamples_techreps_pos[, 3] <- "positive" -# combine information on samples and technical replicates for pos and neg -allsamples_techreps <- rbind(allsamples_techreps_pos, allsamples_techreps_neg) -write.table(allsamples_techreps, - file = paste0("replicates_per_sample.txt"), +# get an overview of suitable technical replicates for both scan modes +allsamples_techreps_neg <- get_overview_tech_reps(repl_pattern_filtered, "negative") +allsamples_techreps_pos <- get_overview_tech_reps(repl_pattern_filtered, "positive") +allsamples_techreps_both_scanmodes <- rbind(allsamples_techreps_pos, allsamples_techreps_neg) +write.table(allsamples_techreps_both_scanmodes, + file = "replicates_per_sample.txt", col.names = FALSE, - row.names = FALSE, - sep = "," + row.names = FALSE, + sep = "," ) -# write information on miss_infusions -write.table(remove_pos, - file = "miss_infusions_positive.txt", - row.names = FALSE, - col.names = FALSE, - sep = "\t" -) ## generate TIC plots # get all txt files @@ -212,3 +134,4 @@ tic_plot_pdf <- marrangeGrob( ggsave(filename = paste0(run_name, "_TICplots.pdf"), tic_plot_pdf, width = 21, height = 29.7, units = "cm") + From 06e5e1a69b4e775bbffa615421715894c59d7389 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 3 Oct 2025 13:53:01 +0200 Subject: [PATCH 071/161] moved functions for DIMS/EvaluateTics to separate file --- DIMS/preprocessing/evaluate_tics_functions.R | 104 +++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 DIMS/preprocessing/evaluate_tics_functions.R diff --git a/DIMS/preprocessing/evaluate_tics_functions.R b/DIMS/preprocessing/evaluate_tics_functions.R new file mode 100644 index 00000000..057b07cd --- /dev/null +++ b/DIMS/preprocessing/evaluate_tics_functions.R @@ -0,0 +1,104 @@ +# EvaluateTics functions +find_bad_replicates <- function(repl_pattern, thresh2remove) { + #' Find technical replicates with a total intensity below a threshold + #' + #' @param repl_pattern: List of samples with corresponding technical replicates (strings) + #' @param thresh2remove: Threshold value for acceptance or rejection of total intensity (integer) + #' + #' @return remove_tech_reps: Array of rejected technical replicates (strings) + + remove_pos <- NULL + remove_neg <- NULL + cat("Pklist sum threshold to remove technical replicate:", thresh2remove, "\n") + for (sample_nr in 1:length(repl_pattern)) { + tech_reps <- as.vector(unlist(repl_pattern[sample_nr])) + tech_reps_array_pos <- NULL + #tech_reps_array_neg <- NULL + for (file_nr in 1:length(tech_reps)) { + load(paste0(tech_reps[file_nr], ".RData")) + cat("\n\nParsing", tech_reps[file_nr]) + # positive scan mode: determine whether sum of intensities is above threshold + cat("\n\tPositive peak_list sum", sum(peak_list$pos[, 1])) + if (sum(peak_list$pos[, 1]) < thresh2remove) { + cat(" ... Removed") + remove_pos <- c(remove_pos, tech_reps[file_nr]) + } + # negative scan mode: determine whether sum of intensities is above threshold + cat("\n\tNegative peak_list sum", sum(peak_list$neg[, 1])) + if (sum(peak_list$neg[, 1]) < thresh2remove) { + cat(" ... Removed") + remove_neg <- c(remove_neg, tech_reps[file_nr]) + } + } + } + cat("\n") + # write information on miss_infusions for both scan modes + write.table(remove_pos, + file = paste0("miss_infusions_positive.txt"), + row.names = FALSE, + col.names = FALSE, + sep = "\t" + ) + write.table(remove_neg, + file = paste0("miss_infusions_negative.txt"), + row.names = FALSE, + col.names = FALSE, + sep = "\t" + ) + + # combine removed technical replicates from pos and neg + remove_tech_reps <- list(pos = remove_pos, neg = remove_neg) + return(remove_tech_reps) +} + +remove_from_repl_pattern <- function(bad_samples, repl_pattern, nr_replicates) { + #' Remove technical replicates with insufficient quality from a biological sample + #' + #' @param bad_samples: Array of technical replicates of insufficient quality (strings) + #' @param repl_pattern: List of samples with corresponding technical replicates (strings) + #' @param nr_replicates: Number of technical replicates per biological sample (integer) + #' + #' @return repl_pattern_filtered: list of technical replicates of sufficient quality (strings) + + # collect list of samples to remove from replication pattern + remove_from_group <- NULL + for (sample_nr in 1:length(repl_pattern)){ + repl_pattern_1sample <- repl_pattern[[sample_nr]] + remove <- NULL + for (file_nr in 1:length(repl_pattern_1sample)) { + if (repl_pattern_1sample[file_nr] %in% bad_samples) { + remove <- c(remove, file_nr) + } + } + if (length(remove) == nr_replicates) { + remove_from_group <- c(remove_from_group, sample_nr) + } + if (!is.null(remove)) { + repl_pattern[[sample_nr]] <- repl_pattern[[sample_nr]][-remove] + } + } + if (length(remove_from_group) != 0) { + repl_pattern_filtered <- repl_pattern[-remove_from_group] + } else { + repl_pattern_filtered <- repl_pattern + } + return(repl_pattern_filtered) +} + +get_overview_tech_reps <- function(repl_pattern_filtered, scanmode) { + #' Create an overview of technical replicates with sufficient quality from a biological sample + #' + #' @param repl_pattern_filtered: List of samples with corresponding technical replicates (strings) + #' @param scanmode: Scan mode "positive" or "negative" (string) + #' + #' @return allsamples_techreps_scanmode: Matrix of technical replicates of sufficient quality (strings) + + allsamples_techreps_scanmode <- matrix("", ncol = 3, nrow = length(repl_pattern_filtered)) + for (sample_nr in 1:length(repl_pattern_filtered)) { + allsamples_techreps_scanmode[sample_nr, 1] <- names(repl_pattern_filtered)[sample_nr] + allsamples_techreps_scanmode[sample_nr, 2] <- paste0(repl_pattern_filtered[[sample_nr]], collapse = ";") + } + allsamples_techreps_scanmode[, 3] <- scanmode + return(allsamples_techreps_scanmode) +} + From 3f24b6dcaa615d0702ffdc634f583f8894ed25de Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 3 Oct 2025 13:54:54 +0200 Subject: [PATCH 072/161] added unit tests for DIMS/EvaluateTics --- DIMS/tests/testthat/test_evaluate_tics.R | 66 ++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 DIMS/tests/testthat/test_evaluate_tics.R diff --git a/DIMS/tests/testthat/test_evaluate_tics.R b/DIMS/tests/testthat/test_evaluate_tics.R new file mode 100644 index 00000000..b474a81b --- /dev/null +++ b/DIMS/tests/testthat/test_evaluate_tics.R @@ -0,0 +1,66 @@ +# unit test for EvaluateTics + +# source all functions for PeakGrouping +source("../../preprocessing/evaluate_tics_functions.R") + +# test find_bad_replicates +testthat::test_that("TICS are correctly accepted or rejected", { + # It's necessary to copy/symlink the files to the current location for the combine_sum_adducts_parts function + # local: setwd("~/Development/DIMS_refactor_PeakFinding_codereview/CustomModules/DIMS/tests/testthat") + test_files <- list.files("fixtures/", "test_evaluate_tics", full.names = TRUE) + file.symlink(file.path(test_files), getwd()) + + # create replication pattern to test on: + technical_replicates <- paste0("test_evaluate_tics_file", 1:3) + test_repl_pattern <- list(technical_replicates) + names(test_repl_pattern) <- "sample1" + test_thresh2remove <- 10^9 + + # test that output has two entries + expect_equal(length(find_bad_replicates(test_repl_pattern, test_thresh2remove)), 2) + # test that first technical replicate is removed in positive scan mode + expect_equal(find_bad_replicates(test_repl_pattern, test_thresh2remove)$pos, "test_evaluate_tics_file1", TRUE) + # test that third technical replicate is removed in negative scan mode + expect_equal(find_bad_replicates(test_repl_pattern, test_thresh2remove)$neg, "test_evaluate_tics_file3", TRUE) + # test that output files are generated + expect_equal(sum(grepl("miss_infusions_", list.files("./"))), 2) + + # Remove symlinked files + files_remove <- list.files("./", "SummedAdducts_test.RData", full.names = TRUE) + file.remove(files_remove) + +}) + +# test remove_from_repl_pattern +testthat::test_that("technical replicates are correctly removed from replication pattern", { + # create replication pattern to test on: + technical_replicates <- paste0("test_evaluate_tics_file", 1:3) + test_repl_pattern <- list(technical_replicates) + names(test_repl_pattern) <- "sample1" + test_bad_samples <- "test_evaluate_tics_file2" + test_nr_replicates <- 3 + + # test that the output contains 1 sample + expect_equal(length(remove_from_repl_pattern(test_bad_samples, test_repl_pattern, test_nr_replicates)), 1) + # test that the output for the sample contains 2 technical replicates + expect_equal(length(remove_from_repl_pattern(test_bad_samples, test_repl_pattern, test_nr_replicates)$sample1), 2) + # test that the second technical replicate has been removed + expect_false(unique(grepl(test_bad_samples, remove_from_repl_pattern(test_bad_samples, test_repl_pattern, test_nr_replicates)$sample1))) +}) + +# test get_overview_tech_reps +testthat::test_that("overview of technical replicates is correctly created", { + # create replication pattern to test on: + technical_replicates <- paste0("test_evaluate_tics_file", 1:3) + test_repl_pattern_filtered <- list(technical_replicates) + names(test_repl_pattern_filtered) <- "sample1" + test_scanmode <- "positive" + + # test that overview is correctly created + expect_equal(get_overview_tech_reps(test_repl_pattern_filtered, test_scanmode)[ ,1], "sample1") + expect_equal(get_overview_tech_reps(test_repl_pattern_filtered, test_scanmode)[ ,3], "positive") + expect_true(get_overview_tech_reps(test_repl_pattern_filtered, test_scanmode)[ ,2] == + paste0(technical_replicates, collapse = ";"), TRUE) + +}) + From c2c65ddfe715881f3bf2277a28b1e6931880c844 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 3 Oct 2025 14:11:17 +0200 Subject: [PATCH 073/161] modifications suggested in code review DIMS/PeakFinding --- DIMS/PeakFinding.R | 6 +++--- DIMS/preprocessing/peak_finding_functions.R | 6 ++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/DIMS/PeakFinding.R b/DIMS/PeakFinding.R index 529370d4..2cf49d20 100644 --- a/DIMS/PeakFinding.R +++ b/DIMS/PeakFinding.R @@ -1,3 +1,5 @@ +library(dplyr) + # define parameters cmd_args <- commandArgs(trailingOnly = TRUE) @@ -10,8 +12,6 @@ peak_thresh <- 2000 # source functions script source(paste0(preprocessing_scripts_dir, "peak_finding_functions.R")) -library(dplyr) - # Load output of AssignToBins (peak_list) for a technical replicate load(replicate_rdatafile) techrepl_name <- colnames(peak_list$pos)[1] @@ -22,7 +22,7 @@ techreps_passed <- read.table("replicates_per_sample.txt", sep=",") # Initialize options(digits = 16) -# run the findPeaks function +# do peak finding scanmodes <- c("positive", "negative") for (scanmode in scanmodes) { # get intensities for scan mode diff --git a/DIMS/preprocessing/peak_finding_functions.R b/DIMS/preprocessing/peak_finding_functions.R index 8c2f62d7..8539c03c 100644 --- a/DIMS/preprocessing/peak_finding_functions.R +++ b/DIMS/preprocessing/peak_finding_functions.R @@ -21,7 +21,7 @@ search_regions_of_interest <- function(ints_fullrange) { } else { regions_of_interest_gte3 <- regions_of_interest_length } - # test for length of roi. If length is greater than 11, remove roi and break up into separate rois + # test for length of roi (region of interest). If length is greater than 11, break up into separate rois remove_roi_index <- c() new_rois_all <- regions_of_interest_gte3[0, ] for (roi_nr in 1:nrow(regions_of_interest_gte3)) { @@ -43,7 +43,7 @@ search_regions_of_interest <- function(ints_fullrange) { new_rois_splitroi <- rbind(new_rois_splitroi, new_rois) start_pos <- new_rois[, 2] } - # last part + # intensities after last local minimum new_rois[, 1] <- start_pos new_rois[, 2] <- regions_of_interest_gte3[roi_nr, "to"] new_rois[, 3] <- new_rois[, 2] - new_rois[, 1] + 1 @@ -67,13 +67,11 @@ search_regions_of_interest <- function(ints_fullrange) { } # remove rois that have been split into chunks or shortened - #print(dim(regions_of_interest_gte3)) if (length(remove_roi_index) > 0) { regions_of_interest_minus_short <- regions_of_interest_gte3[-remove_roi_index, ] } else { regions_of_interest_minus_short <- regions_of_interest_gte3 } - #print(dim(regions_of_interest_minus_short)) # combine remaining rois with info on chunks regions_of_interest_split <- rbind(regions_of_interest_minus_short, new_rois_all) # remove regions of interest with short lengths again From 000b1781d6c85d41a95cabdce4e79ebd52458179 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 3 Oct 2025 16:39:18 +0200 Subject: [PATCH 074/161] added extra output on QC of SST sample to DIMS/GenerateQCOutput --- DIMS/GenerateQCOutput.R | 7 +++++++ DIMS/GenerateQCOutput.nf | 1 + 2 files changed, 8 insertions(+) diff --git a/DIMS/GenerateQCOutput.R b/DIMS/GenerateQCOutput.R index 4e3394d0..af63962f 100644 --- a/DIMS/GenerateQCOutput.R +++ b/DIMS/GenerateQCOutput.R @@ -356,6 +356,13 @@ xlsx_name <- paste0(outdir, "/", project, "_IS_SST.xlsx") openxlsx::saveWorkbook(wb, xlsx_name, overwrite = TRUE) rm(wb) +# generate text file for workflow completed mail for components with Z-score < 2 +if (grepl("Zscore", colnames(sst_list_intensities))) { + zscore_column <- grep("_Zscore", colnames(sst_list_intensities) + sst_list_intensities_qc <- sst_list_intensities[sst_list_intensities[, zscore_column] < 2, ] + write.table(sst_list_intensities_qc, file = paste(outdir, "sst_qc.txt", sep = "/")) +} + ### MISSING M/Z CHECK # check the outlist_identified_(negative/positive).RData files for missing m/z values and save to file diff --git a/DIMS/GenerateQCOutput.nf b/DIMS/GenerateQCOutput.nf index eb17301f..bee440a5 100644 --- a/DIMS/GenerateQCOutput.nf +++ b/DIMS/GenerateQCOutput.nf @@ -17,6 +17,7 @@ process GenerateQCOutput { tuple path('*_IS_SST.xlsx'), path('*_positive_control.xlsx'), optional: true path('plots/IS_*.png'), emit: plot_files path('Check_number_of_controls.txt'), optional: true + path('sst_qc.txt'), optional: true script: """ From 15a25ba489a52702b64122b2c2d0b2b9d4a3356f Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Thu, 16 Oct 2025 16:21:48 +0200 Subject: [PATCH 075/161] removed two obsolete lines --- DIMS/preprocessing/evaluate_tics_functions.R | 2 -- 1 file changed, 2 deletions(-) diff --git a/DIMS/preprocessing/evaluate_tics_functions.R b/DIMS/preprocessing/evaluate_tics_functions.R index 057b07cd..318ce2b6 100644 --- a/DIMS/preprocessing/evaluate_tics_functions.R +++ b/DIMS/preprocessing/evaluate_tics_functions.R @@ -12,8 +12,6 @@ find_bad_replicates <- function(repl_pattern, thresh2remove) { cat("Pklist sum threshold to remove technical replicate:", thresh2remove, "\n") for (sample_nr in 1:length(repl_pattern)) { tech_reps <- as.vector(unlist(repl_pattern[sample_nr])) - tech_reps_array_pos <- NULL - #tech_reps_array_neg <- NULL for (file_nr in 1:length(tech_reps)) { load(paste0(tech_reps[file_nr], ".RData")) cat("\n\nParsing", tech_reps[file_nr]) From 007bea441c48b9019b35e6d7ea5fdaa9190122ec Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Thu, 16 Oct 2025 17:14:26 +0200 Subject: [PATCH 076/161] moved parameter ppm_peak from DIMS/AveragePeaks.R to inside function --- DIMS/AveragePeaks.R | 3 --- DIMS/preprocessing/average_peaks_functions.R | 4 +++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/DIMS/AveragePeaks.R b/DIMS/AveragePeaks.R index 7e8d9fe1..997b1d3c 100644 --- a/DIMS/AveragePeaks.R +++ b/DIMS/AveragePeaks.R @@ -12,9 +12,6 @@ tech_reps <- strsplit(techreps, ";")[[1]] # load in function scripts source(paste0(preprocessing_scripts_dir, "average_peaks_functions.R")) -# set ppm as fixed value, not the same ppm as in peak grouping -ppm_peak <- 2 - # Initialize per sample peaklist_allrepl <- NULL nr_repl_persample <- 0 diff --git a/DIMS/preprocessing/average_peaks_functions.R b/DIMS/preprocessing/average_peaks_functions.R index 2545b757..77309acf 100644 --- a/DIMS/preprocessing/average_peaks_functions.R +++ b/DIMS/preprocessing/average_peaks_functions.R @@ -7,7 +7,9 @@ average_peaks_per_sample <- function(peaklist_allrepl_sorted) { # initialize averaged_peaks <- peaklist_allrepl_sorted[0, ] - + # set ppm as fixed value, not the same ppm as in peak grouping + ppm_peak <- 2 + while (nrow(peaklist_allrepl_sorted) > 1) { # store row numbers peaklist_allrepl_sorted$rownr <- 1:nrow(peaklist_allrepl_sorted) From e58640b703250c7c702c1f39a78606827854c890 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Thu, 16 Oct 2025 17:20:12 +0200 Subject: [PATCH 077/161] added parameter sample_name to DIMS/preprocessing/average_peaks_functions.R --- DIMS/AveragePeaks.R | 2 +- DIMS/preprocessing/average_peaks_functions.R | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DIMS/AveragePeaks.R b/DIMS/AveragePeaks.R index 997b1d3c..7114e3c4 100644 --- a/DIMS/AveragePeaks.R +++ b/DIMS/AveragePeaks.R @@ -32,6 +32,6 @@ peaklist_allrepl_df$height.pkt <- as.numeric(peaklist_allrepl_df$height.pkt) peaklist_allrepl_sorted <- peaklist_allrepl_df %>% arrange(mzmed.pkt) # average over technical replicates -averaged_peaks <- average_peaks_per_sample(peaklist_allrepl_sorted) +averaged_peaks <- average_peaks_per_sample(peaklist_allrepl_sorted, sample_name) save(averaged_peaks, file = paste0("AvgPeaks_", sample_name, "_", scanmode, ".RData")) diff --git a/DIMS/preprocessing/average_peaks_functions.R b/DIMS/preprocessing/average_peaks_functions.R index 77309acf..beed29bb 100644 --- a/DIMS/preprocessing/average_peaks_functions.R +++ b/DIMS/preprocessing/average_peaks_functions.R @@ -1,4 +1,4 @@ -average_peaks_per_sample <- function(peaklist_allrepl_sorted) { +average_peaks_per_sample <- function(peaklist_allrepl_sorted, sample_name) { #' Average the intensity of peaks that occur in different technical replicates of a biological sample #' #' @param peaklist_allrepl_sorted: Dataframe with peaks sorted on median m/z (float) From f0763a012907c191916b5d9fa6eed21be1386e42 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Thu, 16 Oct 2025 17:24:58 +0200 Subject: [PATCH 078/161] modified DIMS/tests/testthat/test_average_peaks.R for extra variable sample_name --- DIMS/tests/testthat/test_average_peaks.R | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/DIMS/tests/testthat/test_average_peaks.R b/DIMS/tests/testthat/test_average_peaks.R index 994de48c..bb945b71 100644 --- a/DIMS/tests/testthat/test_average_peaks.R +++ b/DIMS/tests/testthat/test_average_peaks.R @@ -14,13 +14,14 @@ testthat::test_that("peaks are correctly averaged", { test_peaklist_sorted <- as.data.frame(cbind(samplenr = samplenrs, test_peaklist_sorted_num)) test_peaklist_sorted[, 2] <- as.numeric(test_peaklist_sorted[, 2]) test_peaklist_sorted[, 6] <- as.numeric(test_peaklist_sorted[, 6]) + test_sample_name <- "P001" # test that first peak is correctly averaged - expect_equal(as.numeric(average_peaks_per_sample(test_peaklist_sorted)[1, 6]), 600, tolerance = 0.001, TRUE) + expect_equal(as.numeric(average_peaks_per_sample(test_peaklist_sorted, test_sample_name)[1, 6]), 600, tolerance = 0.001, TRUE) # test number of rows - expect_equal(nrow(average_peaks_per_sample(test_peaklist_sorted)), 2) + expect_equal(nrow(average_peaks_per_sample(test_peaklist_sorted, test_sample_name)), 2) # test column names - expect_equal(colnames(average_peaks_per_sample(test_peaklist_sorted)), + expect_equal(colnames(average_peaks_per_sample(test_peaklist_sorted, test_sample_name)), c("samplenr", "mzmed.pkt", "fq", "mzmin.pkt", "mzmax.pkt", "height.pkt"), TRUE) }) From ac9f43fc6508e5db29beb9d5d10cb8dbaa337e3a Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 17 Oct 2025 14:50:10 +0200 Subject: [PATCH 079/161] added fixture files for unit test for DIMS/EvaluateTics --- .../fixtures/test_evaluate_tics_file1.RData | Bin 0 -> 307 bytes .../fixtures/test_evaluate_tics_file2.RData | Bin 0 -> 301 bytes .../fixtures/test_evaluate_tics_file3.RData | Bin 0 -> 309 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 DIMS/tests/testthat/fixtures/test_evaluate_tics_file1.RData create mode 100644 DIMS/tests/testthat/fixtures/test_evaluate_tics_file2.RData create mode 100644 DIMS/tests/testthat/fixtures/test_evaluate_tics_file3.RData diff --git a/DIMS/tests/testthat/fixtures/test_evaluate_tics_file1.RData b/DIMS/tests/testthat/fixtures/test_evaluate_tics_file1.RData new file mode 100644 index 0000000000000000000000000000000000000000..6d9f40d89645aeee7de35825602471c5252ff7a0 GIT binary patch literal 307 zcmV-30nGj%iwFP!000000}FDAFy@NjVqjokW?*3flB_@`18ZoAo2~@|0}B(7!^ptG zzzL)|3sMua<8v~LOBfiKguyx(nD~G+mt(Tf1qP}D6pNWtGIN0xFA#%-xxiAWN;rT@ z@)C1Xi-Bxms5$|#TBy94rJjL-fw2)9-vX6yVvNSOMCF^BqVmlQ(fH=5d~-DW%+c(# zFoC)g<`15<%$!t^N8$|(41xTAupo9~S`e>5^&n>Yk3C$O{|~=#WB`YwL!bl8KI1ee zAkDgwt7;pNW?THQUlT~PPusD0E|BKv?B5{>q&aKS&a?n&u6)}EKY%oMlE~K_Ak7o< zhVwp<=5;?T7mSt;Sz(C~|eRIZH! F003yQiBA9k literal 0 HcmV?d00001 diff --git a/DIMS/tests/testthat/fixtures/test_evaluate_tics_file2.RData b/DIMS/tests/testthat/fixtures/test_evaluate_tics_file2.RData new file mode 100644 index 0000000000000000000000000000000000000000..77d650f943d6314e2650f02db73963f0d1413f73 GIT binary patch literal 301 zcmV+|0n+{-iwFP!000000}FDAFy@NjVqjokW?*3flB_@`18ZoAo2~@|0}B(7!^ptG zzzL)|3sMua<8v~LOBfiKguyx(nD~G+mlM;1cm)P(0u+mxQ!;ab6fY2igt@>{s7g41 zO7aqOQ;UIYVW>I*uv)0RnWdhAfq}6R8s7qyZ(@wbw?yTenxgW}4AJ=JsC;uY`^?eo zvoL|W6Xp+|w9K4TkVoPT3=Dz%f3yu^rvKQ(mHGeh3r7ZUI64G6u*>%>mLpA#XVE18H9O z!*aoB>5vtc2tkfvhNa(v{9+h0FEt&?{s7g41 zO7aqOQ;UIYVW>I*uv)0RnWdhAfq}6R8s7qyZ(@wbw?yTenxgW}4AJ=JsC;uY`^?eo zvoL|W6Xp+|w9K4TkVoPT3=Dz%f3P5SOcuI8)er`UAJc#A;mZ7f_=O__I2;`U9a#1m zr#S&>){R_M+kiCN;)ng3K$?Bpj>U6 Date: Tue, 28 Oct 2025 14:12:34 +0100 Subject: [PATCH 080/161] Fix for error dIEM plots --- DIMS/GenerateViolinPlots.R | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 224b1f70..38c0a105 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -176,14 +176,21 @@ for (patient_id in patients) { if (nrow(patient_top_iems_probs) > 0) { top_iems <- patient_top_iems_probs %>% pull(Disease) # Get the metabolites for each IEM and their probability + metabs_iems <- list() metabs_iems_names <- c() - metabs_iems <- lapply(top_iems, function(iem) { + for (iem in top_iems) { iem_probablity <- patient_top_iems_probs %>% filter(Disease == iem) %>% pull(!!sym(patient_id)) metabs_iems_names <- c(metabs_iems_names, paste0(iem, ", probability score ", iem_probablity)) - metab_iem <- expected_biomarkers_df %>% filter(Disease == iem) %>% select(HMDB_code, HMDB_name) - return(metab_iem) - }) - names(metabs_iems) <- metabs_iems_names + metab_iem_df <- expected_biomarkers_df %>% filter(Disease == iem) %>% select(HMDB_code, HMDB_name) + metabs_iems[[iem]] <- metab_iem_df + } + # metabs_iems <- lapply(top_iems, function(iem) { + # iem_probablity <- patient_top_iems_probs %>% filter(Disease == iem) %>% pull(!!sym(patient_id)) + # metabs_iems_names <- c(metabs_iems_names, paste0(iem, ", probability score ", iem_probablity)) + # metab_iem <- expected_biomarkers_df %>% filter(Disease == iem) %>% select(HMDB_code, HMDB_name) + # return(metab_iem) + # }) + # names(metabs_iems) <- metabs_iems_names # Get the Z-scores with metabolite information metab_iem_sorted <- combine_metab_info_zscores(metabs_iems, zscore_patients_df) From c1dcfe0b93e302a6939c95784749c9a401e5a37c Mon Sep 17 00:00:00 2001 From: ellendejong Date: Mon, 24 Nov 2025 13:33:09 +0100 Subject: [PATCH 081/161] fix indentation env (line 13) --- .github/workflows/kinship_test.yml | 6 +++--- .github/workflows/moisaichunter_test.yml | 6 +++--- .github/workflows/utils_test.yml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/kinship_test.yml b/.github/workflows/kinship_test.yml index bb7725dc..67537cd4 100644 --- a/.github/workflows/kinship_test.yml +++ b/.github/workflows/kinship_test.yml @@ -10,8 +10,8 @@ on: jobs: pytest: runs-on: ubuntu-latest - env: - working-directory: Kinship/ + env: + working-directory: Kinship/ defaults: run: working-directory: ${{ env.working-directory }} @@ -25,7 +25,7 @@ jobs: id: setup-python uses: actions/setup-python@v5 with: - python-version: '3.11.5' + python-version: "3.11.5" #---------------------------------------------- # install & configure poetry #---------------------------------------------- diff --git a/.github/workflows/moisaichunter_test.yml b/.github/workflows/moisaichunter_test.yml index c38b2fab..14549208 100644 --- a/.github/workflows/moisaichunter_test.yml +++ b/.github/workflows/moisaichunter_test.yml @@ -10,8 +10,8 @@ on: jobs: pytest: runs-on: ubuntu-latest - env: - working-directory: MosaicHunter/1.0.0/ + env: + working-directory: MosaicHunter/1.0.0/ defaults: run: working-directory: ${{ env.working-directory }} @@ -25,7 +25,7 @@ jobs: id: setup-python uses: actions/setup-python@v5 with: - python-version: '3.11.5' + python-version: "3.11.5" #---------------------------------------------- # install & configure poetry #---------------------------------------------- diff --git a/.github/workflows/utils_test.yml b/.github/workflows/utils_test.yml index 9490573f..be457b81 100644 --- a/.github/workflows/utils_test.yml +++ b/.github/workflows/utils_test.yml @@ -10,8 +10,8 @@ on: jobs: pytest: runs-on: ubuntu-latest - env: - working-directory: Utils/ + env: + working-directory: Utils/ defaults: run: working-directory: ${{ env.working-directory }} @@ -25,7 +25,7 @@ jobs: id: setup-python uses: actions/setup-python@v5 with: - python-version: '3.11.5' + python-version: "3.11.5" #---------------------------------------------- # install & configure poetry #---------------------------------------------- From c7edbb54cf4200e1e74aab94fa2f007df3b9d92e Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 4 Dec 2025 11:10:14 +0100 Subject: [PATCH 082/161] Removed unnecessary params --- DIMS/GenerateQCOutput.nf | 1 - 1 file changed, 1 deletion(-) diff --git a/DIMS/GenerateQCOutput.nf b/DIMS/GenerateQCOutput.nf index eb17301f..73f772e9 100644 --- a/DIMS/GenerateQCOutput.nf +++ b/DIMS/GenerateQCOutput.nf @@ -23,7 +23,6 @@ process GenerateQCOutput { Rscript ${baseDir}/CustomModules/DIMS/GenerateQCOutput.R $init_file \ $analysis_id \ $params.matrix \ - $params.zscore \ $params.sst_components_file \ $params.export_scripts_dir """ From f5210be1ec996595c83184477ea4d80eea1c7ad2 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 4 Dec 2025 11:10:50 +0100 Subject: [PATCH 083/161] Removed unnecessary params, added comments and changed variable names --- DIMS/GenerateQCOutput.R | 57 ++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/DIMS/GenerateQCOutput.R b/DIMS/GenerateQCOutput.R index 1bc3e65c..605c8db0 100644 --- a/DIMS/GenerateQCOutput.R +++ b/DIMS/GenerateQCOutput.R @@ -12,7 +12,6 @@ cmd_args <- commandArgs(trailingOnly = TRUE) init_file <- cmd_args[1] project <- cmd_args[2] dims_matrix <- cmd_args[3] -z_score <- cmd_args[4] sst_components_file <- cmd_args[5] export_scripts_dir <- cmd_args[6] @@ -38,11 +37,9 @@ dir.create(paste0(outdir, "/plots"), showWarnings = FALSE) control_label <- "C" #### CHECK NUMBER OF CONTROLS #### -if (z_score == 1) { - file_name <- "Check_number_of_controls.txt" - min_num_controls <- 25 - check_number_of_controls(outlist, min_num_controls, file_name) -} +file_name <- "Check_number_of_controls.txt" +min_num_controls <- 25 +check_number_of_controls(outlist, min_num_controls, file_name) #### INTERNAL STANDARDS #### is_list <- outlist[grep("Internal standard", outlist[, "relevance"], fixed = TRUE), ] @@ -314,30 +311,38 @@ is_list_intensities <- get_is_intensities(is_list, int_cols = intensity_col_ids) is_neg_intensities <- get_is_intensities(outlist_tot_neg, is_codes = is_codes) is_pos_intensities <- get_is_intensities(outlist_tot_pos, is_codes = is_codes) -# SST components. -sst_comp <- read.csv(sst_components_file, header = TRUE, sep = "\t") -sst_list <- outlist %>% filter(HMDB_code %in% sst_comp$HMDB_ID) -sst_colnrs <- grep("P1001", colnames(sst_list)) - -if (length(sst_colnrs) > 0) { - sst_list_intensities <- sst_list[, sst_colnrs] - control_col_ids <- grep(control_label, colnames(sst_list), fixed = TRUE) - control_list_intensities <- sst_list[, control_col_ids] - control_list_cv <- calc_coefficient_of_variation(control_list_intensities) - sst_list_intensities <- cbind(sst_list_intensities, CV_controls = control_list_cv[, "CV_perc"]) +# SST components +sst_components <- read.csv(sst_components_file, header = TRUE, sep = "\t") +sst_metabolites_df <- outlist %>% filter(HMDB_code %in% sst_components$HMDB_ID) +sst_sample_column_index <- grep("P1001", colnames(sst_metabolites_df)) + +# Check if SST mix sample(s) are present +if (length(sst_sample_column_index) > 0) { + # Get the SST intensities of the controls, calculate the coefficient of variation + # and add to SST mix intensities + sst_sample_intensities_df <- sst_metabolites_df[, sst_sample_column_index] + control_col_ids <- grep(control_label, colnames(sst_metabolites_df), fixed = TRUE) + control_sst_intensities_df <- sst_metabolites_df[, control_col_ids] + control_sst_metabolites_cv <- calc_coefficient_of_variation(control_sst_intensities_df) + sst_intensities_df <- cbind(sst_sample_intensities_df, CV_controls = control_sst_metabolites_cv[, "CV_perc"]) } else { - sst_list_intensities <- sst_list[, intensity_col_ids] + # Use intensities when there is not SST mix sample added + sst_intensities_df <- sst_metabolites_df[, intensity_col_ids] } -sst_list_intensities <- as.data.frame(sst_list_intensities) -for (col_nr in seq_len(ncol(sst_list_intensities))) { - sst_list_intensities[, col_nr] <- as.numeric(sst_list_intensities[, col_nr]) - if (grepl("Zscore", colnames(sst_list_intensities)[col_nr])) { - sst_list_intensities[, col_nr] <- round(sst_list_intensities[, col_nr], 2) + +sst_intensities_df <- as.data.frame(sst_intensities_df) +for (col_nr in seq_len(ncol(sst_intensities_df))) { + # Change column type to numeric + sst_intensities_df[, col_nr] <- as.numeric(sst_intensities_df[, col_nr]) + if (grepl("Zscore", colnames(sst_intensities_df)[col_nr])) { + # Round numeric value of Z-score columns to 2 decimal places + sst_intensities_df[, col_nr] <- round(sst_intensities_df[, col_nr], 2) } else { - sst_list_intensities[, col_nr] <- round(sst_list_intensities[, col_nr]) + # Round numeric value of intensity columns to an intiger + sst_intensities_df[, col_nr] <- round(sst_intensities_df[, col_nr]) } } -sst_list_intensities <- cbind(SST_comp_name = sst_list$HMDB_name, sst_list_intensities) +sst_intensities_df <- cbind(SST_comp_name = sst_metabolites_df$HMDB_name, sst_intensities_df) # Create Excel file wb <- createWorkbook("IS_SST") @@ -351,7 +356,7 @@ addWorksheet(wb, "IS neg") openxlsx::writeData(wb, sheet = 3, is_neg_intensities) setColWidths(wb, 3, cols = 1, widths = 24) addWorksheet(wb, "SST components") -openxlsx::writeData(wb, sheet = 4, sst_list_intensities) +openxlsx::writeData(wb, sheet = 4, sst_intensities_df) setColWidths(wb, 4, cols = 1:3, widths = 24) xlsx_name <- paste0(outdir, "/", project, "_IS_SST.xlsx") openxlsx::saveWorkbook(wb, xlsx_name, overwrite = TRUE) From 204351e0577af543639a87a0c6b7e9fc639532fa Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 4 Dec 2025 11:20:26 +0100 Subject: [PATCH 084/161] Removed old, unused code --- DIMS/GenerateViolinPlots.R | 8 -------- 1 file changed, 8 deletions(-) diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 38c0a105..2f32e3bc 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -184,14 +184,6 @@ for (patient_id in patients) { metab_iem_df <- expected_biomarkers_df %>% filter(Disease == iem) %>% select(HMDB_code, HMDB_name) metabs_iems[[iem]] <- metab_iem_df } - # metabs_iems <- lapply(top_iems, function(iem) { - # iem_probablity <- patient_top_iems_probs %>% filter(Disease == iem) %>% pull(!!sym(patient_id)) - # metabs_iems_names <- c(metabs_iems_names, paste0(iem, ", probability score ", iem_probablity)) - # metab_iem <- expected_biomarkers_df %>% filter(Disease == iem) %>% select(HMDB_code, HMDB_name) - # return(metab_iem) - # }) - # names(metabs_iems) <- metabs_iems_names - # Get the Z-scores with metabolite information metab_iem_sorted <- combine_metab_info_zscores(metabs_iems, zscore_patients_df) metab_iem_controls <- combine_metab_info_zscores(metabs_iems, zscore_controls_df) From 4b395e1e57791cca9d70933b07a4e03a71369ed7 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 9 Dec 2025 12:40:22 +0100 Subject: [PATCH 085/161] old FillMissing functions replaced by new ones in preprocessing folder --- DIMS/FillMissing.R | 25 ++++++----------- DIMS/FillMissing.nf | 2 +- DIMS/preprocessing/fill_missing_functions.R | 31 +++++++++++++++++++++ 3 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 DIMS/preprocessing/fill_missing_functions.R diff --git a/DIMS/FillMissing.R b/DIMS/FillMissing.R index a76c163b..32480d38 100755 --- a/DIMS/FillMissing.R +++ b/DIMS/FillMissing.R @@ -1,25 +1,14 @@ -## adapted from 9-runFillMissing.R - # define parameters cmd_args <- commandArgs(trailingOnly = TRUE) peakgrouplist_file <- cmd_args[1] -scripts_dir <- cmd_args[2] +preprocessing_scripts_dir <- cmd_args[2] thresh <- as.numeric(cmd_args[3]) resol <- as.numeric(cmd_args[4]) ppm <- as.numeric(cmd_args[5]) -outdir <- "./" # load in function scripts -source(paste0(scripts_dir, "replace_zeros.R")) -source(paste0(scripts_dir, "fit_optim.R")) -source(paste0(scripts_dir, "get_fwhm.R")) -source(paste0(scripts_dir, "get_stdev.R")) -source(paste0(scripts_dir, "estimate_area.R")) -source(paste0(scripts_dir, "optimize_gaussfit.R")) -source(paste0(scripts_dir, "identify_noisepeaks.R")) -source(paste0(scripts_dir, "get_element_info.R")) -source(paste0(scripts_dir, "atomic_info.R")) +source(paste0(preprocessing_scripts_dir, "fill_missing_functions.R")) # determine scan mode if (grepl("_pos", peakgrouplist_file)) { @@ -33,12 +22,14 @@ pattern_file <- paste0(scanmode, "_repl_pattern.RData") repl_pattern <- get(load(pattern_file)) # load peak group list and determine output file name -outpgrlist_identified <- get(load(peakgrouplist_file)) - -outputfile_name <- gsub(".RData", "_filled.RData", peakgrouplist_file) +peakgroup_list <- get(load(peakgrouplist_file)) # replace missing values (zeros) with random noise -peakgrouplist_filled <- replace_zeros(outpgrlist_identified, repl_pattern, scanmode, resol, outdir, thresh, ppm) +peakgrouplist_filled <- fill_missing_intensities(peakgroup_list, repl_pattern, thresh) + +# set name of output file +outputfile_name <- gsub(".RData", "_filled.RData", peakgrouplist_file) # save output save(peakgrouplist_filled, file = outputfile_name) + diff --git a/DIMS/FillMissing.nf b/DIMS/FillMissing.nf index b0d18505..50227026 100644 --- a/DIMS/FillMissing.nf +++ b/DIMS/FillMissing.nf @@ -13,6 +13,6 @@ process FillMissing { script: """ - Rscript ${baseDir}/CustomModules/DIMS/FillMissing.R $peakgrouplist_file $params.scripts_dir $params.thresh $params.resolution $params.ppm + Rscript ${baseDir}/CustomModules/DIMS/FillMissing.R $peakgrouplist_file $params.preprocessing_scripts_dir $params.thresh """ } diff --git a/DIMS/preprocessing/fill_missing_functions.R b/DIMS/preprocessing/fill_missing_functions.R new file mode 100644 index 00000000..86a70d9a --- /dev/null +++ b/DIMS/preprocessing/fill_missing_functions.R @@ -0,0 +1,31 @@ +fill_missing_intensities <- function(peakgroup_list, repl_pattern, thresh) { + #' Replace intensities that are zero with random value + #' + #' @param peakgroup_list: Peak groups (matrix) + #' @param repl_pattern: Replication pattern (list of strings) + #' @param thresh: Value for threshold between noise and signal (integer) + #' + #' @return final_outlist: peak groups with filled-in intensities (matrix) + + # replace missing intensities with random values around threshold + if (!is.null(peakgroup_list)) { + for (sample_index in 1:length(names(repl_pattern))) { + sample_peaks <- peakgroup_list[, names(repl_pattern)[sample_index]] + zero_intensity <- which(sample_peaks <= 0) + if (!length(zero_intensity)) { + next + } + for (zero_index in 1:length(zero_intensity)) { + peakgroup_list[zero_intensity[zero_index], names(repl_pattern)[sample_index]] <- rnorm(n = 1, + mean = thresh, + sd = 100) + } + } + + # Add column with average intensity; find intensity columns first + int_cols <- which(colnames(peakgroup_list) %in% names(repl_pattern)) + peakgroup_list <- cbind(peakgroup_list, "avg.int" = apply(peakgroup_list[, int_cols], 1, mean)) + + return(peakgroup_list) + } +} From 1f77b2139096190dead1b3a9e0561715ce0c7a70 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 9 Dec 2025 12:53:43 +0100 Subject: [PATCH 086/161] removed identification of noise peaks --- DIMS/CollectFilled.R | 10 +++++----- DIMS/Utils/calculate_zscores.R | 2 +- DIMS/Utils/merge_duplicate_rows.R | 4 ---- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/DIMS/CollectFilled.R b/DIMS/CollectFilled.R index 4bd408e0..5735bc56 100755 --- a/DIMS/CollectFilled.R +++ b/DIMS/CollectFilled.R @@ -50,11 +50,11 @@ for (scanmode in scanmodes) { # make a copy of the outlist outlist_ident <- outlist_total - # take care of NAs in theormz_noise - outlist_ident$theormz_noise[which(is.na(outlist_ident$theormz_noise))] <- 0 - outlist_ident$theormz_noise <- as.numeric(outlist_ident$theormz_noise) - outlist_ident$theormz_noise[which(is.na(outlist_ident$theormz_noise))] <- 0 - outlist_ident$theormz_noise <- as.numeric(outlist_ident$theormz_noise) + # select identified peak groups if ppm deviation is within limits + if (z_score == 1) { + outlist_ident$ppmdev <- as.numeric(outlist_ident$ppmdev) + outlist_ident <- outlist_ident[which(outlist_ident["ppmdev"] >= -ppm & outlist_ident["ppmdev"] <= ppm), ] + } # Extra output in Excel-readable format: remove_columns <- c("fq.best", "fq.worst", "mzmin.pgrp", "mzmax.pgrp") diff --git a/DIMS/Utils/calculate_zscores.R b/DIMS/Utils/calculate_zscores.R index d687376c..12365e41 100644 --- a/DIMS/Utils/calculate_zscores.R +++ b/DIMS/Utils/calculate_zscores.R @@ -55,7 +55,7 @@ calculate_zscores <- function(peakgroup_list, adducts) { ppmdev[i] <- NA } } - peakgroup_list <- cbind(peakgroup_list[, 1:6], ppmdev = ppmdev, peakgroup_list[, 7:ncol(peakgroup_list)]) + peakgroup_list <- cbind(peakgroup_list[, 1:4], ppmdev = ppmdev, peakgroup_list[, 5:ncol(peakgroup_list)]) } } diff --git a/DIMS/Utils/merge_duplicate_rows.R b/DIMS/Utils/merge_duplicate_rows.R index 901f1160..25afd312 100644 --- a/DIMS/Utils/merge_duplicate_rows.R +++ b/DIMS/Utils/merge_duplicate_rows.R @@ -38,10 +38,6 @@ merge_duplicate_rows <- function(peakgroup_list) { single_peakgroup[, "assi_HMDB"] <- collapse("assi_HMDB", peakgroup_list, peaklist_index) single_peakgroup[, "iso_HMDB"] <- collapse("iso_HMDB", peakgroup_list, peaklist_index) single_peakgroup[, "HMDB_code"] <- collapse("HMDB_code", peakgroup_list, peaklist_index) - single_peakgroup[, "assi_noise"] <- collapse("assi_noise", peakgroup_list, peaklist_index) - if (single_peakgroup[, "assi_noise"] == ";") single_peakgroup[, "assi_noise"] <- NA - single_peakgroup[, "theormz_noise"] <- collapse("theormz_noise", peakgroup_list, peaklist_index) - if (single_peakgroup[, "theormz_noise"] == "0;0") single_peakgroup[, "theormz_noise"] <- NA single_peakgroup[, "all_hmdb_ids"] <- collapse("all_hmdb_ids", peakgroup_list, peaklist_index) single_peakgroup[, "sec_hmdb_ids"] <- collapse("sec_hmdb_ids", peakgroup_list, peaklist_index) if (single_peakgroup[, "sec_hmdb_ids"] == ";") single_peakgroup[, "sec_hmdb_ids"] < NA From d0aff6cf2791fa2b9b1d361d321c6800735ebbbe Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 9 Dec 2025 12:55:18 +0100 Subject: [PATCH 087/161] added as.data.frame to avoid error of non-numeric argument --- DIMS/GenerateQCOutput.R | 1 + 1 file changed, 1 insertion(+) diff --git a/DIMS/GenerateQCOutput.R b/DIMS/GenerateQCOutput.R index 4e3394d0..bdc14f86 100644 --- a/DIMS/GenerateQCOutput.R +++ b/DIMS/GenerateQCOutput.R @@ -329,6 +329,7 @@ if (length(sst_colnrs) > 0) { sst_list_intensities <- sst_list[, intensity_col_ids] } for (col_nr in seq_len(ncol(sst_list_intensities))) { + sst_list_intensities <- as.data.frame(sst_list_intensities) sst_list_intensities[, col_nr] <- as.numeric(sst_list_intensities[, col_nr]) if (grepl("Zscore", colnames(sst_list_intensities)[col_nr])) { sst_list_intensities[, col_nr] <- round(sst_list_intensities[, col_nr], 2) From 519c5b71b0a46503851bb30707e05ad0fb213263 Mon Sep 17 00:00:00 2001 From: mraves2 Date: Tue, 9 Dec 2025 14:57:39 +0100 Subject: [PATCH 088/161] linting modifications --- DIMS/FillMissing.R | 1 - DIMS/preprocessing/fill_missing_functions.R | 13 +++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/DIMS/FillMissing.R b/DIMS/FillMissing.R index 32480d38..0e8a26c7 100755 --- a/DIMS/FillMissing.R +++ b/DIMS/FillMissing.R @@ -32,4 +32,3 @@ outputfile_name <- gsub(".RData", "_filled.RData", peakgrouplist_file) # save output save(peakgrouplist_filled, file = outputfile_name) - diff --git a/DIMS/preprocessing/fill_missing_functions.R b/DIMS/preprocessing/fill_missing_functions.R index 86a70d9a..e1c455d5 100644 --- a/DIMS/preprocessing/fill_missing_functions.R +++ b/DIMS/preprocessing/fill_missing_functions.R @@ -1,4 +1,4 @@ -fill_missing_intensities <- function(peakgroup_list, repl_pattern, thresh) { +fill_missing_intensities <- function(peakgroup_list, repl_pattern, thresh, not_random = FALSE) { #' Replace intensities that are zero with random value #' #' @param peakgroup_list: Peak groups (matrix) @@ -7,16 +7,21 @@ fill_missing_intensities <- function(peakgroup_list, repl_pattern, thresh) { #' #' @return final_outlist: peak groups with filled-in intensities (matrix) + # for unit test, turn off randomness + if (not_random) { + set.seed(123) + } + # replace missing intensities with random values around threshold if (!is.null(peakgroup_list)) { - for (sample_index in 1:length(names(repl_pattern))) { + for (sample_index in seq_along(names(repl_pattern))) { sample_peaks <- peakgroup_list[, names(repl_pattern)[sample_index]] zero_intensity <- which(sample_peaks <= 0) if (!length(zero_intensity)) { next } - for (zero_index in 1:length(zero_intensity)) { - peakgroup_list[zero_intensity[zero_index], names(repl_pattern)[sample_index]] <- rnorm(n = 1, + for (zero_index in seq_along(zero_intensity)) { + peakgroup_list[zero_intensity[zero_index], names(repl_pattern)[sample_index]] <- rnorm(n = 1, mean = thresh, sd = 100) } From 4bbccd867085bea356a9199de373cf72816025a9 Mon Sep 17 00:00:00 2001 From: mraves2 Date: Tue, 9 Dec 2025 14:58:26 +0100 Subject: [PATCH 089/161] added unit test for FillMissing --- DIMS/tests/testthat/test_fill_missing.R | 45 +++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 DIMS/tests/testthat/test_fill_missing.R diff --git a/DIMS/tests/testthat/test_fill_missing.R b/DIMS/tests/testthat/test_fill_missing.R new file mode 100644 index 00000000..1926b632 --- /dev/null +++ b/DIMS/tests/testthat/test_fill_missing.R @@ -0,0 +1,45 @@ +# unit tests for FillMissing +# function: fill_missing_intensities +source("../../preprocessing/fill_missing_functions.R") + +# test fill_missing_intensities +testthat::test_that("missing values are corretly filled with random values", { + # create peakgroup_list to test on in diagnostics setting + test_peakgroup_list <- data.frame(matrix(NA, nrow = 4, ncol = 23)) + colnames(test_peakgroup_list) <- c("mzmed.pgrp", "nrsamples", "ppmdev", "assi_HMDB", "all_hmdb_names", + "iso_HMDB", "HMDB_code", "all_hmdb_ids", "sec_hmdb_ids", "theormz_HMDB", + "C101.1", "C102.1", "P2.1", "P3.1", + "avg.int", "assi_noise", "theormz_noise", "avg.ctrls", "sd.ctrls", + "C101.1_Zscore", "C102.1_Zscore", "P2.1_Zscore", "P3.1_Zscore") + test_peakgroup_list[, c(1)] <- 300 + runif(4) + test_peakgroup_list[, c(2, 3)] <- runif(8) + test_peakgroup_list[, "HMDB_code"] <- c("HMDB1234567", "HMDB1234567_1", "HMDB1234567_2", "HMDB1234567_7") + test_peakgroup_list[, "all_hmdb_ids"] <- paste(test_peakgroup_list[, "HMDB_code"], + test_peakgroup_list[, "HMDB_code"], sep = ";") + test_peakgroup_list[, "all_hmdb_names"] <- paste(test_peakgroup_list[, "assi_HMDB"], + test_peakgroup_list[, "assi_HMDB"], sep = ";") + test_peakgroup_list[, grep("C", colnames(test_peakgroup_list))] <- 1000 * (1:16) + test_peakgroup_list[, grep("P", colnames(test_peakgroup_list))] <- 0 + test_repl_pattern <- c(list(1), list(2), list(3), list(4)) + names(test_repl_pattern) <- c("C101.1", "C102.1", "P2.1", "P3.1") + test_thresh <- 2000 + + # create a large peak group list to test for negative values + test_large_peakgroup_list <- rbind(test_peakgroup_list, test_peakgroup_list) + for (index in 1:15) { + test_large_peakgroup_list <- rbind(test_large_peakgroup_list, test_large_peakgroup_list) + } + # for the sake of time, leave only one intensity column with zeros + test_large_peakgroup_list$P2.1 <- 1 + + expect_equal(round(fill_missing_intensities(test_peakgroup_list, test_repl_pattern, test_thresh, not_random = TRUE)$P2.1), + c(1944, 1977, 2156, 2007), TRUE, tolerance = 0.1) + # fill_missing_intensities should not produce any negative values, even if a large quantity of numbers are filled in + start.time <- Sys.time() + expect_gt(min(fill_missing_intensities(test_large_peakgroup_list, test_repl_pattern, test_thresh, not_random = FALSE)$P3.1), + 0, TRUE) + end.time <- Sys.time() + time.taken <- end.time - start.time + time.taken + +}) From 17b6d50f2d1c0929eaaea6badd310cdf5a53a0c7 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Mon, 15 Dec 2025 13:44:34 +0100 Subject: [PATCH 090/161] added output for QC on internal standards to include in mail --- DIMS/GenerateQCOutput.R | 90 +++++++++++++++++++++++++++++++++------- DIMS/GenerateQCOutput.nf | 1 + 2 files changed, 75 insertions(+), 16 deletions(-) diff --git a/DIMS/GenerateQCOutput.R b/DIMS/GenerateQCOutput.R index af63962f..aa1cdae9 100644 --- a/DIMS/GenerateQCOutput.R +++ b/DIMS/GenerateQCOutput.R @@ -136,40 +136,52 @@ is_sum_selection <- c( "2H3-Glutamate (IS)", "2H4_13C5-Arginine (IS)", "13C6-Tyrosine (IS)" ) +all_is_names <- list(neg = is_neg_selection, pos = is_pos_selection, sum = is_sum_selection) + +# define threshold for acceptance of selected internal standards +threshold_is_dbs_neg <- c(15000, 200000, 130000, 18000, 50000) +threshold_is_dbs_pos <- c(150000, 3300000, 1750000, 150000, 270000) +threshold_is_dbs_sum <- c(1300000, 2500000, 500000, 1800000, 1400000) +threshold_is_pl_neg <- c(70000, 700000, 700000, 65000, 350000) +threshold_is_pl_pos <- c(1500000, 9000000, 3000000, 400000, 700000) +threshold_is_pl_sum <- c(8000000, 12500000, 2500000, 3000000, 4000000) +all_is_thresholds_dbs <- list(neg = threshold_is_dbs_neg, pos = threshold_is_dbs_pos, sum = threshold_is_dbs_sum) +all_is_thresholds_pl <- list(neg = threshold_is_pl_neg, pos = threshold_is_pl_pos, sum = threshold_is_pl_sum) +all_is_thresholds <- list(names = all_is_names, plasma = all_is_thresholds_pl, dbs = all_is_thresholds_dbs) # add minimal intensity lines based on matrix (DBS or Plasma) and machine mode (neg, pos, sum) if (dims_matrix == "DBS") { add_min_intens_lines <- TRUE hline_data_neg <- data.frame( - int_line = c(15000, 200000, 130000, 18000, 50000), + int_line = threshold_is_dbs_neg, HMDB_name = is_neg_selection ) hline_data_pos <- data.frame( - int_line = c(150000, 3300000, 1750000, 150000, 270000), + int_line = threshold_is_dbs_pos, HMDB_name = is_pos_selection ) hline_data_sum <- data.frame( - int_line = c(1300000, 2500000, 500000, 1800000, 1400000), + int_line = threshold_is_dbs_sum, HMDB_name = is_sum_selection ) } else if (dims_matrix == "Plasma") { add_min_intens_lines <- TRUE hline_data_neg <- data.frame( - int_line = c(70000, 700000, 700000, 65000, 350000), + int_line = threshold_is_pl_neg, HMDB_name = is_neg_selection ) hline_data_pos <- data.frame( - int_line = c(1500000, 9000000, 3000000, 400000, 700000), + int_line = threshold_is_pl_pos, HMDB_name = is_pos_selection ) hline_data_sum <- data.frame( - int_line = c(8000000, 12500000, 2500000, 3000000, 4000000), + int_line = threshold_is_pl_sum, HMDB_name = is_sum_selection ) } else { @@ -180,9 +192,60 @@ if (dims_matrix == "DBS") { plot_width <- 8 + 0.2 * sample_count plot_height <- plot_width / 2.5 -is_neg_selection <- subset(is_neg, HMDB_name %in% is_neg_selection) -is_pos_selection <- subset(is_pos, HMDB_name %in% is_pos_selection) -is_sum_selection <- subset(is_summed, HMDB_name %in% is_sum_selection) +is_neg_selection_subset <- subset(is_neg, HMDB_name %in% is_neg_selection) +is_pos_selection_subset <- subset(is_pos, HMDB_name %in% is_pos_selection) +is_sum_selection_subset <- subset(is_summed, HMDB_name %in% is_sum_selection) + +# export txt file with samples with internal standard level below threshold +is_below_threshold <- is_pos_selection_subset[0, ] +scanmode_is <- c() +if (dims_matrix == "Plasma") { + # pos + for (line_index in seq_len(nrow(is_pos_selection_subset))) { + is_selected <- is_pos_selection_subset$HMDB_name[line_index] + thresh_selected <- all_is_thresholds$plasma$pos[which(all_is_thresholds$names$pos == is_selected)] + if (is_pos_selection_subset$Intensity[line_index] < thresh_selected) { + is_below_threshold <- rbind(is_below_threshold, is_pos_selection_subset[line_index, ]) + scanmode_is <- c(scanmode_is, "pos") + } + } + # neg + for (line_index in seq_len(nrow(is_neg_selection_subset))) { + is_selected <- is_neg_selection_subset$HMDB_name[line_index] + thresh_selected <- all_is_thresholds$plasma$neg[which(all_is_thresholds$names$neg == is_selected)] + if (is_neg_selection_subset$Intensity[line_index] < thresh_selected) { + is_below_threshold <- rbind(is_below_threshold, is_neg_selection_subset[line_index, ]) + scanmode_is <- c(scanmode_is, "neg") + } + } +} else if (dims_matrix == "DBS") { + # pos + for (line_index in seq_len(nrow(is_pos_selection_subset))) { + is_selected <- is_pos_selection_subset$HMDB_name[line_index] + thresh_selected <- all_is_thresholds$dbs$pos[which(all_is_thresholds$names$pos == is_selected)] + if (is_pos_selection_subset$Intensity[line_index] < thresh_selected) { + is_below_threshold <- rbind(is_below_threshold, is_pos_selection_subset[line_index, ]) + scanmode_is <- c(scanmode_is, "pos") + } + } + # neg + for (line_index in seq_len(nrow(is_neg_selection_subset))) { + is_selected <- is_neg_selection_subset$HMDB_name[line_index] + thresh_selected <- all_is_thresholds$dbs$neg[which(all_is_thresholds$names$neg == is_selected)] + if (is_neg_selection_subset$Intensity[line_index] < thresh_selected) { + is_below_threshold <- rbind(is_below_threshold, is_neg_selection_subset[line_index, ]) + scanmode_is <- c(scanmode_is, "neg") + } + } +} +if (nrow(is_below_threshold) > 0) { + write.table(cbind(is_below_threshold, scanmode = scanmode_is), file = "internal_standards_below_threshold.txt", sep = "\t") +} else { + write.table("no internal standards are below threshold", + file = "internal_standards_below_threshold.txt" + row.names = FALSE, col.names = FALSE + ) +} # bar plot either with or without minimal intensity lines if (add_min_intens_lines) { @@ -325,10 +388,12 @@ if (length(sst_colnrs) > 0) { control_list_intensities <- sst_list[, control_col_ids] control_list_cv <- calc_coefficient_of_variation(control_list_intensities) sst_list_intensities <- cbind(sst_list_intensities, CV_controls = control_list_cv[, "CV_perc"]) + sst_list_intensities <- as.data.frame(sst_list_intensities) } else { sst_list_intensities <- sst_list[, intensity_col_ids] } for (col_nr in seq_len(ncol(sst_list_intensities))) { + sst_list_intensities <- as.data.frame(sst_list_intensities) sst_list_intensities[, col_nr] <- as.numeric(sst_list_intensities[, col_nr]) if (grepl("Zscore", colnames(sst_list_intensities)[col_nr])) { sst_list_intensities[, col_nr] <- round(sst_list_intensities[, col_nr], 2) @@ -356,13 +421,6 @@ xlsx_name <- paste0(outdir, "/", project, "_IS_SST.xlsx") openxlsx::saveWorkbook(wb, xlsx_name, overwrite = TRUE) rm(wb) -# generate text file for workflow completed mail for components with Z-score < 2 -if (grepl("Zscore", colnames(sst_list_intensities))) { - zscore_column <- grep("_Zscore", colnames(sst_list_intensities) - sst_list_intensities_qc <- sst_list_intensities[sst_list_intensities[, zscore_column] < 2, ] - write.table(sst_list_intensities_qc, file = paste(outdir, "sst_qc.txt", sep = "/")) -} - ### MISSING M/Z CHECK # check the outlist_identified_(negative/positive).RData files for missing m/z values and save to file diff --git a/DIMS/GenerateQCOutput.nf b/DIMS/GenerateQCOutput.nf index bee440a5..be2eac37 100644 --- a/DIMS/GenerateQCOutput.nf +++ b/DIMS/GenerateQCOutput.nf @@ -18,6 +18,7 @@ process GenerateQCOutput { path('plots/IS_*.png'), emit: plot_files path('Check_number_of_controls.txt'), optional: true path('sst_qc.txt'), optional: true + path('internal_standards_below_threshold.txt'), optional: true script: """ From 195ed54340edf76616990dc8c91469145c5c6a81 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Mon, 15 Dec 2025 13:54:38 +0100 Subject: [PATCH 091/161] Removed SST mix and pos controls from plots --- DIMS/GenerateExcel.R | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/DIMS/GenerateExcel.R b/DIMS/GenerateExcel.R index 53662649..7dfcf9ec 100644 --- a/DIMS/GenerateExcel.R +++ b/DIMS/GenerateExcel.R @@ -115,10 +115,6 @@ if (z_score == 1) { # save outlist for GenerateQC step save(outlist, file = "outlist.RData") - # get the IDs of the patients and sort - patient_ids <- unique(gsub("\\.[0-9]*", "", patient_columns)) - patient_ids <- patient_ids[order(nchar(patient_ids), patient_ids)] - # get Helix IDs for extra Excel file metabolite_files <- list.files( path = paste(path_metabolite_groups, "Diagnostics", sep = "/"), @@ -151,16 +147,21 @@ if (z_score == 1) { relocate(c(HMDB_name, HMDB_name_all, HMDB_ID_all, sec_HMDB_ID), .after = last_col()) %>% rename(Name = H_Name) - for (row_index in seq_len(nrow(outlist))) { + # Get intensity columns for controls and patients + # Remove SST mix (P1001.x) and positive controls (P1002.x, P1002.x, P1005.x) + intensities_plots_df <- outlist %>% + select(HMDB_key, matches("^C|^P[0-9]"), -ends_with("_Zscore"), -matches("^P\\d{4}\\.\\d+$")) + + for (row_index in seq_len(nrow(intensities_plots_df))) { # get HMDB ID - hmdb_name <- rownames(outlist[row_index, ]) - - # get intensities of controls and patient for a metabolite, get intensity columns, + hmdb_id <- intensities_plots_df %>% slice(row_index) %>% pull(HMDB_key) + + # get intensities of controls and patient for the selected metabolite, # pivot to long format, arrange Samples nummerically, change Sample names, get group size and # set Intensities to numeric. - intensities <- outlist %>% + intensities_plots_df_long <- intensities_plots_df %>% slice(row_index) %>% - select(all_of(intensity_col_ids)) %>% + select(-HMDB_key) %>% as.data.frame() %>% pivot_longer(everything(), names_to = "Samples", values_to = "Intensities") %>% arrange(nchar(Samples)) %>% @@ -175,17 +176,17 @@ if (z_score == 1) { ungroup() # set plot width to 40 times the number of samples - plot_width <- length(unique(intensities$Samples)) * 40 + plot_width <- length(unique(intensities_plots_df_long$Samples)) * 40 col_width <- plot_width * 2 plot.new() - tmp_png <- paste0("plots/plot_", hmdb_name, ".png") + tmp_png <- paste0("plots/plot_", hmdb_id, ".png") png(filename = tmp_png, width = plot_width, height = 300) # plot intensities for the controls and patients, use boxplot if group size is above 2, otherwise use a dash/line - p <- ggplot(intensities, aes(Samples, Intensities)) + - geom_boxplot(data = subset(intensities, group_size > 2), aes(fill = type)) + - geom_point(data = subset(intensities, group_size <= 2), shape = "-", size = 10, aes(colour = type, fill = type)) + + p <- ggplot(intensities_plots_df_long, aes(Samples, Intensities)) + + geom_boxplot(data = subset(intensities_plots_df_long, group_size > 2), aes(fill = type)) + + geom_point(data = subset(intensities_plots_df_long, group_size <= 2), shape = "-", size = 10, aes(colour = type, fill = type)) + scale_fill_manual(values = c("Control" = "green", "Patients" = "#b20000")) + scale_color_manual(values = c("Control" = "black", "Patients" = "#b20000")) + theme( @@ -193,7 +194,7 @@ if (z_score == 1) { plot.title = element_text(hjust = 0.5, size = 18, face = "bold"), axis.text = element_text(size = 12, face = "bold"), panel.background = element_rect(fill = "white", colour = "black") ) + - ggtitle(hmdb_name) + ggtitle(hmdb_id) print(p) dev.off() @@ -210,8 +211,7 @@ if (z_score == 1) { units = "px" ) - if (hmdb_name %in% metab_list_helix) { - print(row_helix) + if (hmdb_id %in% metab_list_helix) { openxlsx::insertImage( wb_helix_intensities, sheetname, From aae998e0b48a312ccbc4b563e4fea6f0295e7689 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Mon, 15 Dec 2025 13:55:51 +0100 Subject: [PATCH 092/161] Styler and lintr changes --- DIMS/GenerateExcel.R | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/DIMS/GenerateExcel.R b/DIMS/GenerateExcel.R index 7dfcf9ec..c075ba34 100644 --- a/DIMS/GenerateExcel.R +++ b/DIMS/GenerateExcel.R @@ -151,11 +151,13 @@ if (z_score == 1) { # Remove SST mix (P1001.x) and positive controls (P1002.x, P1002.x, P1005.x) intensities_plots_df <- outlist %>% select(HMDB_key, matches("^C|^P[0-9]"), -ends_with("_Zscore"), -matches("^P\\d{4}\\.\\d+$")) - + for (row_index in seq_len(nrow(intensities_plots_df))) { # get HMDB ID - hmdb_id <- intensities_plots_df %>% slice(row_index) %>% pull(HMDB_key) - + hmdb_id <- intensities_plots_df %>% + slice(row_index) %>% + pull(HMDB_key) + # get intensities of controls and patient for the selected metabolite, # pivot to long format, arrange Samples nummerically, change Sample names, get group size and # set Intensities to numeric. @@ -186,7 +188,8 @@ if (z_score == 1) { # plot intensities for the controls and patients, use boxplot if group size is above 2, otherwise use a dash/line p <- ggplot(intensities_plots_df_long, aes(Samples, Intensities)) + geom_boxplot(data = subset(intensities_plots_df_long, group_size > 2), aes(fill = type)) + - geom_point(data = subset(intensities_plots_df_long, group_size <= 2), shape = "-", size = 10, aes(colour = type, fill = type)) + + geom_point(data = subset(intensities_plots_df_long, group_size <= 2), + shape = "-", size = 10, aes(colour = type, fill = type)) + scale_fill_manual(values = c("Control" = "green", "Patients" = "#b20000")) + scale_color_manual(values = c("Control" = "black", "Patients" = "#b20000")) + theme( From cab9ce7a249be65f35f97b74cf6ff39bce3c4232 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Mon, 15 Dec 2025 14:09:34 +0100 Subject: [PATCH 093/161] Added pipe_consistency_linter to default --- .lintr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.lintr b/.lintr index a952bb1b..db672220 100644 --- a/.lintr +++ b/.lintr @@ -1,6 +1,7 @@ linters: linters_with_defaults( line_length_linter(127), object_usage_linter = NULL, - return_linter = NULL + return_linter = NULL, + pipe_consistency_linter = lintr::pipe_consistency_linter("auto") ) encoding: "UTF-8" From 9588296a68ade81f9ac9dfbcfcec92aeccce43ba Mon Sep 17 00:00:00 2001 From: ALuesink Date: Mon, 15 Dec 2025 14:09:49 +0100 Subject: [PATCH 094/161] Lintr changes --- DIMS/export/generate_excel_functions.R | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/DIMS/export/generate_excel_functions.R b/DIMS/export/generate_excel_functions.R index 5183758c..bc1b0ed3 100644 --- a/DIMS/export/generate_excel_functions.R +++ b/DIMS/export/generate_excel_functions.R @@ -46,7 +46,8 @@ calculate_zscores <- function(outlist, zscore_type, control_cols, stat_filter, i control_cols, stat_filter)) } else { # Calculate mean, sd and number of remaining controls, remove outlier controls by using grubbs test - intensities_without_outliers <- remove_outliers_grubbs(as.numeric(outlist[metabolite_index, control_cols]), stat_filter) + intensities_without_outliers <- remove_outliers_grubbs(as.numeric(outlist[metabolite_index, control_cols]), + stat_filter) outlist$avg_ctrls[metabolite_index] <- mean(intensities_without_outliers) outlist$sd_ctrls[metabolite_index] <- sd(intensities_without_outliers) outlist$nr_ctrls[metabolite_index] <- length(intensities_without_outliers) @@ -61,7 +62,7 @@ calculate_zscores <- function(outlist, zscore_type, control_cols, stat_filter, i }) outlist <- cbind(outlist, outlist_zscores) colnames(outlist)[startcol:ncol(outlist)] <- paste0(colnames(outlist)[intensity_col_ids], zscore_type) - + return(outlist) } From aa56eb2427998d287a3f1af816bc6db2c1df3bd6 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Tue, 16 Dec 2025 09:47:53 +0100 Subject: [PATCH 095/161] Create separate plot for Helix Exel, moved code to functions and did styler and linter --- DIMS/GenerateExcel.R | 83 ++++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 50 deletions(-) diff --git a/DIMS/GenerateExcel.R b/DIMS/GenerateExcel.R index c075ba34..48dd7248 100644 --- a/DIMS/GenerateExcel.R +++ b/DIMS/GenerateExcel.R @@ -148,9 +148,7 @@ if (z_score == 1) { rename(Name = H_Name) # Get intensity columns for controls and patients - # Remove SST mix (P1001.x) and positive controls (P1002.x, P1002.x, P1005.x) - intensities_plots_df <- outlist %>% - select(HMDB_key, matches("^C|^P[0-9]"), -ends_with("_Zscore"), -matches("^P\\d{4}\\.\\d+$")) + intensities_plots_df <- outlist %>% select(HMDB_key, matches("^C|^P[0-9]"), -ends_with("_Zscore")) for (row_index in seq_len(nrow(intensities_plots_df))) { # get HMDB ID @@ -158,48 +156,47 @@ if (z_score == 1) { slice(row_index) %>% pull(HMDB_key) - # get intensities of controls and patient for the selected metabolite, - # pivot to long format, arrange Samples nummerically, change Sample names, get group size and - # set Intensities to numeric. - intensities_plots_df_long <- intensities_plots_df %>% - slice(row_index) %>% - select(-HMDB_key) %>% - as.data.frame() %>% - pivot_longer(everything(), names_to = "Samples", values_to = "Intensities") %>% - arrange(nchar(Samples)) %>% - mutate( - Samples = gsub("\\..*", "", Samples), - Samples = gsub("(C).*", "\\1", Samples), - Intensities = as.numeric(Intensities), - type = ifelse(Samples == "C", "Control", "Patients") - ) %>% - group_by(Samples) %>% - mutate(group_size = n()) %>% - ungroup() + # Transform dataframe to long format + intensities_plots_df_long <- transform_ints_df_plots(intensities_plots_df, row_index) # set plot width to 40 times the number of samples plot_width <- length(unique(intensities_plots_df_long$Samples)) * 40 col_width <- plot_width * 2 + if (hmdb_id %in% metab_list_helix) { + # Make separate plot for Helix Excel containing all samples + plot.new() + tmp_png_helix <- paste0("plots/plot_helix_", hmdb_id, ".png") + png(filename = tmp_png_helix, width = plot_width, height = 300) + + boxplot_excel_helix <- create_boxplot_excel(intensities_plots_df_long, hmdb_id) + + print(boxplot_excel_helix) + dev.off() + + openxlsx::insertImage( + wb_helix_intensities, + sheetname, + tmp_png_helix, + startRow = row_helix, + startCol = 1, + height = 560, + width = col_width, + units = "px" + ) + row_helix <- row_helix + 1 + } + + # Remove postive controls and SST mix samples, (e.g. P1001, P1002, P1003, P1005) + intensities_plots_df_long <- intensities_plots_df_long %>% filter(!grepl("^P[0-9]{4}$", Samples)) + plot.new() tmp_png <- paste0("plots/plot_", hmdb_id, ".png") png(filename = tmp_png, width = plot_width, height = 300) - # plot intensities for the controls and patients, use boxplot if group size is above 2, otherwise use a dash/line - p <- ggplot(intensities_plots_df_long, aes(Samples, Intensities)) + - geom_boxplot(data = subset(intensities_plots_df_long, group_size > 2), aes(fill = type)) + - geom_point(data = subset(intensities_plots_df_long, group_size <= 2), - shape = "-", size = 10, aes(colour = type, fill = type)) + - scale_fill_manual(values = c("Control" = "green", "Patients" = "#b20000")) + - scale_color_manual(values = c("Control" = "black", "Patients" = "#b20000")) + - theme( - legend.position = "none", axis.text.x = element_text(angle = 90, hjust = 1), axis.title = element_blank(), - plot.title = element_text(hjust = 0.5, size = 18, face = "bold"), axis.text = element_text(size = 12, face = "bold"), - panel.background = element_rect(fill = "white", colour = "black") - ) + - ggtitle(hmdb_id) - - print(p) + boxplot_excel <- create_boxplot_excel(intensities_plots_df_long, hmdb_id) + + print(boxplot_excel) dev.off() # place the plot in the Excel file @@ -213,20 +210,6 @@ if (z_score == 1) { width = col_width, units = "px" ) - - if (hmdb_id %in% metab_list_helix) { - openxlsx::insertImage( - wb_helix_intensities, - sheetname, - tmp_png, - startRow = row_helix, - startCol = 1, - height = 560, - width = col_width, - units = "px" - ) - row_helix <- row_helix + 1 - } } wb_intensities <- set_row_height_col_width_wb( wb_intensities, From 9e638aed1e9c99a45a83f7ecd159fe90d557b24c Mon Sep 17 00:00:00 2001 From: ALuesink Date: Tue, 16 Dec 2025 09:48:22 +0100 Subject: [PATCH 096/161] Added new functions and did styler and linter --- DIMS/export/generate_excel_functions.R | 92 +++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 11 deletions(-) diff --git a/DIMS/export/generate_excel_functions.R b/DIMS/export/generate_excel_functions.R index bc1b0ed3..727437f0 100644 --- a/DIMS/export/generate_excel_functions.R +++ b/DIMS/export/generate_excel_functions.R @@ -34,23 +34,29 @@ calculate_zscores <- function(outlist, zscore_type, control_cols, stat_filter, i if (zscore_type == "_Zscore") { # Calculate mean and sd with all controls outlist$avg_ctrls <- apply(control_cols, 1, function(x) mean(as.numeric(x), na.rm = TRUE)) - outlist$sd_ctrls <- apply(control_cols, 1, function(x) sd(as.numeric(x), na.rm = TRUE)) + outlist$sd_ctrls <- apply(control_cols, 1, function(x) sd(as.numeric(x), na.rm = TRUE)) } else { if (length(control_cols) > 3) { for (metabolite_index in seq_len(nrow(outlist))) { if (zscore_type == "_RobustZscore") { # Calculate mean and sd, remove outlier controls by using robust scaler - outlist$avg_ctrls[metabolite_index] <- mean(robust_scaler(outlist[metabolite_index, control_cols], - control_cols, stat_filter)) - outlist$sd_ctrls[metabolite_index] <- sd(robust_scaler(outlist[metabolite_index, control_cols], - control_cols, stat_filter)) + outlist$avg_ctrls[metabolite_index] <- mean(robust_scaler( + outlist[metabolite_index, control_cols], + control_cols, stat_filter + )) + outlist$sd_ctrls[metabolite_index] <- sd(robust_scaler( + outlist[metabolite_index, control_cols], + control_cols, stat_filter + )) } else { # Calculate mean, sd and number of remaining controls, remove outlier controls by using grubbs test - intensities_without_outliers <- remove_outliers_grubbs(as.numeric(outlist[metabolite_index, control_cols]), - stat_filter) + intensities_without_outliers <- remove_outliers_grubbs( + outlist[metabolite_index, control_cols], + stat_filter + ) outlist$avg_ctrls[metabolite_index] <- mean(intensities_without_outliers) - outlist$sd_ctrls[metabolite_index] <- sd(intensities_without_outliers) - outlist$nr_ctrls[metabolite_index] <- length(intensities_without_outliers) + outlist$sd_ctrls[metabolite_index] <- sd(intensities_without_outliers) + outlist$nr_ctrls[metabolite_index] <- length(intensities_without_outliers) } } } @@ -76,8 +82,8 @@ robust_scaler <- function(control_intensities, control_col_ids, perc = 5) { #' @return trimmed_control_intensities: Intensities trimmed for outliers nr_to_remove <- ceiling(length(control_col_ids) * perc / 100) sorted_control_intensities <- sort(as.numeric(control_intensities)) - trimmed_control_intensities <- sorted_control_intensities[(nr_to_remove + 1) : - (length(sorted_control_intensities) - nr_to_remove)] + trimmed_control_intensities <- sorted_control_intensities[(nr_to_remove + 1): + (length(sorted_control_intensities) - nr_to_remove)] return(trimmed_control_intensities) } @@ -88,6 +94,8 @@ remove_outliers_grubbs <- function(control_intensities, outlier_threshold = 2) { #' @param outlier_threshold: Threshold for outliers which will be removed from controls (float) #' #' @return trimmed_control_intensities: Intensities trimmed for outliers + + control_intensities <- as.numeric(control_intensities) mean_permetabolite <- mean(as.numeric(control_intensities)) stdev_permetabolite <- sd(as.numeric(control_intensities)) zscores_permetabolite <- (control_intensities - mean_permetabolite) / stdev_permetabolite @@ -97,6 +105,7 @@ remove_outliers_grubbs <- function(control_intensities, outlier_threshold = 2) { } else { trimmed_control_intensities <- control_intensities } + trimmed_control_intensities <- as.numeric(trimmed_control_intensities) return(trimmed_control_intensities) } @@ -130,3 +139,64 @@ set_row_height_col_width_wb <- function(wb, sheetname, num_rows_df, num_cols_df, } return(wb) } + +#' Transform a dataframe with intensities to long format +#' +#' Get intensities of controls and patient for the selected metabolite, +#' pivot to long format, arrange Samples nummerically, change Sample names, get group size and +#' set Intensities to numeric. +#' +#' @param intensities_plots_df: a dataframe with HMDB_key column and intensities for all samples +#' +#' @returns intensities_plots_df_long: a dataframe with on each row a sample and their intensity +transform_ints_df_plots <- function(intensities_plots_df, row_index) { + intensities_plots_df_long <- intensities_plots_df %>% + slice(row_index) %>% + select(-HMDB_key) %>% + as.data.frame() %>% + pivot_longer(everything(), names_to = "Samples", values_to = "Intensities") %>% + arrange(nchar(Samples)) %>% + mutate( + Samples = gsub("\\..*", "", Samples), + Samples = gsub("(C).*", "\\1", Samples), + Intensities = as.numeric(Intensities), + type = ifelse(Samples == "C", "Control", "Patients") + ) %>% + group_by(Samples) %>% + mutate(group_size = n()) %>% + ungroup() + + return(intensities_plots_df_long) +} + + +#' Create a plot of intensities of samples for Excel +#' Use boxplot if group size is above 2, otherwise use a dash/line +#' +#' @param intensities_plots_df_long: a dataframe with on each row a sample and their intensity +#' @param hmdb_id: HMDB ID of the selected metabolite +#' +#' @returns boxplot_excel: ggplot2 object containing the plot of intensities +create_boxplot_excel <- function(intensities_plots_df_long, hmdb_id) { + boxplot_excel <- ggplot(intensities_plots_df_long, aes(Samples, Intensities)) + + geom_boxplot(data = subset(intensities_plots_df_long, group_size > 2), aes(fill = type)) + + geom_point( + data = subset(intensities_plots_df_long, group_size <= 2), + shape = "-", + size = 10, + aes(colour = type, fill = type) + ) + + scale_fill_manual(values = c("Control" = "green", "Patients" = "#b20000")) + + scale_color_manual(values = c("Control" = "black", "Patients" = "#b20000")) + + theme( + legend.position = "none", + axis.text = element_text(size = 12, face = "bold"), + axis.text.x = element_text(angle = 90, hjust = 1), + axis.title = element_blank(), + plot.title = element_text(hjust = 0.5, size = 18, face = "bold"), + panel.background = element_rect(fill = "white", colour = "black") + ) + + ggtitle(hmdb_id) + + return(boxplot_excel) +} From 08dbf31ef0a7c36a93795ad1dbc5e15e8029f137 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Tue, 16 Dec 2025 09:48:38 +0100 Subject: [PATCH 097/161] Added new unit tests and did styler and linter --- DIMS/tests/testthat/test_generate_excel.R | 422 ++++++++++++++++++---- 1 file changed, 352 insertions(+), 70 deletions(-) diff --git a/DIMS/tests/testthat/test_generate_excel.R b/DIMS/tests/testthat/test_generate_excel.R index 7a1b4280..817bb36f 100644 --- a/DIMS/tests/testthat/test_generate_excel.R +++ b/DIMS/tests/testthat/test_generate_excel.R @@ -5,107 +5,273 @@ library("ggplot2") library("reshape2") library("openxlsx") +library("vdiffr") suppressMessages(library("tidyr")) suppressMessages(library("dplyr")) suppressMessages(library("stringr")) source("../../export/generate_excel_functions.R") -testthat::test_that("Get indices of columns and dataframe of intensities of a given label", { +testthat::test_that("get_intensities_cols: Get indices of columns and dataframe of intensities of a given label", { test_outlist <- read.delim(test_path("fixtures", "test_outlist.txt")) - + control_label <- "C" case_label <- "P" expect_equal(get_intensities_cols(test_outlist, control_label)$col_idx, c(2:13)) - expect_equal(colnames(get_intensities_cols(test_outlist, control_label)$df_intensities), - c("C101.1", "C102.1", "C103.1", "C104.1", "C105.1", "C106.1", "C107.1", "C108.1", - "C109.1", "C110.1", "C111.1", "C112.1")) - expect_equal(rownames(get_intensities_cols(test_outlist, control_label)$df_intensities), - c("HMDB001", "HMDB002", "HMDB003", "HMDB004")) + expect_equal( + colnames(get_intensities_cols(test_outlist, control_label)$df_intensities), + c("C101.1", "C102.1", "C103.1", "C104.1", "C105.1", "C106.1", "C107.1", "C108.1", "C109.1", "C110.1", "C111.1", "C112.1") + ) + expect_equal( + rownames(get_intensities_cols(test_outlist, control_label)$df_intensities), + c("HMDB001", "HMDB002", "HMDB003", "HMDB004") + ) expect_equal(get_intensities_cols(test_outlist, control_label)$df_intensities$C101.1, c(1000, 1200, 1300, 1400)) expect_equal(get_intensities_cols(test_outlist, case_label)$col_idx, c(14, 15)) expect_equal(colnames(get_intensities_cols(test_outlist, case_label)$df_intensities), c("P2.1", "P3.1")) }) -testthat::test_that("Calculating Z-scores using different methods for excluding controls", { +testthat::test_that("calculate_zscores: Calculating Z-scores using different methods for excluding controls", { test_outlist <- read.delim(test_path("fixtures", "test_outlist.txt")) control_intensities <- read.delim(test_path("fixtures", "test_control_intensities.txt")) - + control_col_idx <- c(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13) intensity_col_ids <- c(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15) startcol <- ncol(test_outlist) + 4 perc <- 5 outlier_threshold <- 2 - expect_type(calculate_zscores(test_outlist, "_Zscore", control_intensities, NULL, intensity_col_ids, startcol), "list") - expect_identical(colnames(calculate_zscores(test_outlist, "_Zscore", control_intensities, NULL, intensity_col_ids, startcol)), - c("plots", "C101.1", "C102.1", "C103.1", "C104.1", "C105.1", "C106.1", "C107.1", "C108.1", "C109.1", "C110.1", - "C111.1", "C112.1", "P2.1", "P3.1", "HMDB_name", "HMDB_name_all", "HMDB_ID_all", "sec_HMDB_ID", - "HMDB_key", "sec_HMDB_ID_rlvc", "name", "relevance", "descr", "origin", "fluids", "tissue", "disease", - "pathway", "HMDB_code", "avg_ctrls", "sd_ctrls", "nr_ctrls", "C101.1_Zscore", "C102.1_Zscore", "C103.1_Zscore", - "C104.1_Zscore", "C105.1_Zscore", "C106.1_Zscore", "C107.1_Zscore", "C108.1_Zscore", "C109.1_Zscore", - "C110.1_Zscore", "C111.1_Zscore", "C112.1_Zscore", "P2.1_Zscore", "P3.1_Zscore")) - expect_equal(round(calculate_zscores(test_outlist, "_Zscore", control_intensities, NULL, intensity_col_ids, startcol)$avg_ctrls, 3), - c(16129.167, 1150.0, 1231.250, 4015.833), tolerance = 0.001) - expect_equal(calculate_zscores(test_outlist, "_Zscore", control_intensities, NULL, intensity_col_ids, startcol)$P2.1_Zscore, - c(-0.2544103, 32.4586955, 13.6066674, 0.4037668), tolerance = 0.001) - - expect_type(calculate_zscores(test_outlist, "_RobustZscore", control_col_idx, perc, intensity_col_ids, startcol), "list") - expect_identical(colnames(calculate_zscores(test_outlist, "_RobustZscore", control_col_idx, perc, intensity_col_ids, startcol))[34:47], - c("C101.1_RobustZscore", "C102.1_RobustZscore", - "C103.1_RobustZscore", "C104.1_RobustZscore", "C105.1_RobustZscore", "C106.1_RobustZscore", - "C107.1_RobustZscore", "C108.1_RobustZscore", "C109.1_RobustZscore", "C110.1_RobustZscore", - "C111.1_RobustZscore", "C112.1_RobustZscore", "P2.1_RobustZscore", "P3.1_RobustZscore")) - expect_equal(calculate_zscores(test_outlist, "_RobustZscore", control_col_idx, perc, intensity_col_ids, startcol)$avg_ctrls, - c(1255.0, 1110.0, 1227.5, 2811.5), tolerance = 0.001) - expect_equal(calculate_zscores(test_outlist, "_RobustZscore", control_col_idx, perc, intensity_col_ids, startcol)$P2.1_RobustZscore, - c(9.1511750, 46.9804468, 16.8039663, 0.8565111), tolerance = 0.001) - - expect_type(calculate_zscores(test_outlist, "_OutlierRemovedZscore", control_col_idx, outlier_threshold, intensity_col_ids, startcol), "list") - expect_identical(colnames(calculate_zscores(test_outlist, "_OutlierRemovedZscore", control_col_idx, outlier_threshold, intensity_col_ids, startcol))[34:47], - c("C101.1_OutlierRemovedZscore", - "C102.1_OutlierRemovedZscore", "C103.1_OutlierRemovedZscore", "C104.1_OutlierRemovedZscore", - "C105.1_OutlierRemovedZscore", "C106.1_OutlierRemovedZscore", "C107.1_OutlierRemovedZscore", - "C108.1_OutlierRemovedZscore", "C109.1_OutlierRemovedZscore", "C110.1_OutlierRemovedZscore", - "C111.1_OutlierRemovedZscore", "C112.1_OutlierRemovedZscore", "P2.1_OutlierRemovedZscore", - "P3.1_OutlierRemovedZscore") - ) - expect_equal(calculate_zscores(test_outlist, "_OutlierRemovedZscore", control_col_idx, outlier_threshold, intensity_col_ids, startcol)$avg_ctrls, - c(1231.818, 1077.273, 1231.250, 2649.091), tolerance = 0.001) - expect_equal(calculate_zscores(test_outlist, "_OutlierRemovedZscore", control_col_idx, outlier_threshold, intensity_col_ids, startcol)$nr_ctrls, - c(11, 11, 12, 11)) - expect_equal(calculate_zscores(test_outlist, "_OutlierRemovedZscore", control_col_idx, outlier_threshold, intensity_col_ids, startcol)$P2.1_OutlierRemovedZscore, - c(8.9955723, 44.9136860, 13.6066674, 0.9345077), tolerance = 0.001) + expect_type( + calculate_zscores( + test_outlist, + "_Zscore", + control_intensities, + NULL, + intensity_col_ids, + startcol + ), + "list" + ) + + expect_identical( + colnames( + calculate_zscores( + test_outlist, + "_Zscore", + control_intensities, + NULL, + intensity_col_ids, + startcol + ) + ), + c( + "plots", "C101.1", "C102.1", "C103.1", "C104.1", "C105.1", "C106.1", "C107.1", "C108.1", "C109.1", "C110.1", "C111.1", + "C112.1", "P2.1", "P3.1", "HMDB_name", "HMDB_name_all", "HMDB_ID_all", "sec_HMDB_ID", "HMDB_key", "sec_HMDB_ID_rlvc", + "name", "relevance", "descr", "origin", "fluids", "tissue", "disease", "pathway", "HMDB_code", "avg_ctrls", "sd_ctrls", + "nr_ctrls", "C101.1_Zscore", "C102.1_Zscore", "C103.1_Zscore", "C104.1_Zscore", "C105.1_Zscore", "C106.1_Zscore", + "C107.1_Zscore", "C108.1_Zscore", "C109.1_Zscore", "C110.1_Zscore", "C111.1_Zscore", "C112.1_Zscore", "P2.1_Zscore", + "P3.1_Zscore" + ) + ) + expect_equal( + round( + calculate_zscores( + test_outlist, + "_Zscore", + control_intensities, + NULL, + intensity_col_ids, + startcol + )$avg_ctrls, 3 + ), + c(16129.167, 1150.0, 1231.250, 4015.833), + tolerance = 0.001 + ) + expect_equal( + calculate_zscores( + test_outlist, + "_Zscore", + control_intensities, + NULL, + intensity_col_ids, + startcol + )$P2.1_Zscore, + c(-0.2544103, 32.4586955, 13.6066674, 0.4037668), + tolerance = 0.001 + ) + + expect_type( + calculate_zscores( + test_outlist, + "_RobustZscore", + control_col_idx, + perc, + intensity_col_ids, + startcol + ), + "list" + ) + + expect_identical( + colnames( + calculate_zscores( + test_outlist, + "_RobustZscore", + control_col_idx, + perc, + intensity_col_ids, + startcol + ) + )[34:47], + c( + "C101.1_RobustZscore", "C102.1_RobustZscore", "C103.1_RobustZscore", "C104.1_RobustZscore", "C105.1_RobustZscore", + "C106.1_RobustZscore", "C107.1_RobustZscore", "C108.1_RobustZscore", "C109.1_RobustZscore", "C110.1_RobustZscore", + "C111.1_RobustZscore", "C112.1_RobustZscore", "P2.1_RobustZscore", "P3.1_RobustZscore" + ) + ) + + expect_equal( + calculate_zscores( + test_outlist, + "_RobustZscore", + control_col_idx, + perc, + intensity_col_ids, + startcol + )$avg_ctrls, + c(1255.0, 1110.0, 1227.5, 2811.5), + tolerance = 0.001 + ) + + expect_equal( + calculate_zscores( + test_outlist, + "_RobustZscore", + control_col_idx, + perc, + intensity_col_ids, + startcol + )$P2.1_RobustZscore, + c(9.1511750, 46.9804468, 16.8039663, 0.8565111), + tolerance = 0.001 + ) + + expect_type( + calculate_zscores( + test_outlist, + "_OutlierRemovedZscore", + control_col_idx, + outlier_threshold, + intensity_col_ids, + startcol + ), + "list" + ) + + expect_identical( + colnames( + calculate_zscores( + test_outlist, + "_OutlierRemovedZscore", + control_col_idx, + outlier_threshold, + intensity_col_ids, + startcol + ) + )[34:47], + c( + "C101.1_OutlierRemovedZscore", "C102.1_OutlierRemovedZscore", "C103.1_OutlierRemovedZscore", + "C104.1_OutlierRemovedZscore", "C105.1_OutlierRemovedZscore", "C106.1_OutlierRemovedZscore", + "C107.1_OutlierRemovedZscore", "C108.1_OutlierRemovedZscore", "C109.1_OutlierRemovedZscore", + "C110.1_OutlierRemovedZscore", "C111.1_OutlierRemovedZscore", "C112.1_OutlierRemovedZscore", + "P2.1_OutlierRemovedZscore", "P3.1_OutlierRemovedZscore" + ) + ) + + expect_equal( + calculate_zscores( + test_outlist, + "_OutlierRemovedZscore", + control_col_idx, + outlier_threshold, + intensity_col_ids, + startcol + )$avg_ctrls, + c(1231.818, 1077.273, 1231.250, 2649.091), + tolerance = 0.001 + ) + expect_equal( + calculate_zscores( + test_outlist, + "_OutlierRemovedZscore", + control_col_idx, + outlier_threshold, + intensity_col_ids, + startcol + )$nr_ctrls, + c(11, 11, 12, 11) + ) + expect_equal( + calculate_zscores( + test_outlist, + "_OutlierRemovedZscore", + control_col_idx, + outlier_threshold, + intensity_col_ids, + startcol + )$P2.1_OutlierRemovedZscore, + c(8.9955723, 44.9136860, 13.6066674, 0.9345077), + tolerance = 0.001 + ) }) -testthat::test_that("Use robust scaler", { +testthat::test_that("robust_scaler: Use robust scaler", { control_intensities <- read.delim(test_path("fixtures", "test_control_intensities.txt")) - + control_col_idx <- c(1) perc <- 5 expect_type(robust_scaler(control_intensities[1, 1:12], control_col_idx, perc), "double") expect_length(robust_scaler(control_intensities[1, 1:12], control_col_idx, perc), 10) - expect_equal(robust_scaler(control_intensities[1, 1:12], control_col_idx, perc), - c(1050, 1050, 1100, 1150, 1200, 1250, 1300, 1350, 1450, 1650), tolerance = 0.001) + expect_equal( + robust_scaler( + control_intensities[1, 1:12], + control_col_idx, + perc + ), + c(1050, 1050, 1100, 1150, 1200, 1250, 1300, 1350, 1450, 1650), + tolerance = 0.001 + ) }) -testthat::test_that("Use Grubbs outlier removal", { +testthat::test_that("remove_outliers_grubbs: Use Grubbs outlier removal", { control_intensities <- read.delim(test_path("fixtures", "test_control_intensities.txt")) - + outlier_threshold <- 2 expect_type(remove_outliers_grubbs(control_intensities[1, ], outlier_threshold), "list") expect_length(remove_outliers_grubbs(control_intensities[1, ], outlier_threshold), 11) - expect_equal(as.numeric(remove_outliers_grubbs(control_intensities[1, ], outlier_threshold)), - c(1000, 1100, 1300, 1650, 1050, 1150, 1350, 1450, 1200, 1050, 1250), tolerance = 0.001) - expect_identical(colnames(remove_outliers_grubbs(control_intensities[1, ], outlier_threshold)), - c("C101.1", "C102.1", "C103.1", "C104.1", "C106.1", "C107.1", "C108.1", - "C109.1", "C110.1", "C111.1", "C112.1")) + expect_equal( + as.numeric(remove_outliers_grubbs( + control_intensities[1, ], + outlier_threshold + )), + c(1000, 1100, 1300, 1650, 1050, 1150, 1350, 1450, 1200, 1050, 1250), + tolerance = 0.001 + ) + expect_identical( + colnames( + remove_outliers_grubbs( + control_intensities[1, ], + outlier_threshold + ) + ), + c("C101.1", "C102.1", "C103.1", "C104.1", "C106.1", "C107.1", "C108.1", "C109.1", "C110.1", "C111.1", "C112.1") + ) }) -testthat::test_that("Save data to RData and txt file", { +testthat::test_that("save_to_rdata_and_txt: Save data to RData and txt file", { test_df <- data.frame( C101.1 = c(100, 200, 300, 400), C102.1 = c(125, 225, 325, 425), @@ -123,14 +289,14 @@ testthat::test_that("Save data to RData and txt file", { expect_identical(colnames(df), c("C101.1", "C102.1", "P2.1", "P3.1", "HMDB_name", "sec_HMDB_ID")) expect_equal(df$P2.1, c(150, 250, 350, 450)) } - + check_cols_and_values(read.delim("test_df.txt")) check_cols_and_values(get(load("test_df.RData"))) file.remove("test_df.txt", "test_df.RData") }) -testthat::test_that("Check row height and column width in a workbook", { +testthat::test_that("set_row_height_col_width_wb: Check row height and column width in a workbook", { test_wb_plots <- openxlsx::createWorkbook("Test") openxlsx::addWorksheet(test_wb_plots, "Test_with_plots") @@ -142,13 +308,30 @@ testthat::test_that("Check row height and column width in a workbook", { correct_col_widths <- c("5", "20", "20", "20", "20") names(correct_col_widths) <- c(1, 2, 3, 4, 5) attr(correct_col_widths, "hidden") <- c("0", "0", "0", "0", "0") - expect_identical(set_row_height_col_width_wb(test_wb_plots, sheetname_with_plots, num_rows_df, num_cols_df, plot_width, - plots_present = TRUE)$colWidths[[1]], correct_col_widths) + expect_identical( + set_row_height_col_width_wb( + test_wb_plots, + sheetname_with_plots, + num_rows_df, + num_cols_df, + plot_width, + plots_present = TRUE + )$colWidths[[1]], + correct_col_widths + ) correct_row_heights <- c("140", "140", "140", "140", "140") names(correct_row_heights) <- c(2, 3, 4, 5, 6) - expect_identical(set_row_height_col_width_wb(test_wb_plots, sheetname_with_plots, num_rows_df, num_cols_df, plot_width, - plots_present = TRUE)$rowHeights[[1]], correct_row_heights) + expect_identical( + set_row_height_col_width_wb(test_wb_plots, + sheetname_with_plots, + num_rows_df, + num_cols_df, + plot_width, + plots_present = TRUE + )$rowHeights[[1]], + correct_row_heights + ) rm(test_wb_plots) @@ -159,13 +342,112 @@ testthat::test_that("Check row height and column width in a workbook", { correct_col_widths <- c("20", "20", "20", "20", "20") names(correct_col_widths) <- c(1, 2, 3, 4, 5) attr(correct_col_widths, "hidden") <- c("0", "0", "0", "0", "0") - expect_identical(set_row_height_col_width_wb(test_wb_no_plots, sheetname_no_plots, num_rows_df, num_cols_df, plot_width = NULL, - plots_present = FALSE)$colWidths[[1]], correct_col_widths) + expect_identical( + set_row_height_col_width_wb( + test_wb_no_plots, + sheetname_no_plots, + num_rows_df, + num_cols_df, + plot_width = NULL, + plots_present = FALSE + )$colWidths[[1]], + correct_col_widths + ) correct_row_heights <- c("18", "18", "18", "18", "18") names(correct_row_heights) <- c(1, 2, 3, 4, 5) - expect_identical(set_row_height_col_width_wb(test_wb_no_plots, sheetname_no_plots, num_rows_df, num_cols_df, plot_width = NULL, - plots_present = FALSE)$rowHeights[[1]], correct_row_heights) + expect_identical( + set_row_height_col_width_wb( + test_wb_no_plots, + sheetname_no_plots, + num_rows_df, + num_cols_df, + plot_width = NULL, + plots_present = FALSE + )$rowHeights[[1]], + correct_row_heights + ) rm(test_wb_no_plots) }) + +testthat::test_that("transform_ints_df_plots: Check transformation of dataframe to long format", { + test_intensities_plots_df <- data.frame( + C101.1 = c(100, 200, 300, 400), + C102.1 = c(125, 225, 325, 425), + P2000M00002.1 = c(150, 250, 350, 450), + P3000M00003.1 = c(175, 275, 375, 475), + HMDB_key = c("metab_1", "metab_2", "metab_3", "metab_4") + ) + + test_row_index <- 1 + + expect_equal(dim(transform_ints_df_plots(test_intensities_plots_df, test_row_index)), c(4, 4)) + expect_identical( + colnames( + transform_ints_df_plots( + test_intensities_plots_df, + test_row_index + ) + ), + c("Samples", "Intensities", "type", "group_size") + ) + expect_identical( + transform_ints_df_plots( + test_intensities_plots_df, + test_row_index + )$Samples, + c("C", "C", "P2000M00002", "P3000M00003") + ) + expect_identical( + transform_ints_df_plots( + test_intensities_plots_df, + test_row_index + )$Intensities, + c(100, 125, 150, 175) + ) + expect_identical( + transform_ints_df_plots( + test_intensities_plots_df, + test_row_index + )$type, + c("Control", "Control", "Patients", "Patients") + ) + expect_equal( + transform_ints_df_plots( + test_intensities_plots_df, + test_row_index + )$group_size, + c(2, 2, 1, 1) + ) + + test_row_index <- 2 + expect_identical( + transform_ints_df_plots( + test_intensities_plots_df, + test_row_index + )$Intensities, + c(200, 225, 250, 275) + ) +}) + +testthat::test_that("create_boxplot_excel: Create a boxplot for the Excel", { + test_ints_plots_df_long <- data.frame( + Samples = c("C", "C", "C", "C", "C", "P1", "P2", "P3", "P4", "P5"), + Intensities = c(150, 250, 225, 300, 175, 325, 600, 150, 350, 275), + type = c( + "Control", "Control", "Control", "Control", "Control", + "Patients", "Patients", "Patients", "Patients", "Patients" + ), + group_size = c(5, 5, 5, 5, 5, 1, 1, 1, 1, 1) + ) + + test_hmdb_id <- "Test Metab 1" + + expect_silent(create_boxplot_excel(test_ints_plots_df_long, test_hmdb_id)) + + expect_doppelganger( + title = "create boxplot excel", + fig = create_boxplot_excel(test_ints_plots_df_long, test_hmdb_id) + ) +}) From 97ec94d8930202aac61afb6c3d73107c5b72c8db Mon Sep 17 00:00:00 2001 From: ALuesink Date: Tue, 16 Dec 2025 09:52:48 +0100 Subject: [PATCH 098/161] Added vdiffr to DIMS test dependencies --- .github/workflows/dims_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dims_test.yml b/.github/workflows/dims_test.yml index fa2a8aa2..b547d126 100644 --- a/.github/workflows/dims_test.yml +++ b/.github/workflows/dims_test.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 - name: Install dependencies - run: Rscript -e "install.packages(c('testthat', 'withr'))" + run: Rscript -e "install.packages(c('testthat', 'withr', 'vdiffr'))" - name: Run tests run: Rscript tests/testthat.R From 1fc1d3ac84cbd88b5a4b5e1a306c015c3d61512a Mon Sep 17 00:00:00 2001 From: ALuesink Date: Tue, 16 Dec 2025 10:19:14 +0100 Subject: [PATCH 099/161] Fix error numeric values --- DIMS/export/generate_excel_functions.R | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/DIMS/export/generate_excel_functions.R b/DIMS/export/generate_excel_functions.R index 727437f0..ef253a8e 100644 --- a/DIMS/export/generate_excel_functions.R +++ b/DIMS/export/generate_excel_functions.R @@ -51,7 +51,7 @@ calculate_zscores <- function(outlist, zscore_type, control_cols, stat_filter, i } else { # Calculate mean, sd and number of remaining controls, remove outlier controls by using grubbs test intensities_without_outliers <- remove_outliers_grubbs( - outlist[metabolite_index, control_cols], + as.numeric(outlist[metabolite_index, control_cols]), stat_filter ) outlist$avg_ctrls[metabolite_index] <- mean(intensities_without_outliers) @@ -83,7 +83,7 @@ robust_scaler <- function(control_intensities, control_col_ids, perc = 5) { nr_to_remove <- ceiling(length(control_col_ids) * perc / 100) sorted_control_intensities <- sort(as.numeric(control_intensities)) trimmed_control_intensities <- sorted_control_intensities[(nr_to_remove + 1): - (length(sorted_control_intensities) - nr_to_remove)] + (length(sorted_control_intensities) - nr_to_remove)] return(trimmed_control_intensities) } @@ -94,8 +94,6 @@ remove_outliers_grubbs <- function(control_intensities, outlier_threshold = 2) { #' @param outlier_threshold: Threshold for outliers which will be removed from controls (float) #' #' @return trimmed_control_intensities: Intensities trimmed for outliers - - control_intensities <- as.numeric(control_intensities) mean_permetabolite <- mean(as.numeric(control_intensities)) stdev_permetabolite <- sd(as.numeric(control_intensities)) zscores_permetabolite <- (control_intensities - mean_permetabolite) / stdev_permetabolite @@ -105,7 +103,6 @@ remove_outliers_grubbs <- function(control_intensities, outlier_threshold = 2) { } else { trimmed_control_intensities <- control_intensities } - trimmed_control_intensities <- as.numeric(trimmed_control_intensities) return(trimmed_control_intensities) } From bb007f0aeebfafef3eaab6616987b1adca0cde88 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Tue, 16 Dec 2025 10:51:47 +0100 Subject: [PATCH 100/161] Snapshot boxplot --- .../generate_excel/create-boxplot-excel.svg | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 DIMS/tests/testthat/_snaps/generate_excel/create-boxplot-excel.svg diff --git a/DIMS/tests/testthat/_snaps/generate_excel/create-boxplot-excel.svg b/DIMS/tests/testthat/_snaps/generate_excel/create-boxplot-excel.svg new file mode 100644 index 00000000..4e0913fb --- /dev/null +++ b/DIMS/tests/testthat/_snaps/generate_excel/create-boxplot-excel.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +- +- +- +- +- + + +200 +300 +400 +500 +600 + + + + + + + + + + + +C +P1 +P2 +P3 +P4 +P5 +Test Metab 1 + + From 083aeac9cb67609e0a3d9c5db7a484fd92e02e4c Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Mon, 22 Dec 2025 11:55:20 +0100 Subject: [PATCH 101/161] added unit test for empty peaklist --- DIMS/tests/testthat/test_average_peaks.R | 3 +++ 1 file changed, 3 insertions(+) diff --git a/DIMS/tests/testthat/test_average_peaks.R b/DIMS/tests/testthat/test_average_peaks.R index bb945b71..0a8d2345 100644 --- a/DIMS/tests/testthat/test_average_peaks.R +++ b/DIMS/tests/testthat/test_average_peaks.R @@ -15,6 +15,7 @@ testthat::test_that("peaks are correctly averaged", { test_peaklist_sorted[, 2] <- as.numeric(test_peaklist_sorted[, 2]) test_peaklist_sorted[, 6] <- as.numeric(test_peaklist_sorted[, 6]) test_sample_name <- "P001" + test_empty_peaklist <- test_peaklist_sorted[0, ] # test that first peak is correctly averaged expect_equal(as.numeric(average_peaks_per_sample(test_peaklist_sorted, test_sample_name)[1, 6]), 600, tolerance = 0.001, TRUE) @@ -23,5 +24,7 @@ testthat::test_that("peaks are correctly averaged", { # test column names expect_equal(colnames(average_peaks_per_sample(test_peaklist_sorted, test_sample_name)), c("samplenr", "mzmed.pkt", "fq", "mzmin.pkt", "mzmax.pkt", "height.pkt"), TRUE) + # test what happens when peak list is empty + expect_error(average_peaks_per_sample(test_empty_peaklist, test_sample_name)) }) From 7ea64f0ab240490301803482102ca8836f41dabc Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Mon, 22 Dec 2025 13:14:50 +0100 Subject: [PATCH 102/161] removed unused variables --- DIMS/FillMissing.R | 2 -- 1 file changed, 2 deletions(-) diff --git a/DIMS/FillMissing.R b/DIMS/FillMissing.R index 32480d38..27390376 100755 --- a/DIMS/FillMissing.R +++ b/DIMS/FillMissing.R @@ -4,8 +4,6 @@ cmd_args <- commandArgs(trailingOnly = TRUE) peakgrouplist_file <- cmd_args[1] preprocessing_scripts_dir <- cmd_args[2] thresh <- as.numeric(cmd_args[3]) -resol <- as.numeric(cmd_args[4]) -ppm <- as.numeric(cmd_args[5]) # load in function scripts source(paste0(preprocessing_scripts_dir, "fill_missing_functions.R")) From c386ea37877f8f646f1d3cfde8581cfbc3c8b8a1 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Mon, 22 Dec 2025 13:55:58 +0100 Subject: [PATCH 103/161] moved CollectFilled functions to preprocessing folder --- DIMS/CollectFilled.R | 11 ++-- DIMS/CollectFilled.nf | 2 +- DIMS/preprocessing/collect_filled_functions.R | 57 +++++++++++++++++++ 3 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 DIMS/preprocessing/collect_filled_functions.R diff --git a/DIMS/CollectFilled.R b/DIMS/CollectFilled.R index 5735bc56..69f9e04c 100755 --- a/DIMS/CollectFilled.R +++ b/DIMS/CollectFilled.R @@ -1,14 +1,11 @@ -## adapted from 10-collectSamplesFilled.R - # define parameters cmd_args <- commandArgs(trailingOnly = TRUE) -scripts_dir <- cmd_args[1] +preprocessing_scripts_dir <- cmd_args[1] ppm <- as.numeric(cmd_args[2]) z_score <- as.numeric(cmd_args[3]) -source(paste0(scripts_dir, "merge_duplicate_rows.R")) -source(paste0(scripts_dir, "calculate_zscores.R")) +source(paste0(preprocessing_scripts_dir, "collect_filled_functions.R")) # for each scan mode, collect all filled peak group lists scanmodes <- c("positive", "negative") @@ -17,7 +14,7 @@ for (scanmode in scanmodes) { filled_files <- list.files("./", full.names = TRUE, pattern = paste0(scanmode, "_identified_filled")) # load files and combine into one object outlist_total <- NULL - for (file_nr in 1:length(filled_files)) { + for (file_nr in seq_along(filled_files)) { peakgrouplist_filled <- get(load(filled_files[file_nr])) outlist_total <- rbind(outlist_total, peakgrouplist_filled) } @@ -47,7 +44,7 @@ for (scanmode in scanmodes) { outlist_stats_more <- cbind(outlist_stats_more, tmp) outlist_total <- outlist_stats_more } - + # make a copy of the outlist outlist_ident <- outlist_total # select identified peak groups if ppm deviation is within limits diff --git a/DIMS/CollectFilled.nf b/DIMS/CollectFilled.nf index 53b19676..b0025286 100644 --- a/DIMS/CollectFilled.nf +++ b/DIMS/CollectFilled.nf @@ -14,6 +14,6 @@ process CollectFilled { script: """ - Rscript ${baseDir}/CustomModules/DIMS/CollectFilled.R $params.scripts_dir $params.ppm $params.zscore + Rscript ${baseDir}/CustomModules/DIMS/CollectFilled.R $params.preprocessing_scripts_dir $params.ppm $params.zscore """ } diff --git a/DIMS/preprocessing/collect_filled_functions.R b/DIMS/preprocessing/collect_filled_functions.R new file mode 100644 index 00000000..2f3ca38b --- /dev/null +++ b/DIMS/preprocessing/collect_filled_functions.R @@ -0,0 +1,57 @@ +merge_duplicate_rows <- function(peakgroup_list) { + #' Merge identification info for peak groups with the same mass + #' + #' @param peakgroup_list: Peak group list (matrix) + #' + #' @return peakgroup_list_dedup: de-duplicated peak group list (matrix) + + collapse <- function(column_label, peakgroup_list, index_dup) { + #' Collapse identification info for peak groups with the same mass + #' + #' @param column_label: Name of column in peakgroup_list (string) + #' @param peakgroup_list: Peak group list (matrix) + #' @param index_dup: Index of duplicate peak group (integer) + #' + #' @return collapsed_items: Semicolon-separated list of info (string) + # get the item(s) that need to be collapsed + list_items <- as.vector(peakgroup_list[index_dup, column_label]) + # remove NA + if (length(which(is.na(list_items))) > 0) list_items <- list_items[-which(is.na(list_items))] + collapsed_items <- paste(list_items, collapse = ";") + return(collapsed_items) + } + + options(digits = 16) + collect <- NULL + remove <- NULL + + # check for peak groups with identical mass + index_dup <- which(duplicated(peakgroup_list[, "mzmed.pgrp"])) + + while (length(index_dup) > 0) { + # get the index for the peak group which is double + peaklist_index <- which(peakgroup_list[, "mzmed.pgrp"] == peakgroup_list[index_dup[1], "mzmed.pgrp"]) + single_peakgroup <- peakgroup_list[peaklist_index[1], , drop = FALSE] + + # use function collapse to concatenate info + single_peakgroup[, "assi_HMDB"] <- collapse("assi_HMDB", peakgroup_list, peaklist_index) + single_peakgroup[, "iso_HMDB"] <- collapse("iso_HMDB", peakgroup_list, peaklist_index) + single_peakgroup[, "HMDB_code"] <- collapse("HMDB_code", peakgroup_list, peaklist_index) + single_peakgroup[, "all_hmdb_ids"] <- collapse("all_hmdb_ids", peakgroup_list, peaklist_index) + single_peakgroup[, "sec_hmdb_ids"] <- collapse("sec_hmdb_ids", peakgroup_list, peaklist_index) + if (single_peakgroup[, "sec_hmdb_ids"] == ";") single_peakgroup[, "sec_hmdb_ids"] < NA + + # keep track of deduplicated entries + collect <- rbind(collect, single_peakgroup) + remove <- c(remove, peaklist_index) + + # remove current entry from index + index_dup <- index_dup[-which(peakgroup_list[index_dup, "mzmed.pgrp"] == peakgroup_list[index_dup[1], "mzmed.pgrp"])] + } + + # remove duplicate entries + if (!is.null(remove)) peakgroup_list <- peakgroup_list[-remove, ] + # append deduplicated entries + peakgroup_list_dedup <- rbind(peakgroup_list, collect) + return(peakgroup_list_dedup) +} From 1ff7e0ee8bec66bac9799d5892fac36c728fb25f Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Mon, 22 Dec 2025 15:05:04 +0100 Subject: [PATCH 104/161] refactored collect_filled_functions --- DIMS/preprocessing/collect_filled_functions.R | 96 +++++++++++++++---- 1 file changed, 79 insertions(+), 17 deletions(-) diff --git a/DIMS/preprocessing/collect_filled_functions.R b/DIMS/preprocessing/collect_filled_functions.R index 2f3ca38b..6741076f 100644 --- a/DIMS/preprocessing/collect_filled_functions.R +++ b/DIMS/preprocessing/collect_filled_functions.R @@ -1,3 +1,23 @@ +# CollectFilled functions + +collapse <- function(column_label, peakgroup_list, index_dup) { + #' Collapse identification info for peak groups with the same mass + #' + #' @param column_label: Name of column in peakgroup_list (string) + #' @param peakgroup_list: Peak group list (matrix) + #' @param index_dup: Index of duplicate peak group (integer) + #' + #' @return collapsed_items: Semicolon-separated list of info (string) + # get the item(s) that need to be collapsed + list_items <- as.vector(peakgroup_list[index_dup, column_label]) + # remove NA + if (length(which(is.na(list_items))) > 0) { + list_items <- list_items[-which(is.na(list_items))] + } + collapsed_items <- paste(list_items, collapse = ";") + return(collapsed_items) +} + merge_duplicate_rows <- function(peakgroup_list) { #' Merge identification info for peak groups with the same mass #' @@ -5,22 +25,6 @@ merge_duplicate_rows <- function(peakgroup_list) { #' #' @return peakgroup_list_dedup: de-duplicated peak group list (matrix) - collapse <- function(column_label, peakgroup_list, index_dup) { - #' Collapse identification info for peak groups with the same mass - #' - #' @param column_label: Name of column in peakgroup_list (string) - #' @param peakgroup_list: Peak group list (matrix) - #' @param index_dup: Index of duplicate peak group (integer) - #' - #' @return collapsed_items: Semicolon-separated list of info (string) - # get the item(s) that need to be collapsed - list_items <- as.vector(peakgroup_list[index_dup, column_label]) - # remove NA - if (length(which(is.na(list_items))) > 0) list_items <- list_items[-which(is.na(list_items))] - collapsed_items <- paste(list_items, collapse = ";") - return(collapsed_items) - } - options(digits = 16) collect <- NULL remove <- NULL @@ -50,8 +54,66 @@ merge_duplicate_rows <- function(peakgroup_list) { } # remove duplicate entries - if (!is.null(remove)) peakgroup_list <- peakgroup_list[-remove, ] + if (!is.null(remove)) { + peakgroup_list <- peakgroup_list[-remove, ] + } # append deduplicated entries peakgroup_list_dedup <- rbind(peakgroup_list, collect) return(peakgroup_list_dedup) } + +calculate_zscores <- function(peakgroup_list) { + #' Calculate Z-scores for peak groups based on average and standard deviation of controls + #' + #' @param peakgroup_list: Peak group list (matrix) + #' @param sort_col: Column to sort on (string) + #' @param adducts: Parameter indicating whether there are adducts in the list (boolean) + #' + #' @return peakgroup_list_dedup: de-duplicated peak group list (matrix) + + case_label <- "P" + control_label <- "C" + # get index for new column names + startcol <- ncol(peakgroup_list) + 3 + + # calculate mean and standard deviation for Control group + ctrl_cols <- grep(control_label, colnames(peakgroup_list), fixed = TRUE) + case_cols <- grep(case_label, colnames(peakgroup_list), fixed = TRUE) + int_cols <- c(ctrl_cols, case_cols) + # set all zeros to NA + peakgroup_list[, int_cols][peakgroup_list[, int_cols] == 0] <- NA + ctrl_ints <- peakgroup_list[, ctrl_cols, drop = FALSE] + peakgroup_list$avg.ctrls <- apply(ctrl_ints, 1, function(x) mean(as.numeric(x), na.rm = TRUE)) + peakgroup_list$sd.ctrls <- apply(ctrl_ints, 1, function(x) sd(as.numeric(x), na.rm = TRUE)) + + # set new column names and calculate Z-scores + colnames_zscores <- NULL + for (col_index in int_cols) { + col_name <- colnames(peakgroup_list)[col_index] + colnames_zscores <- c(colnames_zscores, paste0(col_name, "_Zscore")) + zscores_1col <- (as.numeric(as.vector(unlist(peakgroup_list[, col_index]))) - + peakgroup_list$avg.ctrls) / peakgroup_list$sd.ctrls + peakgroup_list <- cbind(peakgroup_list, zscores_1col) + } + + # apply new column names to columns at end plus avg and sd columns + colnames(peakgroup_list)[startcol:ncol(peakgroup_list)] <- colnames_zscores + + # add ppm deviation column + zscore_cols <- grep("Zscore", colnames(peakgroup_list), fixed = TRUE) + # calculate ppm deviation + for (row_index in seq_len(nrow(peakgroup_list))) { + if (!is.na(peakgroup_list$theormz_HMDB[row_index]) && + !is.null(peakgroup_list$theormz_HMDB[row_index]) && + (peakgroup_list$theormz_HMDB[row_index] != "")) { + peakgroup_list$ppmdev[row_index] <- 10^6 * (as.numeric(as.vector(peakgroup_list$mzmed.pgrp[row_index])) - + as.numeric(as.vector(peakgroup_list$theormz_HMDB[row_index]))) / + as.numeric(as.vector(peakgroup_list$theormz_HMDB[row_index])) + } else { + peakgroup_list$ppmdev[row_index] <- NA + } + } + + return(peakgroup_list) +} + From e813fe716ed8eae56fcb71c7e6fe45f931b4da7e Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 6 Jan 2026 18:45:16 +0100 Subject: [PATCH 105/161] extra output peak list per technical replicate --- DIMS/PeakFinding.R | 1 + DIMS/PeakFinding.nf | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/DIMS/PeakFinding.R b/DIMS/PeakFinding.R index 2cf49d20..ab62a880 100644 --- a/DIMS/PeakFinding.R +++ b/DIMS/PeakFinding.R @@ -54,5 +54,6 @@ for (scanmode in scanmodes) { # save output to file save(integrated_peak_df, file = paste0(techrepl_name, "_", scanmode, ".RData")) + write.table(integrated_peak_df, file = paste0(techrepl_name, "_", scanmode, ".txt"), sep = "\t", row.names = FALSE) } diff --git a/DIMS/PeakFinding.nf b/DIMS/PeakFinding.nf index 04c82540..1d02e505 100644 --- a/DIMS/PeakFinding.nf +++ b/DIMS/PeakFinding.nf @@ -9,7 +9,8 @@ process PeakFinding { each path(sample_techreps) output: - path '*tive.RData', optional: true + path '*tive.RData', emit: peaklist_rdata, optional: true + path '*tive.txt', optional: true script: """ From af62a0ed8277dca812c08cca190d49c4b34b5bf0 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 9 Jan 2026 17:13:26 +0100 Subject: [PATCH 106/161] modified DIMS AverageTechReplicates for QC info in mail --- DIMS/AverageTechReplicates.R | 6 ++++++ DIMS/AverageTechReplicates.nf | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/DIMS/AverageTechReplicates.R b/DIMS/AverageTechReplicates.R index 1c727d10..2235e47c 100644 --- a/DIMS/AverageTechReplicates.R +++ b/DIMS/AverageTechReplicates.R @@ -112,6 +112,9 @@ for (sample_nr in 1:length(repl_pattern)) { pattern_list <- remove_from_repl_pattern(remove_neg, repl_pattern, nr_replicates) repl_pattern_filtered <- pattern_list$pattern save(repl_pattern_filtered, file = "negative_repl_pattern.RData") +if (is.null(remove_neg)) { + remove_neg <- "none" +} write.table( remove_neg, file = "miss_infusions_negative.txt", @@ -123,6 +126,9 @@ write.table( pattern_list <- remove_from_repl_pattern(remove_pos, repl_pattern, nr_replicates) repl_pattern_filtered <- pattern_list$pattern save(repl_pattern_filtered, file = "positive_repl_pattern.RData") +if (is.null(remove_pos)) { + remove_pos <- "none" +} write.table( remove_pos, file = "miss_infusions_positive.txt", diff --git a/DIMS/AverageTechReplicates.nf b/DIMS/AverageTechReplicates.nf index f03d553c..8dafde45 100644 --- a/DIMS/AverageTechReplicates.nf +++ b/DIMS/AverageTechReplicates.nf @@ -17,8 +17,8 @@ process AverageTechReplicates { output: path('*_repl_pattern.RData'), emit: pattern_files path('*_avg.RData'), emit: binned_files - path('miss_infusions_negative.txt') - path('miss_infusions_positive.txt') + path('miss_infusions_negative.txt'), emit: qc_miss_neg + path('miss_infusions_positive.txt'), emit: qc_miss_pos path('*_TICplots.pdf'), emit: tic_plots_pdf script: From 7c349cea048b7ad7f244d8e6ad0c212e1e44828f Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 9 Jan 2026 17:28:18 +0100 Subject: [PATCH 107/161] modified DIMS GenerateQCOutput for QC info in mail --- DIMS/GenerateQCOutput.R | 28 +++++++++++++++++++++------- DIMS/GenerateQCOutput.nf | 2 +- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/DIMS/GenerateQCOutput.R b/DIMS/GenerateQCOutput.R index aa1cdae9..988c059b 100644 --- a/DIMS/GenerateQCOutput.R +++ b/DIMS/GenerateQCOutput.R @@ -48,14 +48,16 @@ if (z_score == 1) { is_list <- outlist[grep("Internal standard", outlist[, "relevance"], fixed = TRUE), ] is_codes <- rownames(is_list) -# check if there is data present for all the samples that the pipeline started with, -# if not write sample name to a log file. +# check if there is data present for all the samples that the pipeline started with sample_names_nodata <- setdiff(names(repl_pattern), names(is_list)) +if (length(sample_names_nodata) == 0) { + sample_names_nodata <- "none" +} +write.table(sample_names_nodata, + file = paste(outdir, "sample_names_nodata.txt", sep = "/"), + row.names = FALSE, col.names = FALSE, quote = FALSE +) if (!is.null(sample_names_nodata)) { - write.table(sample_names_nodata, - file = paste(outdir, "sample_names_nodata.txt", sep = "/"), - row.names = FALSE, col.names = FALSE, quote = FALSE - ) for (sample_name in sample_names_nodata) { repl_pattern[[sample_name]] <- NULL } @@ -238,8 +240,11 @@ if (dims_matrix == "Plasma") { } } } +is_below_threshold <- select(is_below_threshold, -c("Matrix", "Rundata", "Sample_level")) if (nrow(is_below_threshold) > 0) { - write.table(cbind(is_below_threshold, scanmode = scanmode_is), file = "internal_standards_below_threshold.txt", sep = "\t") + write.table(cbind(is_below_threshold, scanmode = scanmode_is), + file = "internal_standards_below_threshold.txt", + row.names = FALSE, sep = "\t") } else { write.table("no internal standards are below threshold", file = "internal_standards_below_threshold.txt" @@ -421,6 +426,15 @@ xlsx_name <- paste0(outdir, "/", project, "_IS_SST.xlsx") openxlsx::saveWorkbook(wb, xlsx_name, overwrite = TRUE) rm(wb) +# generate text file for workflow completed mail for components with Z-score < 2 +if (sum(grepl("P1001", colnames(sst_list_intensities))) > 0) { + zscore_column <- grep("_Zscore", colnames(sst_list_intensities))[1] + sst_list_intensities_qc <- sst_list_intensities[sst_list_intensities[, zscore_column] < 2, ] + sst_list_intensities_qc <- select(sst_list_intensities_qc, -c("CV_controls")) + write.table(sst_list_intensities_qc, file = paste(outdir, "sst_qc.txt", sep = "/"), row.names = FALSE, sep = "\t") +} else { + write.table("no SST sample present", file = paste(outdir, "sst_qc.txt", sep = "/"), row.names = FALSE, col.names = FALSE) +} ### MISSING M/Z CHECK # check the outlist_identified_(negative/positive).RData files for missing m/z values and save to file diff --git a/DIMS/GenerateQCOutput.nf b/DIMS/GenerateQCOutput.nf index be2eac37..6a4f6135 100644 --- a/DIMS/GenerateQCOutput.nf +++ b/DIMS/GenerateQCOutput.nf @@ -16,7 +16,7 @@ process GenerateQCOutput { tuple path('positive_controls_warning.txt'), path('missing_mz_warning.txt'), path('sample_names_nodata.txt'), optional: true tuple path('*_IS_SST.xlsx'), path('*_positive_control.xlsx'), optional: true path('plots/IS_*.png'), emit: plot_files - path('Check_number_of_controls.txt'), optional: true + path('check_number_of_controls.txt'), optional: true path('sst_qc.txt'), optional: true path('internal_standards_below_threshold.txt'), optional: true From 76baae9dfbfcaecd94122484e8d9749e81245dc9 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Thu, 22 Jan 2026 12:14:00 +0100 Subject: [PATCH 108/161] moved generation of list of internal standards below threshold to function --- DIMS/GenerateQCOutput.R | 55 +++++----------------- DIMS/export/generate_qc_output_functions.R | 26 ++++++++++ 2 files changed, 38 insertions(+), 43 deletions(-) diff --git a/DIMS/GenerateQCOutput.R b/DIMS/GenerateQCOutput.R index 988c059b..936ef25a 100644 --- a/DIMS/GenerateQCOutput.R +++ b/DIMS/GenerateQCOutput.R @@ -138,7 +138,6 @@ is_sum_selection <- c( "2H3-Glutamate (IS)", "2H4_13C5-Arginine (IS)", "13C6-Tyrosine (IS)" ) -all_is_names <- list(neg = is_neg_selection, pos = is_pos_selection, sum = is_sum_selection) # define threshold for acceptance of selected internal standards threshold_is_dbs_neg <- c(15000, 200000, 130000, 18000, 50000) @@ -147,9 +146,6 @@ threshold_is_dbs_sum <- c(1300000, 2500000, 500000, 1800000, 1400000) threshold_is_pl_neg <- c(70000, 700000, 700000, 65000, 350000) threshold_is_pl_pos <- c(1500000, 9000000, 3000000, 400000, 700000) threshold_is_pl_sum <- c(8000000, 12500000, 2500000, 3000000, 4000000) -all_is_thresholds_dbs <- list(neg = threshold_is_dbs_neg, pos = threshold_is_dbs_pos, sum = threshold_is_dbs_sum) -all_is_thresholds_pl <- list(neg = threshold_is_pl_neg, pos = threshold_is_pl_pos, sum = threshold_is_pl_sum) -all_is_thresholds <- list(names = all_is_names, plasma = all_is_thresholds_pl, dbs = all_is_thresholds_dbs) # add minimal intensity lines based on matrix (DBS or Plasma) and machine mode (neg, pos, sum) if (dims_matrix == "DBS") { @@ -199,48 +195,21 @@ is_pos_selection_subset <- subset(is_pos, HMDB_name %in% is_pos_selection) is_sum_selection_subset <- subset(is_summed, HMDB_name %in% is_sum_selection) # export txt file with samples with internal standard level below threshold -is_below_threshold <- is_pos_selection_subset[0, ] -scanmode_is <- c() if (dims_matrix == "Plasma") { - # pos - for (line_index in seq_len(nrow(is_pos_selection_subset))) { - is_selected <- is_pos_selection_subset$HMDB_name[line_index] - thresh_selected <- all_is_thresholds$plasma$pos[which(all_is_thresholds$names$pos == is_selected)] - if (is_pos_selection_subset$Intensity[line_index] < thresh_selected) { - is_below_threshold <- rbind(is_below_threshold, is_pos_selection_subset[line_index, ]) - scanmode_is <- c(scanmode_is, "pos") - } - } - # neg - for (line_index in seq_len(nrow(is_neg_selection_subset))) { - is_selected <- is_neg_selection_subset$HMDB_name[line_index] - thresh_selected <- all_is_thresholds$plasma$neg[which(all_is_thresholds$names$neg == is_selected)] - if (is_neg_selection_subset$Intensity[line_index] < thresh_selected) { - is_below_threshold <- rbind(is_below_threshold, is_neg_selection_subset[line_index, ]) - scanmode_is <- c(scanmode_is, "neg") - } - } + is_below_threshold_neg <- find_is_below_threshold(is_neg_selection_subset, threshold_is_pl_neg, is_neg_selection, "neg") + is_below_threshold_pos <- find_is_below_threshold(is_pos_selection_subset, threshold_is_pl_pos, is_pos_selection, "pos") + is_below_threshold_sum <- find_is_below_threshold(is_sum_selection_subset, threshold_is_pl_sum, is_sum_selection, "sum") + is_below_threshold <- rbind(is_below_threshold_pos, is_below_threshold_neg, is_below_threshold_sum) } else if (dims_matrix == "DBS") { - # pos - for (line_index in seq_len(nrow(is_pos_selection_subset))) { - is_selected <- is_pos_selection_subset$HMDB_name[line_index] - thresh_selected <- all_is_thresholds$dbs$pos[which(all_is_thresholds$names$pos == is_selected)] - if (is_pos_selection_subset$Intensity[line_index] < thresh_selected) { - is_below_threshold <- rbind(is_below_threshold, is_pos_selection_subset[line_index, ]) - scanmode_is <- c(scanmode_is, "pos") - } - } - # neg - for (line_index in seq_len(nrow(is_neg_selection_subset))) { - is_selected <- is_neg_selection_subset$HMDB_name[line_index] - thresh_selected <- all_is_thresholds$dbs$neg[which(all_is_thresholds$names$neg == is_selected)] - if (is_neg_selection_subset$Intensity[line_index] < thresh_selected) { - is_below_threshold <- rbind(is_below_threshold, is_neg_selection_subset[line_index, ]) - scanmode_is <- c(scanmode_is, "neg") - } - } + is_below_threshold_neg <- find_is_below_threshold(is_neg_selection_subset, threshold_is_dbs_neg, is_neg_selection, "neg") + is_below_threshold_pos <- find_is_below_threshold(is_pos_selection_subset, threshold_is_dbs_pos, is_pos_selection, "pos") + is_below_threshold_sum <- find_is_below_threshold(is_sum_selection_subset, threshold_is_dbs_sum, is_neg_selection, "sum") + is_below_threshold <- rbind(is_below_threshold_pos, is_below_threshold_neg, is_below_threshold_sum) +} else { + # generate empty table + is_below_threshold <- is_neg_selection_subset[0, ] } -is_below_threshold <- select(is_below_threshold, -c("Matrix", "Rundata", "Sample_level")) + if (nrow(is_below_threshold) > 0) { write.table(cbind(is_below_threshold, scanmode = scanmode_is), file = "internal_standards_below_threshold.txt", diff --git a/DIMS/export/generate_qc_output_functions.R b/DIMS/export/generate_qc_output_functions.R index 23c81d06..9f672adb 100644 --- a/DIMS/export/generate_qc_output_functions.R +++ b/DIMS/export/generate_qc_output_functions.R @@ -217,3 +217,29 @@ check_missing_mz <- function(mzmed_pgrp_ident, scanmode) { } return(results_mz_missing) } + +find_is_below_threshold <- function(is_selection_subset, thresholds, is_names, scanmode) { + #' Create a list of all internal standards with intensity below a threshold value + #' + #' @param is_selection_subset: Matrix with intensities for each internal standard in each sample + #' @param thresholds: Threshold values for a given scan mode and matrix + #' @param is_names: Array of names of internal standards for a given scan mode + #' @param scanmode: string indicating scan mode to include in output + #' + #' @return is_below_threshold: Matrix listing all samples for which internal standard intensity is below threshold + + # initialize; get the headers of the matrix + is_below_threshold <- is_selection_subset[0, ] + # for every line, check if intensity is below the appropriate threshold + for (line_index in seq_len(nrow(is_selection_subset))) { + is_selected <- is_selection_subset$HMDB_name[line_index] + thresh_selected <- thresholds[which(is_names == is_selected)] + if (is_selection_subset$Intensity[line_index] < thresh_selected) { + is_below_threshold <- rbind(is_below_threshold, is_selection_subset[line_index, ]) + } + } + # add information on scan mode + is_below_threshold <- cbind(is_below_threshold, scanmode = rep(scanmode, nrow(is_below_threshold))) + return(is_below_threshold) +} + From 22140fb4c4b10471460ecd00f14eb4d354720945 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Thu, 22 Jan 2026 14:08:21 +0100 Subject: [PATCH 109/161] added unit test for function find_is_below_threshold --- DIMS/tests/testthat/test_generate_qc_output.R | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/DIMS/tests/testthat/test_generate_qc_output.R b/DIMS/tests/testthat/test_generate_qc_output.R index 2bb953aa..0586799f 100644 --- a/DIMS/tests/testthat/test_generate_qc_output.R +++ b/DIMS/tests/testthat/test_generate_qc_output.R @@ -201,3 +201,22 @@ testthat::test_that("Check missing mz values", { expect_identical(check_missing_mz(test_mz_pgrp, "Test")$`1`, c(550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560)) }) + +testthat:: testthat("list of internal standards below threshold is correctly created", { + test_is <- read.delim(test_path("fixtures", "test_internal_standards.txt")) + # select columns + test_is_wide <- test_is[ , c("HMDB_code", "HMDB_name", "C101.1", "C102.1", "P2.1", "P3.1")] + # melt into long format + test_is_long <- melt(setDT(test_is_wide), id.vars = c("HMDB_name","HMDB_code")) + colnames(test_is_long)[4] <- "Intensity" + + test_is_names <- c("metab_1 (IS)", "metab_2 (IS)", "metab_3 (IS)", "metab_4 (IS)") + test_thresholds <- rep(300, 4) + + # 8 rows have intensity below threshold + expect_equal(nrow(find_is_below_threshold(test_is_long, test_thresholds, test_is_names, "test")), 8) + # the maximum intensity in the output should be less than threshold + expect_lt(max(find_is_below_threshold(test_is_long, test_thresholds, test_is_names, "test")$Intensity), 300) + # if all values are above threshold, the result should be an empty data table + expect_equal(nrow(find_is_below_threshold(test_is_long, test_thresholds / 3, test_is_names, "test")), 0) +}) From 4501fd95434f1b27f37625795bde42c01af99419 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 23 Jan 2026 11:19:36 +0100 Subject: [PATCH 110/161] removed erroneous space --- DIMS/tests/testthat/test_generate_qc_output.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DIMS/tests/testthat/test_generate_qc_output.R b/DIMS/tests/testthat/test_generate_qc_output.R index 0586799f..b411034e 100644 --- a/DIMS/tests/testthat/test_generate_qc_output.R +++ b/DIMS/tests/testthat/test_generate_qc_output.R @@ -202,7 +202,7 @@ testthat::test_that("Check missing mz values", { c(550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560)) }) -testthat:: testthat("list of internal standards below threshold is correctly created", { +testthat::testthat("list of internal standards below threshold is correctly created", { test_is <- read.delim(test_path("fixtures", "test_internal_standards.txt")) # select columns test_is_wide <- test_is[ , c("HMDB_code", "HMDB_name", "C101.1", "C102.1", "P2.1", "P3.1")] From 5f32d2f620a3869c85038b45b7b213ea811eb8b8 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 23 Jan 2026 11:32:59 +0100 Subject: [PATCH 111/161] corrected typo --- DIMS/tests/testthat/test_generate_qc_output.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DIMS/tests/testthat/test_generate_qc_output.R b/DIMS/tests/testthat/test_generate_qc_output.R index b411034e..81b74c17 100644 --- a/DIMS/tests/testthat/test_generate_qc_output.R +++ b/DIMS/tests/testthat/test_generate_qc_output.R @@ -202,7 +202,7 @@ testthat::test_that("Check missing mz values", { c(550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560)) }) -testthat::testthat("list of internal standards below threshold is correctly created", { +testthat::test_that("list of internal standards below threshold is correctly created", { test_is <- read.delim(test_path("fixtures", "test_internal_standards.txt")) # select columns test_is_wide <- test_is[ , c("HMDB_code", "HMDB_name", "C101.1", "C102.1", "P2.1", "P3.1")] From 534002e8f81d34b9f07654223ee0db6c2dc904ac Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 23 Jan 2026 11:39:12 +0100 Subject: [PATCH 112/161] added library data.table --- DIMS/tests/testthat/test_generate_qc_output.R | 1 + 1 file changed, 1 insertion(+) diff --git a/DIMS/tests/testthat/test_generate_qc_output.R b/DIMS/tests/testthat/test_generate_qc_output.R index 81b74c17..f355a021 100644 --- a/DIMS/tests/testthat/test_generate_qc_output.R +++ b/DIMS/tests/testthat/test_generate_qc_output.R @@ -4,6 +4,7 @@ # get_is_intensities, calc_coefficient_of_variation, # check_missing_mz library(ggplot2) +library(data.table) suppressMessages(library("dplyr")) source("../../export/generate_qc_output_functions.R") From 7c0d0563a19a79f4e67b5f511c3341d1230864fa Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 23 Jan 2026 11:49:55 +0100 Subject: [PATCH 113/161] replaced library data.table with reshape2 --- DIMS/tests/testthat/test_generate_qc_output.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DIMS/tests/testthat/test_generate_qc_output.R b/DIMS/tests/testthat/test_generate_qc_output.R index f355a021..07be0178 100644 --- a/DIMS/tests/testthat/test_generate_qc_output.R +++ b/DIMS/tests/testthat/test_generate_qc_output.R @@ -4,7 +4,7 @@ # get_is_intensities, calc_coefficient_of_variation, # check_missing_mz library(ggplot2) -library(data.table) +library(reshape2) suppressMessages(library("dplyr")) source("../../export/generate_qc_output_functions.R") @@ -208,7 +208,7 @@ testthat::test_that("list of internal standards below threshold is correctly cre # select columns test_is_wide <- test_is[ , c("HMDB_code", "HMDB_name", "C101.1", "C102.1", "P2.1", "P3.1")] # melt into long format - test_is_long <- melt(setDT(test_is_wide), id.vars = c("HMDB_name","HMDB_code")) + test_is_long <- reshape2::melt(test_is_wide, id.vars = c("HMDB_name","HMDB_code")) colnames(test_is_long)[4] <- "Intensity" test_is_names <- c("metab_1 (IS)", "metab_2 (IS)", "metab_3 (IS)", "metab_4 (IS)") From 59423bd49d637514378918ff8c3762ca7817bfe0 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Fri, 23 Jan 2026 14:15:08 +0100 Subject: [PATCH 114/161] Rename variables, move duplicate code to function + unit test --- DIMS/GenerateExcel.R | 84 +++++++++-------------- DIMS/export/generate_excel_functions.R | 71 +++++++++++++++---- DIMS/tests/testthat/test_generate_excel.R | 45 ++++++++++-- 3 files changed, 132 insertions(+), 68 deletions(-) diff --git a/DIMS/GenerateExcel.R b/DIMS/GenerateExcel.R index 48dd7248..51d28cfa 100644 --- a/DIMS/GenerateExcel.R +++ b/DIMS/GenerateExcel.R @@ -60,14 +60,14 @@ outlist <- outlist[order(outlist[, "HMDB_code"]), ] # Create excel sheetname <- "AllPeakGroups" -wb_intensities <- openxlsx::createWorkbook("SinglePatient") -openxlsx::addWorksheet(wb_intensities, sheetname) +wb_intensities_zscores <- openxlsx::createWorkbook("SinglePatient") +openxlsx::addWorksheet(wb_intensities_zscores, sheetname) # Add Z-scores and create plots if (z_score == 1) { dir.create(paste0(outdir, "/plots"), showWarnings = FALSE) - wb_helix_intensities <- openxlsx::createWorkbook("SinglePatient") - openxlsx::addWorksheet(wb_helix_intensities, sheetname) + wb_helix_zscores <- openxlsx::createWorkbook("SinglePatient") + openxlsx::addWorksheet(wb_helix_zscores, sheetname) row_helix <- 2 # start on row 2 because of header # add a column for plots outlist <- cbind(plots = NA, outlist) @@ -148,71 +148,55 @@ if (z_score == 1) { rename(Name = H_Name) # Get intensity columns for controls and patients - intensities_plots_df <- outlist %>% select(HMDB_key, matches("^C|^P[0-9]"), -ends_with("_Zscore")) + intensities_df <- outlist %>% select(HMDB_key, matches("^C|^P[0-9]"), -ends_with("_Zscore")) - for (row_index in seq_len(nrow(intensities_plots_df))) { + for (row_index in seq_len(nrow(intensities_df))) { # get HMDB ID - hmdb_id <- intensities_plots_df %>% + hmdb_id <- intensities_df %>% slice(row_index) %>% pull(HMDB_key) # Transform dataframe to long format - intensities_plots_df_long <- transform_ints_df_plots(intensities_plots_df, row_index) + intensities_df_long <- intensities_df_to_long_format(intensities_df, row_index) # set plot width to 40 times the number of samples - plot_width <- length(unique(intensities_plots_df_long$Samples)) * 40 + plot_width <- length(unique(intensities_df_long$Samples)) * 40 col_width <- plot_width * 2 if (hmdb_id %in% metab_list_helix) { # Make separate plot for Helix Excel containing all samples - plot.new() - tmp_png_helix <- paste0("plots/plot_helix_", hmdb_id, ".png") - png(filename = tmp_png_helix, width = plot_width, height = 300) - boxplot_excel_helix <- create_boxplot_excel(intensities_plots_df_long, hmdb_id) - - print(boxplot_excel_helix) - dev.off() - - openxlsx::insertImage( - wb_helix_intensities, + start_row_index <- row_index + 1 + save_plot_to_excel_workbook( + wb_helix_zscores, sheetname, - tmp_png_helix, - startRow = row_helix, - startCol = 1, - height = 560, - width = col_width, - units = "px" + intensities_df_long, + "plots/plot_helix_", + hmdb_id, + plot_width, + col_width, + row_helix ) row_helix <- row_helix + 1 } # Remove postive controls and SST mix samples, (e.g. P1001, P1002, P1003, P1005) - intensities_plots_df_long <- intensities_plots_df_long %>% filter(!grepl("^P[0-9]{4}$", Samples)) - - plot.new() - tmp_png <- paste0("plots/plot_", hmdb_id, ".png") - png(filename = tmp_png, width = plot_width, height = 300) - - boxplot_excel <- create_boxplot_excel(intensities_plots_df_long, hmdb_id) - - print(boxplot_excel) - dev.off() + intensities_df_long <- intensities_df_long %>% filter(!grepl("^P[0-9]{4}$", Samples)) - # place the plot in the Excel file - openxlsx::insertImage( - wb_intensities, + start_row_index <- row_index + 1 + save_plot_to_excel_workbook( + wb_intensities_zscores, sheetname, - tmp_png, - startRow = row_index + 1, - startCol = 1, - height = 560, - width = col_width, - units = "px" + intensities_df_long, + "plots/plot_", + hmdb_id, + plot_width, + col_width, + start_row_index ) } wb_intensities <- set_row_height_col_width_wb( - wb_intensities, + wb_intensities_zscores, sheetname, nrow(outlist), ncol(outlist), @@ -221,7 +205,7 @@ if (z_score == 1) { ) wb_helix_intensities <- set_row_height_col_width_wb( - wb_helix_intensities, + wb_helix_zscores, sheetname, nrow(outlist_helix), ncol(outlist_helix), @@ -240,7 +224,7 @@ if (z_score == 1) { } else { save(outlist, file = "outlist.RData") wb_intensities <- set_row_height_col_width_wb( - wb_intensities, + wb_intensities_zscores, sheetname, nrow(outlist), ncol(outlist), @@ -252,7 +236,7 @@ if (z_score == 1) { } # write Excel file -openxlsx::writeData(wb_intensities, sheet = 1, outlist, startCol = 1) -openxlsx::saveWorkbook(wb_intensities, paste0(outdir, "/", project, ".xlsx"), overwrite = TRUE) -rm(wb_intensities) +openxlsx::writeData(wb_intensities_zscores, sheet = 1, outlist, startCol = 1) +openxlsx::saveWorkbook(wb_intensities_zscores, paste0(outdir, "/", project, ".xlsx"), overwrite = TRUE) +rm(wb_intensities_zscores) unlink("plots", recursive = TRUE) diff --git a/DIMS/export/generate_excel_functions.R b/DIMS/export/generate_excel_functions.R index ef253a8e..a41c862f 100644 --- a/DIMS/export/generate_excel_functions.R +++ b/DIMS/export/generate_excel_functions.R @@ -82,8 +82,9 @@ robust_scaler <- function(control_intensities, control_col_ids, perc = 5) { #' @return trimmed_control_intensities: Intensities trimmed for outliers nr_to_remove <- ceiling(length(control_col_ids) * perc / 100) sorted_control_intensities <- sort(as.numeric(control_intensities)) - trimmed_control_intensities <- sorted_control_intensities[(nr_to_remove + 1): - (length(sorted_control_intensities) - nr_to_remove)] + start_index <- nr_to_remove + 1 + end_index <- length(sorted_control_intensities) - nr_to_remove + trimmed_control_intensities <- sorted_control_intensities[start_index:end_index] return(trimmed_control_intensities) } @@ -145,9 +146,9 @@ set_row_height_col_width_wb <- function(wb, sheetname, num_rows_df, num_cols_df, #' #' @param intensities_plots_df: a dataframe with HMDB_key column and intensities for all samples #' -#' @returns intensities_plots_df_long: a dataframe with on each row a sample and their intensity -transform_ints_df_plots <- function(intensities_plots_df, row_index) { - intensities_plots_df_long <- intensities_plots_df %>% +#' @returns intensities_df_long: a dataframe with on each row a sample and their intensity +intensities_df_to_long_format <- function(intensities_plots_df, row_index) { + intensities_df_long <- intensities_plots_df %>% slice(row_index) %>% select(-HMDB_key) %>% as.data.frame() %>% @@ -163,22 +164,21 @@ transform_ints_df_plots <- function(intensities_plots_df, row_index) { mutate(group_size = n()) %>% ungroup() - return(intensities_plots_df_long) + return(intensities_df_long) } - #' Create a plot of intensities of samples for Excel #' Use boxplot if group size is above 2, otherwise use a dash/line #' -#' @param intensities_plots_df_long: a dataframe with on each row a sample and their intensity +#' @param intensities_df_long: a dataframe with on each row a sample and their intensity #' @param hmdb_id: HMDB ID of the selected metabolite #' -#' @returns boxplot_excel: ggplot2 object containing the plot of intensities -create_boxplot_excel <- function(intensities_plots_df_long, hmdb_id) { - boxplot_excel <- ggplot(intensities_plots_df_long, aes(Samples, Intensities)) + - geom_boxplot(data = subset(intensities_plots_df_long, group_size > 2), aes(fill = type)) + +#' @returns boxplot_object: ggplot2 object containing the plot of intensities +create_boxplot <- function(intensities_df_long, hmdb_id) { + boxplot_object <- ggplot(intensities_df_long, aes(Samples, Intensities)) + + geom_boxplot(data = subset(intensities_df_long, group_size > 2), aes(fill = type)) + geom_point( - data = subset(intensities_plots_df_long, group_size <= 2), + data = subset(intensities_df_long, group_size <= 2), shape = "-", size = 10, aes(colour = type, fill = type) @@ -195,5 +195,48 @@ create_boxplot_excel <- function(intensities_plots_df_long, hmdb_id) { ) + ggtitle(hmdb_id) - return(boxplot_excel) + return(boxplot_object) +} + +#' Make and save a boxplot of intensities to an Excel workbook +#' +#' For the Helix Excel the positive controls and SST mix samples are removed. +#' +#' @param excel_workbook: an openxlsx Workbook object +#' @param sheetname: a string containing the sheetname where the plots are to be placed +#' @param intensities_df: a dataframe containing intensities for controls and patients of a specific HMDB ID +#' @param file_path: a string containing the filepath for the png +#' @param hmdb_id: a string containing the HMDB ID that the intensities_df contains data for +#' @param plot_width: an integer containing the plot width for the png +#' @param col_width: an integer containing the width of the column that has the plots +#' @param start_row_index: an integer containing the index of the row where the plot has to be placed +save_plot_to_excel_workbook <- function(excel_workbook, + sheetname, + intensities_df, + file_path, + hmdb_id, + plot_width, + col_width, + start_row_index) { + plot.new() + tmp_png <- paste0(file_path, hmdb_id, ".png") + png(filename = tmp_png, width = plot_width, height = 300) + + boxplot <- create_boxplot(intensities_df, hmdb_id) + + print(boxplot) + dev.off() + + openxlsx::insertImage( + excel_workbook, + sheetname, + tmp_png, + startRow = start_row_index, + startCol = 1, + height = 560, + width = col_width, + units = "px" + ) + + return(excel_workbook) } diff --git a/DIMS/tests/testthat/test_generate_excel.R b/DIMS/tests/testthat/test_generate_excel.R index 817bb36f..f9d8945c 100644 --- a/DIMS/tests/testthat/test_generate_excel.R +++ b/DIMS/tests/testthat/test_generate_excel.R @@ -431,8 +431,8 @@ testthat::test_that("transform_ints_df_plots: Check transformation of dataframe ) }) -testthat::test_that("create_boxplot_excel: Create a boxplot for the Excel", { - test_ints_plots_df_long <- data.frame( +testthat::test_that("create_boxplot: Create a boxplot for the Excel", { + test_ints_df_long <- data.frame( Samples = c("C", "C", "C", "C", "C", "P1", "P2", "P3", "P4", "P5"), Intensities = c(150, 250, 225, 300, 175, 325, 600, 150, 350, 275), type = c( @@ -444,10 +444,47 @@ testthat::test_that("create_boxplot_excel: Create a boxplot for the Excel", { test_hmdb_id <- "Test Metab 1" - expect_silent(create_boxplot_excel(test_ints_plots_df_long, test_hmdb_id)) + expect_silent(create_boxplot(test_ints_df_long, test_hmdb_id)) expect_doppelganger( title = "create boxplot excel", - fig = create_boxplot_excel(test_ints_plots_df_long, test_hmdb_id) + fig = create_boxplot(test_ints_df_long, test_hmdb_id) ) }) + +testthat::test_that("save_plot_to_excel_workbook: Make and save a boxplot of intensities to an Excel workbook", { + test_excel_workbook <- openxlsx::createWorkbook("Test") + openxlsx::addWorksheet(test_excel_workbook, "Test_with_plots") + + test_intensities_df_long <- data.frame( + Samples = c("C", "C", "C", "C", "C", "P1", "P2", "P3", "P4", "P5"), + Intensities = c(150, 250, 225, 300, 175, 325, 600, 150, 350, 275), + type = c( + "Control", "Control", "Control", "Control", "Control", + "Patients", "Patients", "Patients", "Patients", "Patients" + ), + group_size = c(5, 5, 5, 5, 5, 1, 1, 1, 1, 1) + ) + + test_hmdb_id <- "Test_metab_1" + test_sheetname <- "Test_with_plots" + test_file_path <- "./plot_test_" + test_plot_width <- length(unique(test_intensities_df_long$Samples)) * 40 + test_col_width <- test_plot_width * 2 + test_start_row_index <- 1 + + expect_silent(save_plot_to_excel_workbook(test_excel_workbook, + test_sheetname, + test_intensities_df_long, + test_file_path, + test_hmdb_id, + test_plot_width, + test_col_width, + test_start_row_index)) + + expect_identical(test_excel_workbook$media$image1.png, "./plot_test_Test_metab_1.png") + + # Remove test png file + unlink(paste0(test_file_path, test_hmdb_id, ".png")) + rm(test_excel_workbook) +}) From 84f317d96572fd6415ede07dae20b22662e3c5c7 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Fri, 23 Jan 2026 14:19:51 +0100 Subject: [PATCH 115/161] Changed function name --- DIMS/tests/testthat/test_generate_excel.R | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/DIMS/tests/testthat/test_generate_excel.R b/DIMS/tests/testthat/test_generate_excel.R index f9d8945c..fda40961 100644 --- a/DIMS/tests/testthat/test_generate_excel.R +++ b/DIMS/tests/testthat/test_generate_excel.R @@ -371,7 +371,7 @@ testthat::test_that("set_row_height_col_width_wb: Check row height and column wi rm(test_wb_no_plots) }) -testthat::test_that("transform_ints_df_plots: Check transformation of dataframe to long format", { +testthat::test_that("intensities_df_to_long_format: Check transformation of dataframe to long format", { test_intensities_plots_df <- data.frame( C101.1 = c(100, 200, 300, 400), C102.1 = c(125, 225, 325, 425), @@ -382,10 +382,10 @@ testthat::test_that("transform_ints_df_plots: Check transformation of dataframe test_row_index <- 1 - expect_equal(dim(transform_ints_df_plots(test_intensities_plots_df, test_row_index)), c(4, 4)) + expect_equal(dim(intensities_df_to_long_format(test_intensities_plots_df, test_row_index)), c(4, 4)) expect_identical( colnames( - transform_ints_df_plots( + intensities_df_to_long_format( test_intensities_plots_df, test_row_index ) @@ -393,28 +393,28 @@ testthat::test_that("transform_ints_df_plots: Check transformation of dataframe c("Samples", "Intensities", "type", "group_size") ) expect_identical( - transform_ints_df_plots( + intensities_df_to_long_format( test_intensities_plots_df, test_row_index )$Samples, c("C", "C", "P2000M00002", "P3000M00003") ) expect_identical( - transform_ints_df_plots( + intensities_df_to_long_format( test_intensities_plots_df, test_row_index )$Intensities, c(100, 125, 150, 175) ) expect_identical( - transform_ints_df_plots( + intensities_df_to_long_format( test_intensities_plots_df, test_row_index )$type, c("Control", "Control", "Patients", "Patients") ) expect_equal( - transform_ints_df_plots( + intensities_df_to_long_format( test_intensities_plots_df, test_row_index )$group_size, @@ -423,7 +423,7 @@ testthat::test_that("transform_ints_df_plots: Check transformation of dataframe test_row_index <- 2 expect_identical( - transform_ints_df_plots( + intensities_df_to_long_format( test_intensities_plots_df, test_row_index )$Intensities, From d4bc6e15e476c5a6e29cdb9ddb7a7339f7aab9c6 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Fri, 23 Jan 2026 14:31:33 +0100 Subject: [PATCH 116/161] Changed variable name --- DIMS/export/generate_excel_functions.R | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DIMS/export/generate_excel_functions.R b/DIMS/export/generate_excel_functions.R index a41c862f..9cf5936e 100644 --- a/DIMS/export/generate_excel_functions.R +++ b/DIMS/export/generate_excel_functions.R @@ -144,11 +144,11 @@ set_row_height_col_width_wb <- function(wb, sheetname, num_rows_df, num_cols_df, #' pivot to long format, arrange Samples nummerically, change Sample names, get group size and #' set Intensities to numeric. #' -#' @param intensities_plots_df: a dataframe with HMDB_key column and intensities for all samples +#' @param intensities_df: a dataframe with HMDB_key column and intensities for all samples #' #' @returns intensities_df_long: a dataframe with on each row a sample and their intensity -intensities_df_to_long_format <- function(intensities_plots_df, row_index) { - intensities_df_long <- intensities_plots_df %>% +intensities_df_to_long_format <- function(intensities_df, row_index) { + intensities_df_long <- intensities_df %>% slice(row_index) %>% select(-HMDB_key) %>% as.data.frame() %>% @@ -237,6 +237,6 @@ save_plot_to_excel_workbook <- function(excel_workbook, width = col_width, units = "px" ) - + return(excel_workbook) } From 276bd0e7ba2cec8efad57d7dff6b627a7581088e Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 10 Feb 2026 16:03:35 +0100 Subject: [PATCH 117/161] refactored DIMS/HMDBparts_main --- DIMS/HMDBparts_main.R | 54 ++++++++++++++++--------------------------- 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/DIMS/HMDBparts_main.R b/DIMS/HMDBparts_main.R index 14335bf1..1a377eb6 100644 --- a/DIMS/HMDBparts_main.R +++ b/DIMS/HMDBparts_main.R @@ -1,5 +1,3 @@ -## adapted from hmdb_part_adductSums.R - # define parameters cmd_args <- commandArgs(trailingOnly = TRUE) @@ -9,7 +7,11 @@ breaks_file <- cmd_args[2] load(db_file) load(breaks_file) -# Cut up HMDB minus adducts minus isotopes into small parts +# get minimum and maximum m/z in dataset +min_mz <- round(breaks_fwhm[1]) +max_mz <- round(breaks_fwhm[length(breaks_fwhm)]) + +# Select HMDB plus adducts and isotopes for each scan mode scanmodes <- c("positive", "negative") for (scanmode in scanmodes) { if (scanmode == "negative") { @@ -20,37 +22,21 @@ for (scanmode in scanmodes) { HMDB_add_iso <- HMDB_add_iso.Pos } - # filter mass range measured - outlist <- HMDB_add_iso[which(HMDB_add_iso[ , column_label] >= breaks_fwhm[1] & - HMDB_add_iso[ ,column_label] <= breaks_fwhm[length(breaks_fwhm)]), ] - + # filter on mass range in dataset + HMDB_mzrange <- HMDB_add_iso[(HMDB_add_iso[, column_label] >= min_mz & HMDB_add_iso[, column_label] <= max_mz), ] # remove adducts and isotopes, put internal standard at the beginning - outlist <- outlist[grep("HMDB", rownames(outlist), fixed = TRUE), ] - outlist <- outlist[-grep("_", rownames(outlist), fixed = TRUE), ] + HMDB_add <- HMDB_mzrange[grep("HMDB", rownames(HMDB_mzrange), fixed = TRUE), ] + HMDB_main <- HMDB_add[-grep("_", rownames(HMDB_add), fixed = TRUE), ] # sort on m/z value - outlist <- outlist[order(outlist[ , column_label]), ] - nr_rows <- dim(outlist)[1] - - # size of hmdb parts in lines: - sub <- 1000 - end <- 0 - check <- 0 - - # generate hmdb parts - if (nr_rows >= sub & (floor(nr_rows / sub)) >= 2) { - for (i in 1:floor(nr_rows / sub)) { - start <- -(sub - 1) + i * sub - end <- i * sub - outlist_part <- outlist[c(start:end), ] - save(outlist_part, file=paste0(scanmode, "_hmdb_main.", i, ".RData")) - } + HMDB_main <- HMDB_main[order(HMDB_main[, column_label]), ] + + # generate hmdb parts of 1000 lines each + nr_parts <- ceiling(nrow(HMDB_main) / 1000) + start_index <- 1 + for (part_index in 1:nr_parts) { + end_index <- min((start_index + 999), nrow(HMDB_main)) + outlist_part <- HMDB_main[start_index:end_index, ] + save(outlist_part, file = paste0(scanmode, "_hmdb_main.", part_index, ".RData")) + start_index = start_index + 1000 } - - # finish last hmdb part - start <- end + 1 - end <- nr_rows - - outlist_part <- outlist[c(start:end), ] - save(outlist_part, file = paste0(scanmode, "_hmdb_main.", i + 1, ".RData")) - -} \ No newline at end of file +} From cbbf4290c7fd6a8a7abe7846036cb51f5b2b3b14 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 13 Feb 2026 14:48:29 +0100 Subject: [PATCH 118/161] added unit tests for CollectFilled --- DIMS/tests/testthat/test_collect_filled.R | 77 +++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 DIMS/tests/testthat/test_collect_filled.R diff --git a/DIMS/tests/testthat/test_collect_filled.R b/DIMS/tests/testthat/test_collect_filled.R new file mode 100644 index 00000000..3927af49 --- /dev/null +++ b/DIMS/tests/testthat/test_collect_filled.R @@ -0,0 +1,77 @@ +# unit tests for CollectFilled +# functions: collapse, merge_duplicate_rows, calculate_zscores_peakgrouplist, +# calculate_ppm_deviation, order_columns_peakgrouplist +source("../../preprocessing/collect_filled_functions.R") + +testthat::test_that("Values for duplicate rows are correctly collapsed", { + test_matrix <- matrix(letters[1:8], nrow = 2, ncol = 4) + colnames(test_matrix) <- paste0("column", 1:4) + + expect_equal(collapse("column1", test_matrix, c(1,2)), "a;b", TRUE) +}) + +testthat::test_that("Duplicate rows in a peak group list are correctly merged", { + # Copy/symlink the files to the current location for the function + test_files <- list.files("fixtures/", "test_peakgroup_list", full.names = TRUE) + file.symlink(file.path(test_files), getwd()) + + # read in peakgroup_list, create duplicate row + test_peakgroup_list <- read.table("./test_peakgroup_list.txt", sep = "\t") + test_peakgroup_list_dup <- test_peakgroup_list[c(1, 2, 2, 3), ] + + # after merging duplicate rows, the test peak group list should have 3 rows + expect_equal(nrow(merge_duplicate_rows(test_peakgroup_list_dup)), 3, TRUE, tolerance = 0.001) + expect_equal(merge_duplicate_rows(test_peakgroup_list_dup)[3, "all_hmdb_ids"], + paste(test_peakgroup_list_dup[2, "all_hmdb_ids"], test_peakgroup_list_dup[3, "all_hmdb_ids"], sep = ";"), + TRUE) +}) + +testthat::test_that("Z-scores are correctly calculated in CollectFilled", { + # read in peakgroup_list; remove avg.int, std.int, noise and Zscore columns + test_peakgroup_list <- read.table("./test_peakgroup_list.txt", sep = "\t") + # remove avg.ctrls, sd.ctrls and Z-score columns + test_peakgroup_list_noz <- test_peakgroup_list[ , -grep("avg.ctrls", colnames(test_peakgroup_list))] + test_peakgroup_list_noz <- test_peakgroup_list_noz[ , -grep("sd.ctrls", colnames(test_peakgroup_list_noz))] + test_peakgroup_list_noz <- test_peakgroup_list_noz[ , -grep("_Zscore", colnames(test_peakgroup_list_noz))] + + # after calculate_zscores_peakgrouplist, there should be 4 columns with _Zscore in the name + expect_equal(length(grep("_Zscore", colnames(calculate_zscores_peakgrouplist(test_peakgroup_list_noz)))), 4, TRUE, tolerance = 0.001) + + # after calculate_zscores_peakgrouplist, the 4 columns with _Zscore in the name should be filled non-zero + expect_equal(calculate_zscores_peakgrouplist(test_peakgroup_list_noz)$C101.1_Zscore[1], -0.7071, TRUE, tolerance = 0.00001) + expect_equal(calculate_zscores_peakgrouplist(test_peakgroup_list_noz)$P2.1_Zscore[4], 12.0208, TRUE, tolerance = 0.00001) + +}) + +testthat::test_that("ppm deviation values are correctly calculated in CollectFilled", { + # read in peakgroup_list + test_peakgroup_list <- read.table("./test_peakgroup_list.txt", sep = "\t") + + # store ppm deviation values + test_ppm_values <- test_peakgroup_list$ppmdev + + # after calculate_ppm_deviation, there ppm values in the new column should approximate the old ones + expect_equal(calculate_ppm_deviation(test_peakgroup_list)$ppmdev, test_ppm_values, TRUE, tolerance = 0.001) + + # calculate_ppm_deviation should give NA if there is no value for theormz_HMDB + test_peakgroup_list$theormz_HMDB[1] <- NA + expect_identical(is.na(calculate_ppm_deviation(test_peakgroup_list)$ppmdev[1]), TRUE) + +}) + +testthat::test_that("columns in peak group list are corretly sorted", { + # read in peakgroup_list + test_peakgroup_list <- read.table("./test_peakgroup_list.txt", sep = "\t") + # original order of columns + original_column_order <- colnames(test_peakgroup_list) + # after ordering, column names should be re-ordered + test_column_order <- original_column_order[c(1, 2, 7:14, 21, 3:6, 15:20)] + + expect_identical(colnames(order_columns_peakgrouplist(test_peakgroup_list)), test_column_order) + + # Remove copied/symlinked files + files_remove <- list.files("./", "test_peakgroup_list.txt", full.names = TRUE) + file.remove(files_remove) + +}) + From 22d8347164fa29e76e2af16a07f96d3fdbe168a3 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 13 Feb 2026 14:51:26 +0100 Subject: [PATCH 119/161] added test_peakgroup_list.txt to fixtures folder for unit tests --- DIMS/tests/testthat/fixtures/test_peakgroup_list.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 DIMS/tests/testthat/fixtures/test_peakgroup_list.txt diff --git a/DIMS/tests/testthat/fixtures/test_peakgroup_list.txt b/DIMS/tests/testthat/fixtures/test_peakgroup_list.txt new file mode 100644 index 00000000..0d83b696 --- /dev/null +++ b/DIMS/tests/testthat/fixtures/test_peakgroup_list.txt @@ -0,0 +1,5 @@ +"mzmed.pgrp" "nrsamples" "C101.1" "C102.1" "P2.1" "P3.1" "assi_HMDB" "all_hmdb_names" "iso_HMDB" "HMDB_code" "all_hmdb_ids" "sec_hmdb_ids" "theormz_HMDB" "avg.int" "avg.ctrls" "sd.ctrls" "C101.1_Zscore" "C102.1_Zscore" "P2.1_Zscore" "P3.1_Zscore" "ppmdev" +"1" 300.199680958642 0.451108327135444 1000 5000 10000 50000 "A" "A;X" NA "HMDB1234567" "HMDB1234567;HMDB1234567" NA 300.1996476 16500 3000 2828.42712474619 9000 13000 90000 130000 0.111112214857712 +"2" 300.000315890415 0.498603057814762 2000 6000 20000 60000 "B" "B;Y" NA "HMDB1234567_1" "HMDB1234567_1;HMDB1234567_1" NA 300.00017417 22000 4000 2828.42712474619 10000 14000 1e+05 140000 0.473299680976197 +"3" 300.254185894039 0.589562055887654 3000 7000 30000 70000 "C" "C;Z" NA "HMDB1234567_2" "HMDB1234567_2;HMDB1234567_2" NA 300.25413357 27500 5000 2828.42712474619 11000 15000 110000 150000 0.17426158930175 +"4" 300.755745105678 0.277923040557653 4000 8000 40000 80000 "D" "D;V" NA "HMDB1234567_7" "HMDB1234567_7;HMDB1234567_7" NA 300.75568892 33000 6000 2828.42712474619 12000 16000 120000 160000 0.186787674436346 From 367a5e5713d3e239186404247b18f04e66ffd98f Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 13 Feb 2026 16:30:38 +0100 Subject: [PATCH 120/161] refactored CollectFilled, code moved to functions --- DIMS/CollectFilled.R | 47 ++---- DIMS/preprocessing/collect_filled_functions.R | 145 ++++++++++++++++++ 2 files changed, 157 insertions(+), 35 deletions(-) mode change 100755 => 100644 DIMS/CollectFilled.R create mode 100644 DIMS/preprocessing/collect_filled_functions.R diff --git a/DIMS/CollectFilled.R b/DIMS/CollectFilled.R old mode 100755 new mode 100644 index 4bd408e0..0a2ff983 --- a/DIMS/CollectFilled.R +++ b/DIMS/CollectFilled.R @@ -1,14 +1,11 @@ -## adapted from 10-collectSamplesFilled.R - # define parameters cmd_args <- commandArgs(trailingOnly = TRUE) -scripts_dir <- cmd_args[1] +preprocessing_scripts_dir <- cmd_args[1] ppm <- as.numeric(cmd_args[2]) z_score <- as.numeric(cmd_args[3]) -source(paste0(scripts_dir, "merge_duplicate_rows.R")) -source(paste0(scripts_dir, "calculate_zscores.R")) +source(paste0(preprocessing_scripts_dir, "collect_filled_functions.R")) # for each scan mode, collect all filled peak group lists scanmodes <- c("positive", "negative") @@ -17,7 +14,7 @@ for (scanmode in scanmodes) { filled_files <- list.files("./", full.names = TRUE, pattern = paste0(scanmode, "_identified_filled")) # load files and combine into one object outlist_total <- NULL - for (file_nr in 1:length(filled_files)) { + for (file_nr in seq_along(filled_files)) { peakgrouplist_filled <- get(load(filled_files[file_nr])) outlist_total <- rbind(outlist_total, peakgrouplist_filled) } @@ -30,37 +27,17 @@ for (scanmode in scanmodes) { repl_pattern <- get(load(pattern_file)) # calculate Z-scores if (z_score == 1) { - outlist_stats <- calculate_zscores(outlist_total, adducts = FALSE) - nr_removed_samples <- length(which(repl_pattern[] == "character(0)")) - order_index_int <- order(colnames(outlist_stats)[8:(length(repl_pattern) - nr_removed_samples + 7)]) - outlist_stats_more <- cbind( - outlist_stats[, 1:7], - outlist_stats[, (length(repl_pattern) - nr_removed_samples + 8):(length(repl_pattern) - nr_removed_samples + 8 + 6)], - outlist_stats[, 8:(length(repl_pattern) - nr_removed_samples + 7)][order_index_int], - outlist_stats[, (length(repl_pattern) - nr_removed_samples + 5 + 10):ncol(outlist_stats)] - ) - # sort Z-score columns and append to peak group list - tmp_index <- grep("_Zscore", colnames(outlist_stats_more), fixed = TRUE) - tmp_index_order <- order(colnames(outlist_stats_more[, tmp_index])) - tmp <- outlist_stats_more[, tmp_index[tmp_index_order]] - outlist_stats_more <- outlist_stats_more[, -tmp_index] - outlist_stats_more <- cbind(outlist_stats_more, tmp) - outlist_total <- outlist_stats_more + outlist_stats <- calculate_zscores_peakgrouplist(outlist_total) } - - # make a copy of the outlist - outlist_ident <- outlist_total - # take care of NAs in theormz_noise - outlist_ident$theormz_noise[which(is.na(outlist_ident$theormz_noise))] <- 0 - outlist_ident$theormz_noise <- as.numeric(outlist_ident$theormz_noise) - outlist_ident$theormz_noise[which(is.na(outlist_ident$theormz_noise))] <- 0 - outlist_ident$theormz_noise <- as.numeric(outlist_ident$theormz_noise) + # calculate ppm deviation + outlist_withppm <- calculate_ppmdeviation(outlist_stats) + # put columns in correct order + outlist_ident <- order_columns_peakgrouplist(outlist_withppm) - # Extra output in Excel-readable format: - remove_columns <- c("fq.best", "fq.worst", "mzmin.pgrp", "mzmax.pgrp") - remove_colindex <- which(colnames(outlist_ident) %in% remove_columns) - outlist_ident <- outlist_ident[, -remove_colindex] + # generate output in Excel-readable format: + remove_columns <- c("mzmin.pgrp", "mzmax.pgrp") + outlist_ident <- outlist_ident[, -which(colnames(outlist_ident) %in% remove_columns)] write.table(outlist_ident, file = paste0("outlist_identified_", scanmode, ".txt"), sep = "\t", row.names = FALSE) - # output in RData format + # generate output in RData format save(outlist_ident, file = paste0("outlist_identified_", scanmode, ".RData")) } diff --git a/DIMS/preprocessing/collect_filled_functions.R b/DIMS/preprocessing/collect_filled_functions.R new file mode 100644 index 00000000..fae06fca --- /dev/null +++ b/DIMS/preprocessing/collect_filled_functions.R @@ -0,0 +1,145 @@ +# CollectFilled functions + +collapse <- function(column_label, peakgroup_list, index_dup) { + #' Collapse identification info for peak groups with the same mass + #' + #' @param column_label: Name of column in peakgroup_list (string) + #' @param peakgroup_list: Peak group list (matrix) + #' @param index_dup: Index of duplicate peak group (integer) + #' + #' @return collapsed_items: Semicolon-separated list of info (string) + # get the item(s) that need to be collapsed + list_items <- as.vector(peakgroup_list[index_dup, column_label]) + # remove NA + if (length(which(is.na(list_items))) > 0) { + list_items <- list_items[-which(is.na(list_items))] + } + collapsed_items <- paste(list_items, collapse = ";") + return(collapsed_items) +} + +merge_duplicate_rows <- function(peakgroup_list) { + #' Merge identification info for peak groups with the same mass + #' + #' @param peakgroup_list: Peak group list (matrix) + #' + #' @return peakgroup_list_dedup: de-duplicated peak group list (matrix) + + options(digits = 16) + collect <- NULL + remove <- NULL + + # check for peak groups with identical mass + index_dup <- which(duplicated(peakgroup_list[, "mzmed.pgrp"])) + + while (length(index_dup) > 0) { + # get the index for the peak group which is double + peaklist_index <- which(peakgroup_list[, "mzmed.pgrp"] == peakgroup_list[index_dup[1], "mzmed.pgrp"]) + single_peakgroup <- peakgroup_list[peaklist_index[1], , drop = FALSE] + + # use function collapse to concatenate info + single_peakgroup[, "assi_HMDB"] <- collapse("assi_HMDB", peakgroup_list, peaklist_index) + single_peakgroup[, "iso_HMDB"] <- collapse("iso_HMDB", peakgroup_list, peaklist_index) + single_peakgroup[, "HMDB_code"] <- collapse("HMDB_code", peakgroup_list, peaklist_index) + single_peakgroup[, "all_hmdb_ids"] <- collapse("all_hmdb_ids", peakgroup_list, peaklist_index) + single_peakgroup[, "sec_hmdb_ids"] <- collapse("sec_hmdb_ids", peakgroup_list, peaklist_index) + if (single_peakgroup[, "sec_hmdb_ids"] == ";") single_peakgroup[, "sec_hmdb_ids"] < NA + + # keep track of deduplicated entries + collect <- rbind(collect, single_peakgroup) + remove <- c(remove, peaklist_index) + + # remove current entry from index + index_dup <- index_dup[-which(peakgroup_list[index_dup, "mzmed.pgrp"] == peakgroup_list[index_dup[1], "mzmed.pgrp"])] + } + + # remove duplicate entries + if (!is.null(remove)) { + peakgroup_list <- peakgroup_list[-remove, ] + } + # append deduplicated entries + peakgroup_list_dedup <- rbind(peakgroup_list, collect) + return(peakgroup_list_dedup) +} + +calculate_zscores_peakgrouplist <- function(peakgroup_list) { + #' Calculate Z-scores for peak groups based on average and standard deviation of controls + #' + #' @param peakgroup_list: Peak group list (matrix) + #' + #' @return peakgroup_list_dedup: de-duplicated peak group list (matrix) + + case_label <- "P" + control_label <- "C" + # get index for new column names + startcol <- ncol(peakgroup_list) + 3 + + # calculate mean and standard deviation for Control group + ctrl_cols <- grep(control_label, colnames(peakgroup_list), fixed = TRUE) + case_cols <- grep(case_label, colnames(peakgroup_list), fixed = TRUE) + int_cols <- c(ctrl_cols, case_cols) + # set all zeros to NA + peakgroup_list[, int_cols][peakgroup_list[, int_cols] == 0] <- NA + ctrl_ints <- peakgroup_list[, ctrl_cols, drop = FALSE] + peakgroup_list$avg.ctrls <- apply(ctrl_ints, 1, function(x) mean(as.numeric(x), na.rm = TRUE)) + peakgroup_list$sd.ctrls <- apply(ctrl_ints, 1, function(x) sd(as.numeric(x), na.rm = TRUE)) + + # set new column names and calculate Z-scores + colnames_zscores <- NULL + for (col_index in int_cols) { + col_name <- colnames(peakgroup_list)[col_index] + colnames_zscores <- c(colnames_zscores, paste0(col_name, "_Zscore")) + zscores_1col <- (as.numeric(as.vector(unlist(peakgroup_list[, col_index]))) - + peakgroup_list$avg.ctrls) / peakgroup_list$sd.ctrls + peakgroup_list <- cbind(peakgroup_list, zscores_1col) + } + + # apply new column names to columns at end plus avg and sd columns + colnames(peakgroup_list)[startcol:ncol(peakgroup_list)] <- colnames_zscores + + return(peakgroup_list) +} + +calculate_ppm_deviation <- function(peakgroup_list) { + #' Calculate ppm deviation between observed mass and expected theoretical mass + #' + #' @param peakgroup_list: Peak group list (matrix) + #' + #' @return peakgroup_list_ppm: peak group list with ppm column (matrix) + + # calculate ppm deviation + for (row_index in seq_len(nrow(peakgroup_list))) { + if (!is.na(peakgroup_list$theormz_HMDB[row_index]) && + !is.null(peakgroup_list$theormz_HMDB[row_index]) && + (peakgroup_list$theormz_HMDB[row_index] != "")) { + peakgroup_list$ppmdev[row_index] <- 10^6 * (as.numeric(as.vector(peakgroup_list$mzmed.pgrp[row_index])) - + as.numeric(as.vector(peakgroup_list$theormz_HMDB[row_index]))) / + as.numeric(as.vector(peakgroup_list$theormz_HMDB[row_index])) + } else { + peakgroup_list$ppmdev[row_index] <- NA + } + } + + return(peakgroup_list) +} + +order_columns_peakgrouplist <- function(peakgroup_list) { + #' Put columns in peak group list in correct order + #' + #' @param peakgroup_list: Peak group list (matrix) + #' + #' @return peakgroup_ordered: peak group list with columns in correct order (matrix) + + original_colnames <- colnames(peakgroup_list) + mass_columns <- c(grep("mzm", original_colnames), grep("nrsamples", original_colnames)) + descriptive_columns <- c(grep("assi_HMDB", original_colnames):grep("avg.int", original_colnames), grep("ppmdev", original_colnames)) + intensity_columns <- c((grep("nrsamples", original_colnames) + 1):(grep("assi_HMDB", original_colnames) - 1)) + # if no Z-scores have been calculated, the following two variables will be empty without consequences for outlist_total + control_columns <- grep ("ctrls", original_colnames) + zscore_columns <- grep("_Zscore", original_colnames) + # create peak group list with columns in correct order + peakgroup_ordered <- peakgroup_list[ , c(mass_columns, descriptive_columns, intensity_columns, control_columns, zscore_columns)] + + return(peakgroup_ordered) +} + From dc8250bc22fd3cc3ed4afe52f2d46598832a9946 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 17 Feb 2026 12:09:06 +0100 Subject: [PATCH 121/161] changes based on code review comments --- DIMS/preprocessing/fill_missing_functions.R | 5 +-- .../testthat/fixtures/test_peakgroup_list.txt | 6 ++++ DIMS/tests/testthat/test_fill_missing.R | 31 +++++++------------ 3 files changed, 20 insertions(+), 22 deletions(-) create mode 100644 DIMS/tests/testthat/fixtures/test_peakgroup_list.txt diff --git a/DIMS/preprocessing/fill_missing_functions.R b/DIMS/preprocessing/fill_missing_functions.R index e1c455d5..23e236ee 100644 --- a/DIMS/preprocessing/fill_missing_functions.R +++ b/DIMS/preprocessing/fill_missing_functions.R @@ -1,14 +1,15 @@ -fill_missing_intensities <- function(peakgroup_list, repl_pattern, thresh, not_random = FALSE) { +fill_missing_intensities <- function(peakgroup_list, repl_pattern, thresh, disable_randomness = FALSE) { #' Replace intensities that are zero with random value #' #' @param peakgroup_list: Peak groups (matrix) #' @param repl_pattern: Replication pattern (list of strings) #' @param thresh: Value for threshold between noise and signal (integer) + #' @param thresh: Variable which indicates whether randomness should be disabled (boolean) #' #' @return final_outlist: peak groups with filled-in intensities (matrix) # for unit test, turn off randomness - if (not_random) { + if (disable_randomness) { set.seed(123) } diff --git a/DIMS/tests/testthat/fixtures/test_peakgroup_list.txt b/DIMS/tests/testthat/fixtures/test_peakgroup_list.txt new file mode 100644 index 00000000..ca96e60e --- /dev/null +++ b/DIMS/tests/testthat/fixtures/test_peakgroup_list.txt @@ -0,0 +1,6 @@ +"mzmed.pgrp" "nrsamples" "ppmdev" "assi_HMDB" "all_hmdb_names" "iso_HMDB" "HMDB_code" "all_hmdb_ids" "sec_hmdb_ids" "theormz_HMDB" "C101.1" "C102.1" "P2.1" "P3.1" "avg.int" "assi_noise" "theormz_noise" "avg.ctrls" "sd.ctrls" "C101.1_Zscore" "C102.1_Zscore" "P2.1_Zscore" "P3.1_Zscore" +"1" 300.199680958642 0.451108327135444 0.111112214857712 NA "NA;NA" NA "HMDB1234567" "HMDB1234567;HMDB1234567" NA NA 1000 5000 10000 50000 NA NA NA NA NA 9000 13000 90000 130000 +"2" 300.000315890415 0.498603057814762 0.473299680976197 NA "NA;NA" NA "HMDB1234567_1" "HMDB1234567_1;HMDB1234567_1" NA NA 2000 6000 20000 60000 NA NA NA NA NA 10000 14000 1e+05 140000 +"3" 300.254185894039 0.589562055887654 0.17426158930175 NA "NA;NA" NA "HMDB1234567_2" "HMDB1234567_2;HMDB1234567_2" NA NA 3000 7000 30000 70000 NA NA NA NA NA 11000 15000 110000 150000 +"4" 300.755745105678 0.277923040557653 0.186787674436346 NA "NA;NA" NA "HMDB1234567_7" "HMDB1234567_7;HMDB1234567_7" NA NA 4000 8000 40000 80000 NA NA NA NA NA 12000 16000 120000 160000 + diff --git a/DIMS/tests/testthat/test_fill_missing.R b/DIMS/tests/testthat/test_fill_missing.R index 1926b632..48452d35 100644 --- a/DIMS/tests/testthat/test_fill_missing.R +++ b/DIMS/tests/testthat/test_fill_missing.R @@ -4,24 +4,18 @@ source("../../preprocessing/fill_missing_functions.R") # test fill_missing_intensities testthat::test_that("missing values are corretly filled with random values", { - # create peakgroup_list to test on in diagnostics setting - test_peakgroup_list <- data.frame(matrix(NA, nrow = 4, ncol = 23)) - colnames(test_peakgroup_list) <- c("mzmed.pgrp", "nrsamples", "ppmdev", "assi_HMDB", "all_hmdb_names", - "iso_HMDB", "HMDB_code", "all_hmdb_ids", "sec_hmdb_ids", "theormz_HMDB", - "C101.1", "C102.1", "P2.1", "P3.1", - "avg.int", "assi_noise", "theormz_noise", "avg.ctrls", "sd.ctrls", - "C101.1_Zscore", "C102.1_Zscore", "P2.1_Zscore", "P3.1_Zscore") - test_peakgroup_list[, c(1)] <- 300 + runif(4) - test_peakgroup_list[, c(2, 3)] <- runif(8) - test_peakgroup_list[, "HMDB_code"] <- c("HMDB1234567", "HMDB1234567_1", "HMDB1234567_2", "HMDB1234567_7") - test_peakgroup_list[, "all_hmdb_ids"] <- paste(test_peakgroup_list[, "HMDB_code"], - test_peakgroup_list[, "HMDB_code"], sep = ";") - test_peakgroup_list[, "all_hmdb_names"] <- paste(test_peakgroup_list[, "assi_HMDB"], - test_peakgroup_list[, "assi_HMDB"], sep = ";") - test_peakgroup_list[, grep("C", colnames(test_peakgroup_list))] <- 1000 * (1:16) - test_peakgroup_list[, grep("P", colnames(test_peakgroup_list))] <- 0 + # It's necessary to copy/symlink the files to the current location for the combine_sum_adducts_parts function + test_files <- list.files("fixtures/", "test_peakgroup_list", full.names = TRUE) + file.symlink(file.path(test_files), getwd()) + + # create replication pattern of technical replicates test_repl_pattern <- c(list(1), list(2), list(3), list(4)) names(test_repl_pattern) <- c("C101.1", "C102.1", "P2.1", "P3.1") + + # read in peakgroup_list, set intensities for patient columns to zero + test_peakgroup_list <- read.table("./test_peakgroup_list.txt", sep= "\t") + test_peakgroup_list[, grep("P", colnames(test_peakgroup_list))] <- 0 + test_thresh <- 2000 # create a large peak group list to test for negative values @@ -35,11 +29,8 @@ testthat::test_that("missing values are corretly filled with random values", { expect_equal(round(fill_missing_intensities(test_peakgroup_list, test_repl_pattern, test_thresh, not_random = TRUE)$P2.1), c(1944, 1977, 2156, 2007), TRUE, tolerance = 0.1) # fill_missing_intensities should not produce any negative values, even if a large quantity of numbers are filled in - start.time <- Sys.time() expect_gt(min(fill_missing_intensities(test_large_peakgroup_list, test_repl_pattern, test_thresh, not_random = FALSE)$P3.1), 0, TRUE) - end.time <- Sys.time() - time.taken <- end.time - start.time - time.taken }) + From 92350654432b4f737781de189056587eaa2e4125 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 17 Feb 2026 12:44:46 +0100 Subject: [PATCH 122/161] replaced parameter not_random with disable_randomness --- DIMS/tests/testthat/test_fill_missing.R | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/DIMS/tests/testthat/test_fill_missing.R b/DIMS/tests/testthat/test_fill_missing.R index 48452d35..b5afcb4e 100644 --- a/DIMS/tests/testthat/test_fill_missing.R +++ b/DIMS/tests/testthat/test_fill_missing.R @@ -4,18 +4,18 @@ source("../../preprocessing/fill_missing_functions.R") # test fill_missing_intensities testthat::test_that("missing values are corretly filled with random values", { - # It's necessary to copy/symlink the files to the current location for the combine_sum_adducts_parts function + # It's necessary to copy/symlink the files to the current location for the fill_missing_intensities function test_files <- list.files("fixtures/", "test_peakgroup_list", full.names = TRUE) file.symlink(file.path(test_files), getwd()) - + # create replication pattern of technical replicates test_repl_pattern <- c(list(1), list(2), list(3), list(4)) names(test_repl_pattern) <- c("C101.1", "C102.1", "P2.1", "P3.1") - + # read in peakgroup_list, set intensities for patient columns to zero test_peakgroup_list <- read.table("./test_peakgroup_list.txt", sep= "\t") test_peakgroup_list[, grep("P", colnames(test_peakgroup_list))] <- 0 - + test_thresh <- 2000 # create a large peak group list to test for negative values @@ -26,10 +26,10 @@ testthat::test_that("missing values are corretly filled with random values", { # for the sake of time, leave only one intensity column with zeros test_large_peakgroup_list$P2.1 <- 1 - expect_equal(round(fill_missing_intensities(test_peakgroup_list, test_repl_pattern, test_thresh, not_random = TRUE)$P2.1), + expect_equal(round(fill_missing_intensities(test_peakgroup_list, test_repl_pattern, test_thresh, disable_randomness = TRUE)$P2.1), c(1944, 1977, 2156, 2007), TRUE, tolerance = 0.1) # fill_missing_intensities should not produce any negative values, even if a large quantity of numbers are filled in - expect_gt(min(fill_missing_intensities(test_large_peakgroup_list, test_repl_pattern, test_thresh, not_random = FALSE)$P3.1), + expect_gt(min(fill_missing_intensities(test_large_peakgroup_list, test_repl_pattern, test_thresh, disable_randomness = FALSE)$P3.1), 0, TRUE) }) From ebdff9e239017fd7e2167d5389192d7a2594e561 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 20 Feb 2026 16:49:37 +0100 Subject: [PATCH 123/161] added source functions for EvaluateTics --- DIMS/EvaluateTics.R | 30 ++++++------------------------ DIMS/EvaluateTics.nf | 3 ++- 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/DIMS/EvaluateTics.R b/DIMS/EvaluateTics.R index 15989868..37e672a4 100644 --- a/DIMS/EvaluateTics.R +++ b/DIMS/EvaluateTics.R @@ -13,6 +13,10 @@ highest_mz_file <- cmd_args[5] highest_mz <- get(load(highest_mz_file)) trim_params_filepath <- cmd_args[6] thresh2remove <- 1000000000 +preprocessing_scripts_dir <- cmd_args[7] + +# load functions +source(paste0(preprocessing_scripts_dir, "evaluate_tics_functions.R")) # load init_file: contains repl_pattern load(init_file) @@ -41,35 +45,11 @@ remove_neg <- remove_tech_reps$neg repl_pattern_filtered <- remove_from_repl_pattern(remove_neg, repl_pattern, nr_replicates) save(repl_pattern_filtered, file = "negative_repl_pattern.RData") -# write output for QC info on missed infusions -if (is.null(remove_neg)) { - remove_neg <- "none" -} -write.table( - remove_neg, - file = "miss_infusions_negative.txt", - row.names = FALSE, - col.names = FALSE, - sep = "\t" -) - # positive scan mode remove_pos <- remove_tech_reps$pos repl_pattern_filtered <- remove_from_repl_pattern(remove_pos, repl_pattern, nr_replicates) save(repl_pattern_filtered, file = "positive_repl_pattern.RData") -# write output for QC info on missed infusions -if (is.null(remove_pos)) { - remove_pos <- "none" -} -write.table( - remove_pos, - file = "miss_infusions_positive.txt", - row.names = FALSE, - col.names = FALSE, - sep = "\t" -) - # get an overview of suitable technical replicates for both scan modes allsamples_techreps_neg <- get_overview_tech_reps(repl_pattern_filtered, "negative") allsamples_techreps_pos <- get_overview_tech_reps(repl_pattern_filtered, "positive") @@ -81,6 +61,7 @@ write.table(allsamples_techreps_both_scanmodes, sep = "," ) + ## generate TIC plots # get all txt files tic_files <- list.files("./", full.names = TRUE, pattern = "*TIC.txt") @@ -158,3 +139,4 @@ tic_plot_pdf <- marrangeGrob( ggsave(filename = paste0(run_name, "_TICplots.pdf"), tic_plot_pdf, width = 21, height = 29.7, units = "cm") + diff --git a/DIMS/EvaluateTics.nf b/DIMS/EvaluateTics.nf index fdf7f713..2cd8bb57 100644 --- a/DIMS/EvaluateTics.nf +++ b/DIMS/EvaluateTics.nf @@ -26,7 +26,8 @@ process EvaluateTics { $analysis_id \ $params.matrix \ $highest_mz_file \ - $trim_params_file + $trim_params_file \ + $params.preprocessing_scripts_dir """ } From 0a80791fa64977b1bd49f6cad63d2b8f704782ea Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Mon, 23 Feb 2026 10:05:51 +0100 Subject: [PATCH 124/161] removed obsolete parameter from calculate_zscores function --- DIMS/CollectFilled.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DIMS/CollectFilled.R b/DIMS/CollectFilled.R index 69f9e04c..726fb605 100755 --- a/DIMS/CollectFilled.R +++ b/DIMS/CollectFilled.R @@ -27,7 +27,7 @@ for (scanmode in scanmodes) { repl_pattern <- get(load(pattern_file)) # calculate Z-scores if (z_score == 1) { - outlist_stats <- calculate_zscores(outlist_total, adducts = FALSE) + outlist_stats <- calculate_zscores(outlist_total) nr_removed_samples <- length(which(repl_pattern[] == "character(0)")) order_index_int <- order(colnames(outlist_stats)[8:(length(repl_pattern) - nr_removed_samples + 7)]) outlist_stats_more <- cbind( From d197b0352623e842aace95ee3c8efe30907b6660 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Mon, 23 Feb 2026 10:06:59 +0100 Subject: [PATCH 125/161] fixed error in arrange by converting table to data frame --- DIMS/preprocessing/peak_finding_functions.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DIMS/preprocessing/peak_finding_functions.R b/DIMS/preprocessing/peak_finding_functions.R index 8539c03c..d4e825d5 100644 --- a/DIMS/preprocessing/peak_finding_functions.R +++ b/DIMS/preprocessing/peak_finding_functions.R @@ -83,7 +83,7 @@ search_regions_of_interest <- function(ints_fullrange) { # sort on first index if (nrow(regions_of_interest_final) > 1){ - regions_of_interest_sorted <- regions_of_interest_final %>% dplyr::arrange(from) + regions_of_interest_sorted <- regions_of_interest_final %>% as.data.frame %>% dplyr::arrange(from) } else { regions_of_interest_sorted <- regions_of_interest_final } From 6c549a7ff164db42eb972f8df9df1eddfb9701e2 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Mon, 23 Feb 2026 10:19:37 +0100 Subject: [PATCH 126/161] removed obsolete step SpectrumPeakFinding --- DIMS/SpectrumPeakFinding.R | 61 ------------------------------------- DIMS/SpectrumPeakFinding.nf | 18 ----------- 2 files changed, 79 deletions(-) delete mode 100644 DIMS/SpectrumPeakFinding.R delete mode 100644 DIMS/SpectrumPeakFinding.nf diff --git a/DIMS/SpectrumPeakFinding.R b/DIMS/SpectrumPeakFinding.R deleted file mode 100644 index 8b249e27..00000000 --- a/DIMS/SpectrumPeakFinding.R +++ /dev/null @@ -1,61 +0,0 @@ -## adapted from 5-collectSamples.R - -# define parameters -scanmodes <- c("positive", "negative") - -# Check whether all jobs terminated correctly -not_run <- NULL - -# collect spectrum peaks for each scanmode -for (scanmode in scanmodes) { - # load peak lists of all biological samples - peaklist_files <- list.files(pattern = paste0("_", scanmode, ".RData")) - - # get sample names - load(paste0(scanmode, "_repl_pattern.RData")) - group_names <- names(repl_pattern_filtered) - for (sample_nr in 1:length(group_names)) { - group <- paste0(group_names[sample_nr], "_", scanmode, ".RData") - if (!(group %in% peaklist_files)) { - not_run <- c(not_run, group) - } - } - - # Collecting samples - outlist_total <- NULL - for (file_nr in 1:length(peaklist_files)) { - cat("\n", peaklist_files[file_nr]) - load(peaklist_files[file_nr]) - if (is.null(outlist_persample) || (dim(outlist_persample)[1] == 0)) { - tmp <- strsplit(peaklist_files[file_nr], "/")[[1]] - fname <- tmp[length(tmp)] - fname <- strsplit(fname, ".RData")[[1]][1] - fname <- substr(fname, 13, nchar(fname)) - if (file_nr == 1) { - outlist_total <- c(fname, rep("-1", 5)) - } else { - outlist_total <- rbind(outlist_total, c(fname, rep("-1", 5))) - } - } else { - if (file_nr == 1) { - outlist_total <- outlist_persample - } else { - outlist_total <- rbind(outlist_total, outlist_persample) - } - } - } - - # remove negative values - index <- which(outlist_total[, "height.pkt"] <= 0) - if (length(index) > 0) outlist_total <- outlist_total[-index, ] - index <- which(outlist_total[, "mzmed.pkt"] <= 0) - if (length(index) > 0) outlist_total <- outlist_total[-index, ] - - save(outlist_total, file = paste0("./SpectrumPeaks_", scanmode, ".RData")) - - if (!is.null(not_run)) { - for (i in 1:length(not_run)) { - message(paste(not_run[i], "was not generated")) - } - } -} diff --git a/DIMS/SpectrumPeakFinding.nf b/DIMS/SpectrumPeakFinding.nf deleted file mode 100644 index 35902658..00000000 --- a/DIMS/SpectrumPeakFinding.nf +++ /dev/null @@ -1,18 +0,0 @@ -process SpectrumPeakFinding { - tag "DIMS SpectrumPeakFinding" - label 'SpectrumPeakFinding' - container = 'docker://umcugenbioinf/dims:1.3' - shell = ['/bin/bash', '-euo', 'pipefail'] - - input: - path(rdata_files) - path(replication_pattern) - - output: - path 'SpectrumPeaks_*.RData' - - script: - """ - Rscript ${baseDir}/CustomModules/DIMS/SpectrumPeakFinding.R - """ -} From 01d76f92b1643f11bb2f9280be521fcc728d8404 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 26 Feb 2026 09:50:23 +0100 Subject: [PATCH 127/161] Refactor functions --- DIMS/GenerateExcel.R | 4 +- DIMS/GenerateViolinPlots.R | 225 ++---- DIMS/export/generate_violin_plots_functions.R | 717 ++++++++++++++---- 3 files changed, 615 insertions(+), 331 deletions(-) diff --git a/DIMS/GenerateExcel.R b/DIMS/GenerateExcel.R index 53662649..efb83600 100644 --- a/DIMS/GenerateExcel.R +++ b/DIMS/GenerateExcel.R @@ -45,7 +45,7 @@ peaks_in_list <- which(rownames(outlist) %in% rlvnc$HMDB_key) outlist_subset <- outlist[peaks_in_list, ] outlist_subset$HMDB_key <- rownames(outlist_subset) outlist <- outlist_subset %>% - left_join(rlvnc %>% rename(sec_HMBD_ID_rlvnc = sec_HMDB_ID), by = "HMDB_key") + left_join(rlvnc %>% rename(sec_HMDB_ID_rlvnc = sec_HMDB_ID), by = "HMDB_key") rownames(outlist) <- outlist$HMDB_key # filter out all irrelevant HMDBs @@ -144,7 +144,7 @@ if (z_score == 1) { filter(HMDB_key %in% metab_list_helix) %>% left_join(., metab_df_helix, by = join_by(HMDB_code == HMDB_code)) %>% select( - -c(HMDB_key, sec_HMBD_ID_rlvnc, name, relevance, descr, origin, fluids, tissue, disease, pathway), + -c(HMDB_key, sec_HMDB_ID_rlvnc, name, relevance, descr, origin, fluids, tissue, disease, pathway), -all_of(control_col_idx), -all_of(patient_col_idx) ) %>% relocate(c(HMDB_code, H_Name, avg_ctrls, sd_ctrls), .after = plots) %>% diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 2f32e3bc..2b742be0 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -6,6 +6,8 @@ library(ggplot2) suppressPackageStartupMessages(library("gridExtra")) library(stringr) +options(digits = 16) + # define parameters cmd_args <- commandArgs(trailingOnly = TRUE) @@ -20,190 +22,77 @@ file_explanation <- cmd_args[6] source(paste0(export_scripts_dir, "generate_violin_plots_functions.R")) # load dataframe with intensities and Z-scores for all samples intensities_zscore_df <- get(load("outlist.RData")) +rm(outlist) # read input files -ratios_metabs_df <- read.csv(file_ratios_metabolites, sep = ";", stringsAsFactors = FALSE) +metabolites_ratios_df <- read.csv(file_ratios_metabolites, sep = ";", stringsAsFactors = FALSE) expected_biomarkers_df <- read.csv(file_expected_biomarkers_iem, sep = ";", stringsAsFactors = FALSE) +expected_biomarkers_df <- expected_biomarkers_df %>% + rename(HMDB_code = HMDB.code, + HMDB_name = Metabolite) explanation_violin_plot <- readLines(file_explanation) - -## Set global variables -output_dir <- "./" # path: output folder for dIEM and violin plots -top_number_iem_diseases <- 5 # number of diseases that score highest in algorithm to plot -threshold_iem <- 5 # probability score cut-off for plotting the top diseases -ratios_cutoff <- -5 # z-score cutoff of axis on the left for top diseases -nr_plots_perpage <- 20 # number of violin plots per page in PDF +# Set global variables +top_number_iem_diseases <- 5 # number of diseases that score highest in algorithm to plot +threshold_iem <- 5 # probability score cut-off for plotting the top diseases +nr_plots_perpage <- 20 # number of violin plots per page in PDF zscore_cutoff <- 5 -xaxis_cutoff <- 20 protocol_name <- "DIMS_PL_DIAG" - -# Remove columns, move HMDB_code & HMDB_name column to the front, change intensity columns to numeric -intensities_zscore_df <- intensities_zscore_df %>% - select(-c(plots, HMDB_name_all, HMDB_ID_all, sec_HMDB_ID, HMDB_key, sec_HMBD_ID_rlvnc, name, - relevance, descr, origin, fluids, tissue, disease, pathway, nr_ctrls)) %>% - relocate(c(HMDB_code, HMDB_name)) %>% - rename(mean_controls = avg_ctrls, sd_controls = sd_ctrls) %>% - mutate(across(!c(HMDB_name, HMDB_code), as.numeric)) - -# Get the controls and patient IDs, select the intensity columns -controls <- colnames(intensities_zscore_df)[grepl("^C", colnames(intensities_zscore_df)) & - !grepl("_Zscore$", colnames(intensities_zscore_df))] -control_intensities_cols_index <- which(colnames(intensities_zscore_df) %in% controls) -nr_of_controls <- length(controls) - -patients <- colnames(intensities_zscore_df)[grepl("^P", colnames(intensities_zscore_df)) & - !grepl("_Zscore$", colnames(intensities_zscore_df))] -patient_intensities_cols_index <- which(colnames(intensities_zscore_df) %in% patients) -nr_of_patients <- length(patients) - -intensity_cols_index <- c(control_intensities_cols_index, patient_intensities_cols_index) -intensity_cols <- colnames(intensities_zscore_df)[intensity_cols_index] - -#### Calculate ratios of intensities for metabolites #### -# Prepare empty data frame to fill with ratios -ratio_zscore_df <- data.frame(matrix( - ncol = ncol(intensities_zscore_df), - nrow = nrow(ratios_metabs_df) -)) -colnames(ratio_zscore_df) <- colnames(intensities_zscore_df) - -# put HMDB info into first two columns of ratio_zscore_df -ratio_zscore_df$HMDB_code <- ratios_metabs_df$HMDB.code -ratio_zscore_df$HMDB_name <- ratios_metabs_df$Ratio_name - -for (row_index in seq_len(nrow(ratios_metabs_df))) { - numerator_intensities <- get_intentities_for_ratios(ratios_metabs_df, row_index, - intensities_zscore_df, "HMDB_numerator", intensity_cols) - denominator_intensities <- get_intentities_for_ratios(ratios_metabs_df, row_index, - intensities_zscore_df, "HMDB_denominator", intensity_cols) - # calculate intensity ratios - ratio_zscore_df[row_index, intensity_cols_index] <- log2(numerator_intensities / denominator_intensities) -} -# Calculate means and SD's of the calculated ratios for Controls -ratio_zscore_df[, "mean_controls"] <- apply(ratio_zscore_df[, control_intensities_cols_index], 1, mean) -ratio_zscore_df[, "sd_controls"] <- apply(ratio_zscore_df[, control_intensities_cols_index], 1, sd) - -# Calculate Zscores for the ratios -samples_zscore_columns <- get_zscore_columns(colnames(intensities_zscore_df), intensity_cols) -ratio_zscore_df[, samples_zscore_columns] <- (ratio_zscore_df[, intensity_cols] - ratio_zscore_df[, "mean_controls"]) / - ratio_zscore_df[, "sd_controls"] - -intensities_zscore_ratios_df <- rbind(intensities_zscore_df, ratio_zscore_df) - +number_of_metabolites <- list( + highest = 20, + lowest = 10 +) + +control_ids <- get_colnames_samples(intensities_zscore_df, "C") +patient_ids <- get_colnames_samples(intensities_zscore_df, "P") +all_sample_ids <- c(control_ids, patient_ids) +number_of_samples <- list( + controls = length(control_ids), + patients = length(patient_ids) +) + +# Add Z-scores for ratios to intensities_zscore_df dataframe +intensities_zscore_ratios_df <- add_zscores_ratios_to_df(intensities_zscore_df, metabolites_ratios_df, all_sample_ids) # for debugging: -save(intensities_zscore_ratios_df, file = paste0(output_dir, "/outlist_with_ratios.RData")) +save(intensities_zscore_ratios_df, file = "./outlist_with_ratios.RData") # Select only the cols with zscores of the patients -zscore_patients_df <- intensities_zscore_ratios_df %>% select(HMDB_code, HMDB_name, any_of(paste0(patients, "_Zscore"))) -zscore_controls_df <- intensities_zscore_ratios_df %>% select(HMDB_code, HMDB_name, any_of(paste0(controls, "_Zscore"))) +zscore_patients_df <- intensities_zscore_ratios_df %>% + select(HMDB_code, HMDB_name, any_of(paste0(patient_ids, "_Zscore"))) %>% + rename_with(~ str_remove(.x, "_Zscore"), .cols = contains("_Zscore")) +zscore_controls_df <- intensities_zscore_ratios_df %>% + select(HMDB_code, HMDB_name, any_of(paste0(control_ids, "_Zscore"))) %>% + rename_with(~ str_remove(.x, "_Zscore"), .cols = contains("_Zscore")) #### Make violin plots ##### -# preparation -colnames(zscore_patients_df) <- gsub("_Zscore", "", colnames(zscore_patients_df)) -colnames(zscore_controls_df) <- gsub("_Zscore", "", colnames(zscore_controls_df)) - -expected_biomarkers_df <- expected_biomarkers_df %>% rename(HMDB_code = HMDB.code, HMDB_name = Metabolite) - -expected_biomarkers_info <- expected_biomarkers_df %>% - select(c(Disease, HMDB_code, HMDB_name)) %>% - distinct(Disease, HMDB_code, .keep_all = TRUE) - -metabolite_dirs <- list.files(path = path_metabolite_groups, full.names = FALSE, recursive = FALSE) -for (metabolite_dir in metabolite_dirs) { - # create a directory for the output PDFs - pdf_dir <- paste(output_dir, metabolite_dir, sep = "/") - dir.create(pdf_dir, showWarnings = FALSE) - - metab_list_all <- get_list_metabolites(paste(path_metabolite_groups, metabolite_dir, sep = "/")) - - # prepare list of metabolites; max nr_plots_perpage on one page - metab_interest_sorted <- combine_metab_info_zscores(metab_list_all, zscore_patients_df) - metab_interest_controls <- combine_metab_info_zscores(metab_list_all, zscore_controls_df) - metab_perpage <- prepare_data_perpage(metab_interest_sorted, metab_interest_controls, - nr_plots_perpage, nr_of_patients, nr_of_controls) - - # for Diagnostics metabolites to be saved in Helix - if (grepl("Diagnost", pdf_dir)) { - # get table that combines DIMS results with stofgroepen/Helix table - dims_helix_table <- get_patient_data_to_helix(metab_interest_sorted, metab_list_all) - - # check if run contains Diagnostics patients (e.g. "P2024M"), not for research runs - if (any(is_diagnostic_patient(dims_helix_table$Sample))) { - # get output file for Helix - output_helix <- output_for_helix(protocol_name, dims_helix_table) - # write output to file - path_helixfile <- paste0(output_dir, "output_Helix_", run_name, ".csv") - write.csv(output_helix, path_helixfile, quote = FALSE, row.names = FALSE) - } - } - - # make violin plots per patient - for (patient_id in patients) { - # for category Diagnostics, make list of metabolites that exceed alarm values for this patient - # for category Other, make list of top highest and lowest Z-scores for this patient - if (grepl("Diagnost", pdf_dir)) { - top_metabs_patient <- prepare_alarmvalues(patient_id, dims_helix_table) - } else { - top_metabs_patient <- prepare_toplist(patient_id, zscore_patients_df) - } - - # generate normal violin plots - create_pdf_violin_plots(pdf_dir, patient_id, metab_perpage, top_metabs_patient, explanation_violin_plot) - } - -} +make_and_save_violin_plot_pdfs( + zscore_patients_df, + zscore_controls_df, + path_metabolite_groups, + nr_plots_perpage, + number_of_samples, + run_name, + protocol_name, + explanation_violin_plot, + number_of_metabolites +) #### Run the IEM algorithm ######### -diem_probability_score <- run_diem_algorithm(expected_biomarkers_df, zscore_patients_df, patients) - -save_prob_scores_to_excel(diem_probability_score, output_dir, run_name) +diem_probability_score <- run_diem_algorithm(expected_biomarkers_df, zscore_patients_df, patient_col_names) +save_prob_scores_to_excel(diem_probability_score, run_name) #### Generate dIEM plots ######### -diem_plot_dir <- paste(output_dir, "dIEM_plots", sep = "/") -dir.create(diem_plot_dir) - -colnames(diem_probability_score) <- gsub("_Zscore", "", colnames(diem_probability_score)) -patient_no_iem <- c() - -for (patient_id in patients) { - # Select the top IEMs and filter on the IEM threshold - patient_top_iems_probs <- diem_probability_score %>% - select(c(Disease, !!sym(patient_id))) %>% - arrange(desc(!!sym(patient_id))) %>% - slice(1:top_number_iem_diseases) %>% - filter(!!sym(patient_id) >= threshold_iem) - - if (nrow(patient_top_iems_probs) > 0) { - top_iems <- patient_top_iems_probs %>% pull(Disease) - # Get the metabolites for each IEM and their probability - metabs_iems <- list() - metabs_iems_names <- c() - for (iem in top_iems) { - iem_probablity <- patient_top_iems_probs %>% filter(Disease == iem) %>% pull(!!sym(patient_id)) - metabs_iems_names <- c(metabs_iems_names, paste0(iem, ", probability score ", iem_probablity)) - metab_iem_df <- expected_biomarkers_df %>% filter(Disease == iem) %>% select(HMDB_code, HMDB_name) - metabs_iems[[iem]] <- metab_iem_df - } - # Get the Z-scores with metabolite information - metab_iem_sorted <- combine_metab_info_zscores(metabs_iems, zscore_patients_df) - metab_iem_controls <- combine_metab_info_zscores(metabs_iems, zscore_controls_df) - # Get a list of dataframes for each IEM - diem_metab_perpage <- prepare_data_perpage(metab_iem_sorted, metab_iem_controls, - nr_plots_perpage, nr_of_patients, nr_of_controls) - # Get a dataframe of the top metabolites - top_metabs_patient <- prepare_toplist(patient_id, zscore_patients_df) - - # Generate and save dIEM violin plots - create_pdf_violin_plots(diem_plot_dir, patient_id, diem_metab_perpage, top_metabs_patient, explanation_violin_plot) - - } else { - patient_no_iem <- c(patient_no_iem, patient_id) - } -} +patient_no_iem <- make_and_save_diem_plots( + diem_probability_score, + patient_ids, + expected_biomarkers_df, + zscore_patients_df, + zscore_controls_df, + nr_plots_perpage, + number_of_samples, + number_of_metabolites +) if (length(patient_no_iem) > 0) { - patient_no_iem <- c(paste0("The following patient(s) did not have dIEM probability scores higher than ", - threshold_iem, " :"), - patient_no_iem) - write(file = paste0(output_dir, "missing_probability_scores.txt"), patient_no_iem) + save_patient_no_iem(threshold_iem, patient_no_iem) } diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index 0ae7edff..5ae656b3 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -1,187 +1,441 @@ -#' Getting the intensities for calculating ratio Z-scores +#' Preparing the intensities and Z-score dataframe. +#' Certain columns are removed, the HMDB_code and HMDB_name column are moved forward, +#' the avg_ctrls and sd_ctrls columns are renamed and the column type of all columns containing numbers +#' is changed to numeric. #' -#' @param ratios_metabs_df: dataframe with HMDB codes for the ratios (dataframe) -#' @param row_index: index of the row in the ratios_metabs_df (integer) -#' @param intensities_zscore_df: dataframe with intensities for each sample (dataframe) -#' @param fraction_side: either numerator or denominator, which side of the fraction (string) -#' @param intensity_cols: names of the columns that contain the intensities (string) +#' @param intensities_zscore_df: dataframe with intensities, Z-scores and metabolite information for all samples #' -#' @returns fraction_side_intensity: a vector of intensities (vector of integers) -get_intentities_for_ratios <- function(ratios_metabs_df, row_index, intensities_zscore_df, fraction_side, intensity_cols) { - fraction_side_hmdb_ids <- ratios_metabs_df[row_index, fraction_side] - if (grepl("plus", fraction_side_hmdb_ids)) { - fraction_side_hmdb_id_list <- strsplit(fraction_side_hmdb_ids, "plus")[[1]] - fraction_side_intensity_list <- intensities_zscore_df %>% - filter(HMDB_code %in% fraction_side_hmdb_id_list) %>% - select(any_of(intensity_cols)) - fraction_side_intensity <- apply(fraction_side_intensity_list, 2, sum) - } else if (fraction_side_hmdb_ids == "one") { - fraction_side_intensity <- 1 - } else { - fraction_side_intensity <- intensities_zscore_df %>% - filter(HMDB_code == fraction_side_hmdb_ids) %>% - select(any_of(intensity_cols)) - } - return(fraction_side_intensity) +#' @returns intensities_zscore_df: a dataframe containing intensities, Z-scores, HMDB IDs, HMDB names and +#' the mean and average of all controls +prepare_intensities_zscore_df <- function(intensities_zscore_df) { + intensities_zscore_df <- intensities_zscore_df %>% + select(-c( + plots, HMDB_name_all, HMDB_ID_all, sec_HMDB_ID, HMDB_key, sec_HMDB_ID_rlvnc, name, + relevance, descr, origin, fluids, tissue, disease, pathway, nr_ctrls + )) %>% + relocate(c(HMDB_code, HMDB_name)) %>% + rename(mean_controls = avg_ctrls, sd_controls = sd_ctrls) %>% + mutate(across(!c(HMDB_name, HMDB_code), as.numeric)) + return(intensities_zscore_df) } -#' Get the sample IDs for columns that have Z-score and intensities +#' Get all column names containing a specific prefix. +#' Find all column names containing a specific prefix, e.g. "P", and remove the _Zscore suffix from the names +#' +#' @param dataframe: dataframe containing multiple columns with Z-scores +#' @param sample_label: a string of a prefix to be searched in the column names, e.g. "P" or "C". +#' +#' @returns sample_colnames: a vector of column names all containing the prefix. +get_colnames_samples <- function(dataframe, sample_label) { + sample_colnames <- unique(gsub("_Zscore", "", grepv(paste0("^", sample_label), colnames(dataframe)))) + return(sample_colnames) +} + +#' Add Zscores for multiple ratios to the dataframe +#' +#' @param outlist: dataframe containing intensities and Z-scores for all controls and patients +#' @param metabolites_ratios_df: dataframe containing numerators and denominators for all ratios +#' @param all_sample_ids: vector of sample IDS, controls and patients +#' +#' @returns intensities_zscore_ratios_df: dataframe containing intensities and Z-scores for all controls and patients +#' for all metabolites and ratios +add_zscores_ratios_to_df <- function(outlist, metabolites_ratios_df, all_sample_ids) { + intensities_zscores_df <- prepare_intensities_zscore_df(outlist) + + # calculate Z-scores for the ratios + zscore_ratios_df <- calculate_zscore_ratios(metabolites_ratios_df, intensities_zscores_df, all_sample_ids) + intensities_zscore_ratios_df <- rbind(intensities_zscores_df, zscore_ratios_df) + + return(intensities_zscore_ratios_df) +} + +#' Calculate Z-scores for ratios #' -#' @param colnames_zscore: vector of sample IDs from the dataframe containing Z-scores (vector of strings) -#' @param intensity_cols: vector of sample IDs form the dataframe containing intensities (vector of strings) +#' @param metabolites_ratios_df: dataframe containing numerators and denominators for all ratios +#' @param intensities_zscores_df: dataframe containing intensities and Z-scores for all controls and patients +#' @param intensity_col_names: vector of sample IDS, controls and patients #' -#' @returns: vector of sample IDs that are in both input vectors (vector of strings) -get_zscore_columns <- function(colnames_zscore, intensity_cols) { - sample_intersect <- intersect(paste0(intensity_cols, "_Zscore"), grep("_Zscore", colnames_zscore, value = TRUE)) - return(sample_intersect) +#' @returns zscore_ratios_df: dataframe containing Z-scores for all ratios for all samples +calculate_zscore_ratios <- function(metabolites_ratios_df, intensities_zscores_df, intensity_col_names) { + zscore_ratios_df <- data.frame(matrix( + ncol = ncol(intensities_zscores_df), + nrow = nrow(metabolites_ratios_df) + )) + colnames(zscore_ratios_df) <- colnames(intensities_zscores_df) + + # put HMDB info into first two columns of ratio_zscore_df + zscore_ratios_df$HMDB_code <- metabolites_ratios_df$HMDB.code + zscore_ratios_df$HMDB_name <- metabolites_ratios_df$Ratio_name + + intensity_cols_index <- which(colnames(zscore_ratios_df) %in% intensity_col_names) + for (row_index in seq_len(nrow(metabolites_ratios_df))) { + # Get a list of intensities for the numerator + numerator_intensities <- get_intensities_fraction_side( + metabolites_ratios_df, + row_index, + intensities_zscores_df, + "HMDB_numerator", + intensity_col_names + ) + # Get a list of intensities for the denominator + denominator_intensities <- get_intensities_fraction_side( + metabolites_ratios_df, + row_index, + intensities_zscores_df, + "HMDB_denominator", + intensity_col_names + ) + # calculate the intensity ratio for each sample + zscore_ratios_df[row_index, intensity_cols_index] <- log2(numerator_intensities / denominator_intensities) + } + + control_intensities_cols_index <- grep("^C[^_]*$", colnames(intensities_zscores_df), perl = TRUE) + # Calculate means and SD's of the calculated ratios for Controls + zscore_ratios_df[, "mean_controls"] <- apply(zscore_ratios_df[, control_intensities_cols_index], 1, mean) + zscore_ratios_df[, "sd_controls"] <- apply(zscore_ratios_df[, control_intensities_cols_index], 1, sd) + + # Calculate Zscores for the ratios + samples_zscore_columns <- get_sample_ids_with_zscores(colnames(intensities_zscores_df), intensity_col_names) + intensity_ratios_df <- zscore_ratios_df[, intensity_col_names] + mean_ratios_controls <- zscore_ratios_df[, "mean_controls"] + sd_ratios_controls <- zscore_ratios_df[, "sd_controls"] + + zscore_ratios_df[, samples_zscore_columns] <- (intensity_ratios_df - mean_ratios_controls) / sd_ratios_controls + + return(zscore_ratios_df) +} + +#' Make and save violin plots for each patient in a PDF +#' +#' @param zscore_patients_df: dataframe with Z-scores for all patient samples +#' @param zscore_controls_df: dataframe with Z-scores for all control samples +#' @param path_metabolite_groups: string containing the path for the metabolite groups directories +#' @param nr_plots_perpage: integer containing the number of metabolites on a plot per page +#' @param number_of_samples: list containing the number of patient and control samples +#' @param run_name: string containing the run name +#' @param protocol_name: string containing the protocol name +#' @param explanation_violin_plot: vector of strings containing the explanation of the violin plots +#' @param number_of_metabolites: list containing the number of metabolites for the top and lowest table +make_and_save_violin_plot_pdfs <- function( + zscore_patients_df, + zscore_controls_df, + path_metabolite_groups, + nr_plots_perpage, + number_of_samples, + run_name, + protocol_name, + explanation_violin_plot, + number_of_metabolites) { + # Get all patient IDs + patient_col_names <- get_colnames_samples(zscore_patients_df, "P") + # get all files from metabolite_groups directory + metabolite_dirs <- list.files(path = path_metabolite_groups, full.names = FALSE, recursive = FALSE) + for (metabolite_dir in metabolite_dirs) { + # create a directory for the output PDFs + pdf_dir <- paste0("./", metabolite_dir) + dir.create(pdf_dir, showWarnings = FALSE) + + metab_list_all <- get_list_dataframes_from_dir(paste(path_metabolite_groups, metabolite_dir, sep = "/")) + metab_interest_patients <- merge_metabolite_info_zscores(metab_list_all, zscore_patients_df) + metab_interest_controls <- merge_metabolite_info_zscores(metab_list_all, zscore_controls_df) + metab_perpage <- get_data_per_metabolite_class( + metab_interest_patients, + metab_interest_controls, + nr_plots_perpage, + number_of_samples$patients, + number_of_samples$controls + ) + + # for Diagnostics metabolites to be saved in Helix + if (grepl("Diagnost", pdf_dir)) { + # get table that combines DIMS results with metabolite classes/Helix table + dims_helix_table <- prepare_helix_patient_data(metab_interest_patients, metab_list_all) + # check if run contains diagnostic patients (e.g. "P2024M") + if (any(is_diagnostic_patients(dims_helix_table$Sample))) { + # transform dataframe for Helix output + output_helix <- transform_metab_df_to_helix_df(protocol_name, dims_helix_table) + # save the DIMS Helix dataframe + path_helixfile <- paste0("./output_Helix_", run_name, ".csv") + write.csv(output_helix, path_helixfile, quote = FALSE, row.names = FALSE) + } + } + + # make violin plots per patient + for (patient_id in patient_col_names) { + if (grepl("Diagnost", pdf_dir)) { + # make list of metabolites that exceed alarm values for this patient + top_metabs_patient <- get_top_metabolites_df(patient_id, dims_helix_table) + } else { + # make list of top highest and lowest Z-scores for this patient + top_metabs_patient <- prepare_toplist( + patient_id, + zscore_patients_df, + number_of_metabolites$highest, + number_of_metabolites$lowest + ) + } + # generate normal violin plots + create_pdf_violin_plots(pdf_dir, patient_id, metab_perpage, top_metabs_patient, explanation_violin_plot) + } + } } #' Get a list with dataframes for all off the metabolite group in a directory #' -#' @param metab_group_dir: directory containing txt files with metabolites per group (string) +#' @param dir_with_subdirs: directory containing txt files with metabolites per group (string) #' -#' @returns: list with dataframes with info on metabolites (list of dataframes) -get_list_metabolites <- function(metab_group_dir) { +#' @returns list_of_dataframes: list with dataframes with info on metabolites (list of dataframes) +get_list_dataframes_from_dir <- function(dir_with_subdirs) { # get a list of all metabolite files - metabolite_files <- list.files(metab_group_dir, pattern = "*.txt", full.names = FALSE, recursive = FALSE) + txt_files_paths <- list.files(dir_with_subdirs, pattern = "*.txt", recursive = FALSE, full.names = TRUE) # put all metabolites into one list - metab_list_all <- lapply(paste(metab_group_dir, metabolite_files, sep = "/"), - read.table, sep = "\t", header = TRUE, quote = "") - names(metab_list_all) <- gsub(".txt", "", metabolite_files) + list_of_dataframes <- lapply(txt_files_paths, + read.table, + sep = "\t", header = TRUE, quote = "" + ) + names(list_of_dataframes) <- gsub(".txt", "", basename(txt_files_paths)) - return(metab_list_all) + return(list_of_dataframes) } -#' Combine patient Z-scores with metabolite info +#' Merge patient Z-scores with metabolite info #' -#' @param metab_list_all: list of dataframes with metabolite information for different stofgroepen (list) +#' @param list_df_metabolite_groups: list of dataframes with metabolite information for different metabolite classes (list) #' @param zscore_df: dataframe with metabolite Z-scores for all patient #' -#' @return: list of dataframes for each stofgroep with data for each metabolite and patient/control per row -combine_metab_info_zscores <- function(metab_list_all, zscore_df) { +#' @return list_dfs_metabs_info_zscores: list of dataframes for each metabolite class +#' containing info and zscores for all samples +merge_metabolite_info_zscores <- function(list_df_metabolite_groups, zscore_df) { # remove HMDB_name column and "_Zscore" from column (patient) names zscore_df <- zscore_df %>% - select(-HMDB_name) %>% - rename_with(~ str_remove(.x, "_Zscore"), .cols = contains("_Zscore")) + select(-HMDB_name) # put data into pages, max 20 violin plots per page in PDF - metab_interest_sorted <- list() + list_dfs_metabs_info_zscores <- list() - for (metab_class in names(metab_list_all)) { - metab_df <- metab_list_all[[metab_class]] - # Select HMDB_code and HMDB_name columns - metab_df <- metab_df %>% select(HMDB_code, HMDB_name) + for (metabolite_class in names(list_df_metabolite_groups)) { + # select the metabolite_class dataframe and select the HMDB_code and HMDB_name columns + metabolite_info_df <- list_df_metabolite_groups[[metabolite_class]] %>% select(HMDB_code, HMDB_name) - # Change the HMDB_name column so all names have 45 characters - metab_df <- metab_df %>% mutate(HMDB_name = case_when( - str_length(HMDB_name) > 45 ~ str_c(str_sub(HMDB_name, 1, 42), "..."), - str_length(HMDB_name) < 45 ~ str_pad(HMDB_name, 45, side = "right", pad = " "), - TRUE ~ HMDB_name - )) + # Pad or truncate the HMDB names + metabolite_info_df <- pad_truncate_hmdb_names(metabolite_info_df, 45, " ") # Join metabolite info with the Z-score dataframe - metab_interest <- metab_df %>% inner_join(zscore_df, by = "HMDB_code") %>% select(-HMDB_code) + metabolite_zscore_df <- metabolite_info_df %>% + inner_join(zscore_df, by = "HMDB_code") %>% + select(-HMDB_code) # put the data frame in long format - metab_interest_melt <- reshape2::melt(metab_interest, id.vars = "HMDB_name", variable.name = "Sample", - value.name = "Z_score") + metabolite_zscore_df_long <- reshape2::melt( + metabolite_zscore_df, + id.vars = "HMDB_name", + variable.name = "Sample", + value.name = "Z_score" + ) # Add the dataframe sorted on HMDB_name to a list - metab_interest_sorted[[metab_class]] <- metab_interest_melt + list_dfs_metabs_info_zscores[[metabolite_class]] <- metabolite_zscore_df_long } - return(metab_interest_sorted) + return(list_dfs_metabs_info_zscores) } #' Combine patient and control data for each page of the violinplot pdf #' -#' @param metab_interest_sorted: list of dataframes with data for each metabolite and patient (list) -#' @param metab_interest_contr: list of dataframes with data for each metabolite and control (list) -#' @param nr_plots_perpage: number of plots per page in the violinplot pdf (integer) -#' @param nr_pat: number of patients (integer) -#' @param nr_contr: number of controls (integer) +#' @param metab_interest_patients: list of dataframes with data for each metabolite and patient (list) +#' @param metab_interest_controls: list of dataframes with data for each metabolite and control (list) +#' @param number_of_plots_per_page: number of plots per page in the violinplot pdf (integer) +#' @param number_of_patients: number of patients (integer) +#' @param number_of_controls: number of controls (integer) #' -#' @return: list of dataframes with metabolite Z-scores for each patient and control, +#' @return list_metabolite_df_per_page: list of dataframes with metabolite Z-scores for each patient and control, #' the length of list is the number of pages for the violinplot pdf (list) -prepare_data_perpage <- function(metab_interest_sorted, metab_interest_contr, nr_plots_perpage, nr_pat, nr_contr) { - metab_perpage <- list() - metab_category <- c() - - for (metab_class in names(metab_interest_sorted)) { +get_data_per_metabolite_class <- function( + metab_interest_patients, + metab_interest_controls, + number_of_plots_per_page, + number_of_patients, + number_of_controls) { + list_metabolite_df_per_page <- list() + metabolite_categories <- c() + + for (metabolite_class in names(metab_interest_patients)) { # Get the data for patients and controls for the metab_interest_sorted list - metab_sort_patients_df <- metab_interest_sorted[[metab_class]] - metab_sort_controls_df <- metab_interest_contr[[metab_class]] - - # Calculate the number of pages - nr_pages <- ceiling(length(unique(metab_sort_patients_df$HMDB_name)) / nr_plots_perpage) - - # Get all metabolites and create list with HMDB naames of max nr_plots_perpage long - metabolites <- unique(metab_sort_patients_df$HMDB_name) - metabolites_in_chunks <- split(metabolites, ceiling(seq_along(metabolites) / nr_plots_perpage)) - nr_chunks <- length(metabolites_in_chunks) - - current_perpage <- lapply(metabolites_in_chunks, function(metab_name) { - patients_df <- metab_sort_patients_df %>% filter(HMDB_name %in% metab_name) - controls_df <- metab_sort_controls_df %>% filter(HMDB_name %in% metab_name) - - # Combine both dataframes - combined_df <- rbind(patients_df, controls_df) - - # Add empty dummy's to extend the number of metabs to the nr_plots_perpage - n_missing <- nr_plots_perpage - length(metab_name) - if (n_missing > 0) { - dummy_names <- paste0(" ", strrep(" ", seq_len(n_missing))) - metab_order <- c(metab_name, dummy_names) - } else { - metab_order <- metab_name - } - attr(combined_df, "y_order") <- rev(metab_order) + metabolite_class_patients_df <- metab_interest_patients[[metabolite_class]] + metabolite_class_controls_df <- metab_interest_controls[[metabolite_class]] + + # Get all metabolites and create list with HMDB names of max nr_plots_perpage long + metabolites <- unique(metabolite_class_patients_df$HMDB_name) + metabolites_in_chunks <- split(metabolites, ceiling(seq_along(metabolites) / number_of_plots_per_page)) + number_of_chunks_metabolites <- length(metabolites_in_chunks) + + # Get a list of plot data per page + page_plot_data_list <- get_list_page_plot_data( + metabolites_in_chunks, + metabolite_class_patients_df, + metabolite_class_controls_df, + number_of_plots_per_page + ) - return(combined_df) - }) # Add new items to main list - metab_perpage <- append(metab_perpage, current_perpage) + list_metabolite_df_per_page <- append(list_metabolite_df_per_page, page_plot_data_list) # create list of page headers - metab_category <- c(metab_category, paste(metab_class, seq(nr_chunks), sep = "_")) + metabolite_categories <- c(metabolite_categories, paste(metabolite_class, seq(number_of_chunks_metabolites), sep = "_")) } # add page headers to list - names(metab_perpage) <- metab_category + names(list_metabolite_df_per_page) <- metabolite_categories - return(metab_perpage) + return(list_metabolite_df_per_page) } #' Get patient data to be uploaded to Helix #' -#' @param metab_interest_sorted: list of dataframes with metabolite Z-scores for each sample/patient (list) -#' @param metab_list_all: list of tables with metabolites for Helix and violin plots (list) +#' @param list_dfs_metab_classes_zscores: list of dataframes with metabolite Z-scores for each sample/patient (list) +#' @param list_metabolite_classes: list of tables with metabolites for Helix and violin plots (list) #' -#' @return: dataframe with patient data with only metabolites for Helix and violin plots +#' @return df_zscores_to_helix: dataframe with patient data with only metabolites for Helix and violin plots #' with Helix name, high/low Z-score cutoffs -get_patient_data_to_helix <- function(metab_interest_sorted, metab_list_all) { +prepare_helix_patient_data <- function(list_dfs_metab_classes_zscores, list_metabolite_classes) { # Combine Z-scores of metab groups together - df_all_metabs_zscores <- bind_rows(metab_interest_sorted) + metabolite_zscore_dataframe <- bind_rows(list_dfs_metab_classes_zscores) # Change the Sample column to characters, trim HMDB_name and split HMDB_name in new column - df_all_metabs_zscores <- df_all_metabs_zscores %>% - mutate(Sample = as.character(Sample), - HMDB_name = str_trim(HMDB_name, "right"), - HMDB_name_split = str_split_fixed(HMDB_name, "nitine;", 2)[, 1]) + metabolite_zscore_dataframe <- metabolite_zscore_dataframe %>% + mutate( + Sample = as.character(Sample), + HMDB_name = str_trim(HMDB_name, "right"), + HMDB_name_split = str_split_fixed(HMDB_name, "nitine;", 2)[, 1] + ) - # Combine stofgroepen - dims_helix_table <- bind_rows(metab_list_all) + # Combine metabolite classes + dims_helix_metabolite_df <- bind_rows(list_metabolite_classes) - # Filter for Helix metabolites and split HMDB_name column for matching with df_all_metabs_zscores - dims_helix_table <- dims_helix_table %>% + # Filter for Helix metabolites and split HMDB_name column for matching with metabolite_zscore_dataframe + dims_helix_metabolite_df <- dims_helix_metabolite_df %>% filter(Helix == "ja") %>% mutate(HMDB_name_split = str_split_fixed(HMDB_name, "nitine;", 2)[, 1]) %>% select(HMDB_name_split, Helix_naam, high_zscore, low_zscore) # Filter DIMS results for metabolites for Helix and combine Helix info - df_metabs_helix <- df_all_metabs_zscores %>% - filter(HMDB_name_split %in% dims_helix_table$HMDB_name_split) %>% - left_join(dims_helix_table, by = join_by(HMDB_name_split)) %>% + df_zscores_to_helix <- metabolite_zscore_dataframe %>% + filter(HMDB_name_split %in% dims_helix_metabolite_df$HMDB_name_split) %>% + left_join(dims_helix_metabolite_df, by = join_by(HMDB_name_split)) %>% select(HMDB_name, Sample, Z_score, Helix_naam, high_zscore, low_zscore) - return(df_metabs_helix) + return(df_zscores_to_helix) +} + +#' Getting the intensities for calculating ratio Z-scores +#' Retrieving a vector of intensities for a particular fraction side of the ratios for all samples. +#' +#' @param ratios_metabs_df: dataframe with HMDB codes for the ratios (dataframe) +#' @param row_index: index of the row in the ratios_metabs_df (integer) +#' @param intensities_zscore_df: dataframe with intensities for each sample (dataframe) +#' @param fraction_side: either numerator or denominator, which side of the fraction (string) +#' @param intensity_cols: names of the columns that contain the intensities (string) +#' +#' @returns fraction_side_intensity: a vector of intensities (vector of integers) +get_intensities_fraction_side <- function(ratios_metabs_df, row_index, intensities_zscore_df, fraction_side, intensity_cols) { + # get the HMDB ID(s) for the given fraction side + fraction_side_hmdb_ids <- ratios_metabs_df[row_index, fraction_side] + if (grepl("plus", fraction_side_hmdb_ids)) { + # if fraction side contains "plus", split to get both HMDB IDs + fraction_side_hmdb_id_list <- strsplit(fraction_side_hmdb_ids, "plus")[[1]] + # get intensities for both HMDB IDs for all samples + fraction_side_intensity_list <- intensities_zscore_df %>% + filter(HMDB_code %in% fraction_side_hmdb_id_list) %>% + select(any_of(intensity_cols)) + # sum intensities to 1 intensity per samples + fraction_side_intensity <- apply(fraction_side_intensity_list, 2, sum) + } else if (fraction_side_hmdb_ids == "one") { + # set intensity to 1 + fraction_side_intensity <- 1 + } else { + # get intensities of the HMDB ID for all samples + fraction_side_intensity <- intensities_zscore_df %>% + filter(HMDB_code == fraction_side_hmdb_ids) %>% + select(any_of(intensity_cols)) + } + # vector of intensities for all samples + fraction_side_intensity <- as.numeric(fraction_side_intensity) + return(fraction_side_intensity) +} + +#' Get the sample IDs for columns that have Z-score and intensities +#' +#' @param colnames_zscore_cols: vector of sample IDs from the dataframe containing Z-scores (vector of strings) +#' @param colnames_intensity_cols: vector of sample IDs form the dataframe containing intensities (vector of strings) +#' +#' @returns colnames_intersect: vector of sample IDs that are in both input vectors, ending on "_Zscore" (vector of strings) +get_sample_ids_with_zscores <- function(colnames_zscore_cols, colnames_intensity_cols) { + colnames_intersect <- intersect( + paste0(colnames_intensity_cols, "_Zscore"), + grep("_Zscore", colnames_zscore_cols, value = TRUE) + ) + return(colnames_intersect) +} + +#' Pad or truncate HMDB names to a fixed width +#' Add spaces or remove HMDB name characters till the length of the name equals the 'width' +#' +#' @param metabolite_info_df: A dataframe containing a column `HMDB_name` (character). +#' @param width: Integer target width for the display names. Default is 45. +#' @param pad_character: Single character used for padding. Default is a space `" "`. +#' +#' @return metabolite_info_df: A dataframe where the HMDB names are transformed +pad_truncate_hmdb_names <- function(metabolite_info_df, width, pad_character) { + # Change the HMDB_name column so all names have 45 characters + # remove characters if name is longer and add "..." + # add empty spaces till 45 charachters if name is shorter + # keep the name if name is exactly 45 characters + keep_lenght <- width - 3 + metabolite_info_df <- metabolite_info_df %>% mutate(HMDB_name = case_when( + str_length(HMDB_name) > width ~ str_c(str_sub(HMDB_name, 1, keep_lenght), "..."), + str_length(HMDB_name) < width ~ str_pad(HMDB_name, width, side = "right", pad = pad_character), + TRUE ~ HMDB_name + )) + return(metabolite_info_df) +} + +#' Get a list of dataframes for each chunk +#' For each chunk, get a dataframe containing the metabolites in that chunk and add it to the list +#' +#' @param metabolites_in_chunks: list of vectors, each containing metabolites +#' @param metabolite_class_patients_df: dataframe of Z-scores for all patient +#' @param metabolite_class_controls_df: dataframe of Z-scores for all control +#' @param number_of_plots_per_page: integer containing the number of metabolites per plot per page +#' +#' @returns page_plot_data_list: a list of dataframes containing Z-scores +get_list_page_plot_data <- function( + metabolites_in_chunks, + metabolite_class_patients_df, + metabolite_class_controls_df, + number_of_plots_per_page) { + # For each chunk, get a dataframe containing the metabolites and add to a list + page_plot_data_list <- lapply(metabolites_in_chunks, function(metabolite_names_chunk) { + patients_df_chunk <- metabolite_class_patients_df %>% filter(HMDB_name %in% metabolite_names_chunk) + controls_df_chunk <- metabolite_class_controls_df %>% filter(HMDB_name %in% metabolite_names_chunk) + + # Combine both dataframes + patients_controls_df_chunk <- rbind(patients_df_chunk, controls_df_chunk) + metabolite_order <- make_metabolite_order(number_of_plots_per_page, metabolite_names_chunk) + + # Set the order of the metabolites for the violin plots + attr(patients_controls_df_chunk, "y_order") <- rev(metabolite_order) + + return(patients_controls_df_chunk) + }) +} + +make_metabolite_order <- function(number_of_plots_per_page, metabolite_names_chunk) { + # Add empty dummy's to extend the number of metabs to the nr_plots_perpage + number_of_plots_missing <- number_of_plots_per_page - length(metabolite_names_chunk) + if (number_of_plots_missing > 0) { + dummy_names <- paste0(" ", strrep(" ", seq_len(number_of_plots_missing))) + metabolite_order <- c(metabolite_names_chunk, dummy_names) + } else { + metabolite_order <- metabolite_names_chunk + } + return(metabolite_order) } #' Check for Diagnostics patients with correct patient number (e.g. starting with "P2024M") @@ -189,7 +443,7 @@ get_patient_data_to_helix <- function(metab_interest_sorted, metab_list_all) { #' @param patient_column: a column from dataframe with IDs (character vector) #' #' @return: a logical vector with TRUE or FALSE for each element (vector) -is_diagnostic_patient <- function(patient_column) { +is_diagnostic_patients <- function(patient_column) { diagnostic_patients <- grepl("^P[0-9]{4}M", patient_column) return(diagnostic_patients) @@ -201,9 +455,9 @@ is_diagnostic_patient <- function(patient_column) { #' @param df_metabs_helix: dataframe with metabolite Z-scores for patients (dataframe) #' #' @return: dataframe with patient metabolite Z-scores in correct format for Helix -output_for_helix <- function(protocol_name, df_metabs_helix) { +transform_metab_df_to_helix_df <- function(protocol_name, df_metabs_helix) { # Remove positive controls - df_metabs_helix <- df_metabs_helix %>% filter(is_diagnostic_patient(Sample)) + df_metabs_helix <- df_metabs_helix %>% filter(is_diagnostic_patients(Sample)) # Add 'Vial' column, each patient has unique ID df_metabs_helix <- df_metabs_helix %>% @@ -255,7 +509,7 @@ add_lab_id_and_onderzoeksnr <- function(df_metabs_helix) { #' @param dims_helix_table: dataframe with metabolite Z-scores for each patient and Helix info (dataframe) #' #' @return: dataframe with metabolites that exceed the min and max Z-score cutoffs for the selected patient -prepare_alarmvalues <- function(patient_name, dims_helix_table) { +get_top_metabolites_df <- function(patient_name, dims_helix_table) { # extract data for patient of interest (patient_name) patient_metabs_helix <- dims_helix_table %>% filter(Sample == patient_name) %>% @@ -266,15 +520,19 @@ prepare_alarmvalues <- function(patient_name, dims_helix_table) { if (nrow(patient_high_df) > 0 || nrow(patient_low_df) > 0) { # sort tables on zscore - patient_high_df <- patient_high_df %>% arrange(desc(Z_score)) %>% select(c(HMDB_name, Z_score)) - patient_low_df <- patient_low_df %>% arrange(Z_score) %>% select(c(HMDB_name, Z_score)) + patient_high_df <- patient_high_df %>% + arrange(desc(Z_score)) %>% + select(c(HMDB_name, Z_score)) + patient_low_df <- patient_low_df %>% + arrange(Z_score) %>% + select(c(HMDB_name, Z_score)) } # add lines for increased, decreased - extra_line1 <- c("Increased", "") - extra_line2 <- c("Decreased", "") + line_increased <- c("Increased", "") + line_decreased <- c("Decreased", "") # combine the two lists - top_metab_patient <- rbind(extra_line1, patient_high_df, extra_line2, patient_low_df) + top_metab_patient <- rbind(line_increased, patient_high_df, line_decreased, patient_low_df) # remove row names rownames(top_metab_patient) <- NULL @@ -292,19 +550,17 @@ prepare_alarmvalues <- function(patient_name, dims_helix_table) { #' @param top_lowest: the number of metabolites with the lowest Z-score to display in the table (numeric) #' #' @return: dataframe with 30 metabolites and Z-scores (dataframe) -prepare_toplist <- function(patient_id, zscore_patients) { - top_highest <- 20 - top_lowest <- 10 +prepare_toplist <- function(patient_id, zscore_patients, num_of_highest_metabolites, num_of_lowest_metabolites) { patient_df <- zscore_patients %>% select(HMDB_code, HMDB_name, !!sym(patient_id)) %>% arrange(!!sym(patient_id)) # Get lowest Zscores - patient_df_low <- patient_df[1:top_lowest, ] + patient_df_low <- patient_df[1:num_of_lowest_metabolites, ] patient_df_low <- patient_df_low %>% mutate(across(!!sym(patient_id), ~ round(.x, 2))) # Get highest Zscores - patient_df_high <- patient_df[nrow(patient_df):(nrow(patient_df) - top_highest + 1), ] + patient_df_high <- patient_df[nrow(patient_df):(nrow(patient_df) - num_of_highest_metabolites + 1), ] patient_df_high <- patient_df_high %>% mutate(across(!!sym(patient_id), ~ round(.x, 2))) # add lines for increased, decreased @@ -332,10 +588,16 @@ create_pdf_violin_plots <- function(pdf_dir, patient_id, metab_perpage, top_meta plot_height <- 9.6 plot_width <- 6 + # get the names and numbers in the table aligned + table_theme <- ttheme_default( + core = list(fg_params = list(hjust = 0, x = 0.05, fontsize = 6)), + colhead = list(fg_params = list(fontsize = 8, fontface = "bold")) + ) + # patient plots, create the PDF device patient_id_sub <- patient_id suffix <- "" - if (grepl("Diagnostics", pdf_dir) && is_diagnostic_patient(patient_id)) { + if (grepl("Diagnostics", pdf_dir) && is_diagnostic_patients(patient_id)) { prefix <- "MB" suffix <- "_DIMS_PL_DIAG" # substitute P and M in P2020M00001 into right format for Helix @@ -350,9 +612,10 @@ create_pdf_violin_plots <- function(pdf_dir, patient_id, metab_perpage, top_meta } pdf(paste0(pdf_dir, "/", prefix, patient_id_sub, suffix, ".pdf"), - onefile = TRUE, - width = plot_width, - height = plot_height) + onefile = TRUE, + width = plot_width, + height = plot_height + ) # page headers: page_headers <- names(metab_perpage) @@ -364,8 +627,10 @@ create_pdf_violin_plots <- function(pdf_dir, patient_id, metab_perpage, top_meta number_of_pages <- ceiling(total_rows / max_rows_per_page) # get the names and numbers in the table aligned - table_theme <- ttheme_default(core = list(fg_params = list(hjust = 0, x = 0.05, fontsize = 6)), - colhead = list(fg_params = list(fontsize = 8, fontface = "bold"))) + table_theme <- ttheme_default( + core = list(fg_params = list(hjust = 0, x = 0.05, fontsize = 6)), + colhead = list(fg_params = list(fontsize = 8, fontface = "bold")) + ) for (page in seq(number_of_pages)) { start_row <- (page - 1) * max_rows_per_page + 1 @@ -440,23 +705,31 @@ create_violin_plot <- function(metab_zscores_df, patient_zscore_df, sub_perpage, # Make violin plots geom_violin(scale = "width", na.rm = TRUE) + # Add Z-score for the selected patient, shape=22 gives square for patient of interest - geom_point(data = patient_zscore_df, aes(color = Z_score), - size = 3.5 * circlesize, shape = 22, fill = "white", na.rm = TRUE) + + geom_point( + data = patient_zscore_df, aes(color = Z_score), + size = 3.5 * circlesize, shape = 22, fill = "white", na.rm = TRUE + ) + # Add the Z-score at the right side of the plot - geom_text(data = patient_zscore_df, - aes(16, label = paste0("Z=", round(Z_score, 2))), - hjust = "left", vjust = +0.2, size = 3, na.rm = TRUE) + + geom_text( + data = patient_zscore_df, + aes(16, label = paste0("Z=", round(Z_score, 2))), + hjust = "left", vjust = +0.2, size = 3, na.rm = TRUE + ) + # Set colour for the Z-score of the selected patient - scale_fill_gradientn(colors = colors_plot, values = NULL, space = "Lab", - na.value = "grey50", guide = "colourbar", aesthetics = "colour") + + scale_fill_gradientn( + colors = colors_plot, values = NULL, space = "Lab", + na.value = "grey50", guide = "colourbar", aesthetics = "colour" + ) + # Add labels to the axis labs(x = "Z-scores", y = "Metabolites", subtitle = sub_perpage, color = "z-score") + # Add a title to the page ggtitle(label = paste0("Results for patient ", patient_id)) + # Set theme: size and font type of y-axis labels, remove legend and make the - theme(axis.text.y = element_text(family = "Courier", size = 6), - legend.position = "none", - plot.caption = element_text(size = rel(fontsize))) + + theme( + axis.text.y = element_text(family = "Courier", size = 6), + legend.position = "none", + plot.caption = element_text(size = rel(fontsize)) + ) + # Set y-axis to set order scale_y_discrete(limits = y_order) + # Limit the x-axis to between -5 and 20 @@ -470,7 +743,7 @@ create_violin_plot <- function(metab_zscores_df, patient_zscore_df, sub_perpage, #' Run the dIEM algorithm (DOI: 10.3390/ijms21030979) #' -#' @param expected_biomarkers_df: table with information for HMDB codes about IEMs (dataframe) +#' @param expected_biomarkers_df: dataframe with information for HMDB codes about IEMs (dataframe) #' @param zscore_patients: dataframe containing Z-scores for patient (dataframe) #' #' @returns probability_score: a dataframe with probability scores for IEMs for each patient (dataframe) @@ -479,11 +752,15 @@ run_diem_algorithm <- function(expected_biomarkers_df, zscore_patients_df, sampl ranking_patients <- zscore_patients_df %>% mutate(across(-c(HMDB_code, HMDB_name), rank_patient_zscores)) - ranking_patients <- merge(x = expected_biomarkers_df, y = ranking_patients, - by.x = c("HMDB_code"), by.y = c("HMDB_code")) + ranking_patients <- merge( + x = expected_biomarkers_df, y = ranking_patients, + by.x = c("HMDB_code"), by.y = c("HMDB_code") + ) - zscore_expected_df <- merge(x = expected_biomarkers_df, y = zscore_patients_df, - by.x = c("HMDB_code"), by.y = c("HMDB_code")) + zscore_expected_df <- merge( + x = expected_biomarkers_df, y = zscore_patients_df, + by.x = c("HMDB_code"), by.y = c("HMDB_code") + ) # Change Z-score to zero for specific cases zscore_expected_df <- zscore_expected_df %>% mutate(across( @@ -557,15 +834,133 @@ rank_patient_zscores <- function(zscore_col) { #' Save the probability score dataframe as an Excel file #' #' @param probability_score: a dataframe containing probability scores for each patient (dataframe) -#' @param output_dir: location where to save the Excel file (string) #' @param run_name: name of the run, for the file name (string) -save_prob_scores_to_excel <- function(probability_score, output_dir, run_name) { +save_prob_scores_to_excel <- function(probability_score, run_name) { # Create conditional formatting for output Excel sheet. Colors according to values. wb <- createWorkbook() addWorksheet(wb, "Probability Scores") writeData(wb, "Probability Scores", probability_score) - conditionalFormatting(wb, "Probability Scores", cols = 2:ncol(probability_score), rows = seq_len(nrow(probability_score)), - type = "colourScale", style = c("white", "#FFFDA2", "red"), rule = c(1, 10, 100)) - saveWorkbook(wb, file = paste0(output_dir, "/dIEM_algoritme_output_", run_name, ".xlsx"), overwrite = TRUE) + conditionalFormatting(wb, "Probability Scores", + cols = 2:ncol(probability_score), rows = seq_len(nrow(probability_score)), + type = "colourScale", style = c("white", "#FFFDA2", "red"), rule = c(1, 10, 100) + ) + saveWorkbook(wb, file = paste0("./dIEM_algoritme_output_", run_name, ".xlsx"), overwrite = TRUE) rm(wb) } + +#' Make and save dIEM plots +#' +#' @param diem_probability_score: dataframe with dIEM probability scores +#' @param patient_col_names: vector containing all patient column names +#' @param expected_biomarkers_df: dataframe with information for HMDB codes about IEMs +#' @param zscore_patients_df: dataframe containing Z-scores for all patients +#' @param zscore_controls_df: dataframe containing Z-scores for all controls +#' @param nr_plots_perpage: integer containing the number of metabolites per page +#' @param number_of_samples: list containing the number of patients and controls +#' @param number_of_metabolites: list containing the number of metabolites for the top and lowest table +#' +#' @returns patient_no_iem: vector of patient IDs that have no IEMs +make_and_save_diem_plots <- function( + diem_probability_score, + patient_col_names, + expected_biomarkers_df, + zscore_patients_df, + zscore_controls_df, + nr_plots_perpage, + number_of_samples, + number_of_metabolites) { + diem_plot_dir <- paste("./dIEM_plots", sep = "/") + dir.create(diem_plot_dir) + + patient_no_iem <- c() + + for (patient_id in patient_col_names) { + # Select the top IEMs and filter on the IEM threshold + patient_top_iems_probs <- diem_probability_score %>% + select(c(Disease, !!sym(patient_id))) %>% + arrange(desc(!!sym(patient_id))) %>% + slice(1:top_number_iem_diseases) %>% + filter(!!sym(patient_id) >= threshold_iem) + + if (nrow(patient_top_iems_probs) > 0) { + list_metabolites_top_iems <- get_probabilities_top_iems(patient_top_iems_probs, expected_biomarkers_df, patient_id) + + # Get the Z-scores with metabolite information + metabolites_iem_sorted <- merge_metabolite_info_zscores(list_metabolites_top_iems, zscore_patients_df) + metabolites_iem_controls <- merge_metabolite_info_zscores(list_metabolites_top_iems, zscore_controls_df) + + # Get a list of dataframes for each IEM + diem_metabolites_perpage <- get_data_per_metabolite_class( + metabolites_iem_sorted, + metabolites_iem_controls, + nr_plots_perpage, + number_of_samples$patients, + number_of_samples$controls + ) + # Get a dataframe of the top metabolites + top_metabolites_patient <- prepare_toplist( + patient_id, + zscore_patients_df, + number_of_metabolites$highest, + number_of_metabolites$lowest + ) + + # Generate and save dIEM violin plots + create_pdf_violin_plots( + diem_plot_dir, + patient_id, + diem_metabolites_perpage, + top_metabolites_patient, + explanation_violin_plot + ) + } else { + patient_no_iem <- c(patient_no_iem, patient_id) + } + } + return(patient_no_iem) +} + +#' Get the IEM probabilities for a patient for all diseases +#' +#' @param patient_top_iems_probs: dataframe containing the probability scores for diseases for a patient +#' @param expected_biomarkers_df: dataframe with information for HMDB codes about IEMs +#' @param patient_id: string containing the patien ID +#' +#' @returns list_metabolites_iems: list of dataframes containing the HMDB codes and names for all diseases +get_probabilities_top_iems <- function(patient_top_iems_probs, expected_biomarkers_df, patient_id) { + # Get the metabolites for each IEM and their probability + list_metabolites_iems <- list() + metabolites_iems_names <- c() + + for (iem in patient_top_iems_probs$Disease) { + # get the IEM probabilities for the selected patient + iem_probablity <- patient_top_iems_probs %>% + filter(Disease == iem) %>% + pull(!!sym(patient_id)) + metabolites_iems_names <- c(metabolites_iems_names, paste0(iem, ", probability score ", iem_probablity)) + # get the HMDB codes and names for the IEM of the selected patient + metabolites_iem_df <- expected_biomarkers_df %>% + filter(Disease == iem) %>% + select(HMDB_code, HMDB_name) + # Add for each IEM the HMDB codes and names to a list + list_metabolites_iems[[iem]] <- metabolites_iem_df + } + names(list_metabolites_iems) <- metabolites_iems_names + + return(list_metabolites_iems) +} + +#' Save a list of patient IDs to a text file +#' +#' @param threshold_iem: integer containing the IEM threshold +#' @param patient_no_iem: vector containing patient IDs +save_patient_no_iem <- function(threshold_iem, patient_no_iem) { + patient_no_iem <- c( + paste0( + "The following patient(s) did not have dIEM probability scores higher than ", + threshold_iem, " :" + ), + patient_no_iem + ) + write(file = paste0("./missing_probability_scores.txt"), patient_no_iem) +} From 0660b1e1816a9587e0906bcc8f4c7fed8dc84e13 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 26 Feb 2026 15:56:16 +0100 Subject: [PATCH 128/161] Moved variables to list --- DIMS/GenerateViolinPlots.R | 9 +++++++-- DIMS/export/generate_violin_plots_functions.R | 15 ++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 2b742be0..cad5d83d 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -32,6 +32,10 @@ expected_biomarkers_df <- expected_biomarkers_df %>% explanation_violin_plot <- readLines(file_explanation) # Set global variables +iem_variables <- list( + top_number_iem_diseases = 5, + threshold_iem = 5 +) top_number_iem_diseases <- 5 # number of diseases that score highest in algorithm to plot threshold_iem <- 5 # probability score cut-off for plotting the top diseases nr_plots_perpage <- 20 # number of violin plots per page in PDF @@ -90,9 +94,10 @@ patient_no_iem <- make_and_save_diem_plots( zscore_controls_df, nr_plots_perpage, number_of_samples, - number_of_metabolites + number_of_metabolites, + iem_variables ) if (length(patient_no_iem) > 0) { - save_patient_no_iem(threshold_iem, patient_no_iem) + save_patient_no_iem(iem_variables$threshold_iem, patient_no_iem) } diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index 5ae656b3..83ad7cd7 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -426,6 +426,14 @@ get_list_page_plot_data <- function( }) } +#' Make the order of metabolites for the violin plots +#' Create the order of metabolites and add empty strings if the number of metabolites is lower than +#' the number of plots per page. +#' +#' @param number_of_plots_per_page: integer containing the number of metabolites per plot per page +#' @param metabolite_names_chunk: list of vectors, each containing metabolites +#' +#' @returns metabolite_order: a vector containing all metabolites and possibly empty strings make_metabolite_order <- function(number_of_plots_per_page, metabolite_names_chunk) { # Add empty dummy's to extend the number of metabs to the nr_plots_perpage number_of_plots_missing <- number_of_plots_per_page - length(metabolite_names_chunk) @@ -868,7 +876,8 @@ make_and_save_diem_plots <- function( zscore_controls_df, nr_plots_perpage, number_of_samples, - number_of_metabolites) { + number_of_metabolites, + iem_variables) { diem_plot_dir <- paste("./dIEM_plots", sep = "/") dir.create(diem_plot_dir) @@ -879,8 +888,8 @@ make_and_save_diem_plots <- function( patient_top_iems_probs <- diem_probability_score %>% select(c(Disease, !!sym(patient_id))) %>% arrange(desc(!!sym(patient_id))) %>% - slice(1:top_number_iem_diseases) %>% - filter(!!sym(patient_id) >= threshold_iem) + slice(1:iem_variables$top_number_iem_diseases) %>% + filter(!!sym(patient_id) >= iem_variables$threshold_iem) if (nrow(patient_top_iems_probs) > 0) { list_metabolites_top_iems <- get_probabilities_top_iems(patient_top_iems_probs, expected_biomarkers_df, patient_id) From 062698ca1628168182c3e1d7baab97c8f81d9985 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 26 Feb 2026 15:56:32 +0100 Subject: [PATCH 129/161] Added new unit tests --- DIMS/tests/testthat/test_generate_excel.R | 2 +- .../testthat/test_generate_violin_plots.R | 948 ++++++++++++++---- 2 files changed, 737 insertions(+), 213 deletions(-) diff --git a/DIMS/tests/testthat/test_generate_excel.R b/DIMS/tests/testthat/test_generate_excel.R index 7a1b4280..0ea67c95 100644 --- a/DIMS/tests/testthat/test_generate_excel.R +++ b/DIMS/tests/testthat/test_generate_excel.R @@ -42,7 +42,7 @@ testthat::test_that("Calculating Z-scores using different methods for excluding expect_identical(colnames(calculate_zscores(test_outlist, "_Zscore", control_intensities, NULL, intensity_col_ids, startcol)), c("plots", "C101.1", "C102.1", "C103.1", "C104.1", "C105.1", "C106.1", "C107.1", "C108.1", "C109.1", "C110.1", "C111.1", "C112.1", "P2.1", "P3.1", "HMDB_name", "HMDB_name_all", "HMDB_ID_all", "sec_HMDB_ID", - "HMDB_key", "sec_HMDB_ID_rlvc", "name", "relevance", "descr", "origin", "fluids", "tissue", "disease", + "HMDB_key", "sec_HMDB_ID_rlvnc", "name", "relevance", "descr", "origin", "fluids", "tissue", "disease", "pathway", "HMDB_code", "avg_ctrls", "sd_ctrls", "nr_ctrls", "C101.1_Zscore", "C102.1_Zscore", "C103.1_Zscore", "C104.1_Zscore", "C105.1_Zscore", "C106.1_Zscore", "C107.1_Zscore", "C108.1_Zscore", "C109.1_Zscore", "C110.1_Zscore", "C111.1_Zscore", "C112.1_Zscore", "P2.1_Zscore", "P3.1_Zscore")) diff --git a/DIMS/tests/testthat/test_generate_violin_plots.R b/DIMS/tests/testthat/test_generate_violin_plots.R index 55a2dac1..5d0a818a 100644 --- a/DIMS/tests/testthat/test_generate_violin_plots.R +++ b/DIMS/tests/testthat/test_generate_violin_plots.R @@ -1,12 +1,4 @@ # unit tests for GenerateViolinPlots -# functions: get_intentities_for_ratios, get_zscore_columns, -# get_list_metabolites, combine_metab_info_zscores, -# prepare_data_perpage, get_patient_data_to_helix -# is_diagnostic_patient, output_for_helix -# add_lab_id_and_onderzoeksnummer, prepare_alarmvalues -# prepare_toplist, create_pdf_violin_plots -# create_violin_plot, run_diem_algorithm -# rank_patient_zscores, save_prob_scores_to_Excel suppressPackageStartupMessages(library("dplyr")) library(reshape2) @@ -17,7 +9,7 @@ library(stringr) source("../../export/generate_violin_plots_functions.R") -testthat::test_that("Get intensities for calculating the ratios", { +testthat::test_that("get_intensities_fraction_side: Get intensities for calculating the ratios", { test_intensities_zscore_df <- read.delim(test_path("fixtures", "test_intensities_zscore_df.txt")) test_ratios_metabs_df <- data.frame( @@ -26,65 +18,99 @@ testthat::test_that("Get intensities for calculating the ratios", { HMDB_numerator = c("HMDB001", "HMDB002plusHMDB003", "HMDB004"), HMDB_denominator = c("HMDB011", "HMDB012", "one") ) - - test_intensity_cols <- c("C101.1", "C102.1", "C103.1", "C104.1", "C105.1", - "P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5") - - expect_equal(colnames(get_intentities_for_ratios(test_ratios_metabs_df, 1, test_intensities_zscore_df, - "HMDB_numerator", test_intensity_cols)), - c("C101.1", "C102.1", "C103.1", "C104.1", "C105.1", - "P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5")) - expect_equal(unname(get_intentities_for_ratios(test_ratios_metabs_df, 2, test_intensities_zscore_df, - "HMDB_numerator", test_intensity_cols)), - c(2500, 2625, 1750, 2525, 2700, 2500, 2625, 1750, 2525, 2700)) - expect_equal(get_intentities_for_ratios(test_ratios_metabs_df, 3, test_intensities_zscore_df, - "HMDB_denominator", test_intensity_cols), - 1) + expect_equal( + get_intensities_fraction_side( + test_ratios_metabs_df, + 2, + test_intensities_zscore_df, + "HMDB_numerator", + test_intensity_cols + ), + c(2500, 2625, 1750, 2525, 2700, 2500, 2625, 1750, 2525, 2700) + ) + expect_equal( + get_intensities_fraction_side( + test_ratios_metabs_df, + 3, + test_intensities_zscore_df, + "HMDB_denominator", + test_intensity_cols + ), + 1 + ) }) -testthat::test_that("Get samples that have both intensity and Z-score columns", { +testthat::test_that("get_sample_ids_with_zscores: Get samples that have both intensity and Z-score columns", { test_intensities_zscore_df <- read.delim(test_path("fixtures", "test_intensities_zscore_df.txt")) - test_intensity_cols <- c("C101.1", "C102.1", "C103.1", "C104.1", "C105.1", - "P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5") + test_intensity_cols <- c( + "C101.1", "C102.1", "C103.1", "C104.1", "C105.1", + "P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5" + ) - expect_equal(length(get_zscore_columns(colnames(test_intensities_zscore_df), test_intensity_cols)), - 10) - expect_equal(get_zscore_columns(colnames(test_intensities_zscore_df), test_intensity_cols), - c("C101.1_Zscore", "C102.1_Zscore", "C103.1_Zscore", "C104.1_Zscore", "C105.1_Zscore", - "P2025M1_Zscore", "P2025M2_Zscore", "P2025M3_Zscore", "P2025M4_Zscore", "P2025M5_Zscore")) + expect_equal( + length(get_sample_ids_with_zscores(colnames(test_intensities_zscore_df), test_intensity_cols)), + 10 + ) + expect_equal( + get_sample_ids_with_zscores(colnames(test_intensities_zscore_df), test_intensity_cols), + c( + "C101.1_Zscore", "C102.1_Zscore", "C103.1_Zscore", "C104.1_Zscore", "C105.1_Zscore", + "P2025M1_Zscore", "P2025M2_Zscore", "P2025M3_Zscore", "P2025M4_Zscore", "P2025M5_Zscore" + ) + ) test_intensity_cols <- c("C101.1", "C102.1", "C103.1", "C104.1", "C105.1", "P2025M1", "P2025M2", "P2025M3") - expect_equal(length(get_zscore_columns(colnames(test_intensities_zscore_df), test_intensity_cols)), - 8) - expect_equal(get_zscore_columns(colnames(test_intensities_zscore_df), test_intensity_cols), - c("C101.1_Zscore", "C102.1_Zscore", "C103.1_Zscore", "C104.1_Zscore", "C105.1_Zscore", - "P2025M1_Zscore", "P2025M2_Zscore", "P2025M3_Zscore")) + expect_equal( + length(get_sample_ids_with_zscores(colnames(test_intensities_zscore_df), test_intensity_cols)), + 8 + ) + expect_equal( + get_sample_ids_with_zscores(colnames(test_intensities_zscore_df), test_intensity_cols), + c( + "C101.1_Zscore", "C102.1_Zscore", "C103.1_Zscore", "C104.1_Zscore", "C105.1_Zscore", + "P2025M1_Zscore", "P2025M2_Zscore", "P2025M3_Zscore" + ) + ) }) -testthat::test_that("Get a list with dataframes from metabilite files in a directory", { - test_path_metabolite_groups <- test_path("fixtures/test_metabolite_groups") +testthat::test_that("get_list_dataframes_from_dir: Get a list with dataframes from metabilite files in a directory", { + test_path_metabolite_groups <- test_path("fixtures/test_metabolite_groups/Diagnostics") + + expect_type(get_list_dataframes_from_dir(test_path_metabolite_groups), "list") + expect_identical( + names(get_list_dataframes_from_dir(test_path_metabolite_groups)), + c("test_acyl_carnitines", "test_crea_gua") + ) - expect_type(get_list_metabolites(test_path_metabolite_groups), "list") - expect_identical(names(get_list_metabolites(test_path_metabolite_groups)), - c("test_acyl_carnitines", "test_crea_gua")) - - expect_identical(colnames(get_list_metabolites(test_path_metabolite_groups)$test_acyl_carnitines), - c("HMDB_code", "HMDB_name", "Helix", "Helix_naam", "high_zscore", "low_zscore")) - expect_identical(get_list_metabolites(test_path_metabolite_groups)$test_acyl_carnitines$HMDB_name, - c("metab1", "metab3", "ratio1")) - expect_identical(get_list_metabolites(test_path_metabolite_groups)$test_acyl_carnitines$Helix, - c("ja", "nee", "ja")) - - expect_identical(colnames(get_list_metabolites(test_path_metabolite_groups)$test_crea_gua), - c("HMDB_code", "HMDB_name", "Helix", "Helix_naam", "high_zscore", "low_zscore")) - expect_identical(get_list_metabolites(test_path_metabolite_groups)$test_crea_gua$HMDB_name, - c("metab4", "metab11")) - expect_identical(get_list_metabolites(test_path_metabolite_groups)$test_crea_gua$Helix, - c("ja", "ja")) + expect_identical( + colnames(get_list_dataframes_from_dir(test_path_metabolite_groups)$test_acyl_carnitines), + c("HMDB_code", "HMDB_name", "Helix", "Helix_naam", "high_zscore", "low_zscore") + ) + expect_identical( + get_list_dataframes_from_dir(test_path_metabolite_groups)$test_acyl_carnitines$HMDB_name, + c("metab1", "metab3", "ratio1") + ) + expect_identical( + get_list_dataframes_from_dir(test_path_metabolite_groups)$test_acyl_carnitines$Helix, + c("ja", "nee", "ja") + ) + + expect_identical( + colnames(get_list_dataframes_from_dir(test_path_metabolite_groups)$test_crea_gua), + c("HMDB_code", "HMDB_name", "Helix", "Helix_naam", "high_zscore", "low_zscore") + ) + expect_identical( + get_list_dataframes_from_dir(test_path_metabolite_groups)$test_crea_gua$HMDB_name, + c("metab4", "metab11") + ) + expect_identical( + get_list_dataframes_from_dir(test_path_metabolite_groups)$test_crea_gua$Helix, + c("ja", "ja") + ) }) -testthat::test_that("Combine metabolite info dataframe and Z-score dataframe", { +testthat::test_that("merge_metabolite_info_zscores: Combine metabolite info dataframe and Z-score dataframe", { test_acyl_carnitines_df <- read.delim(test_path("fixtures/test_metabolite_groups/", "test_acyl_carnitines.txt")) test_crea_gua_df <- read.delim(test_path("fixtures/test_metabolite_groups/", "test_crea_gua.txt")) @@ -95,38 +121,72 @@ testthat::test_that("Combine metabolite info dataframe and Z-score dataframe", { test_intensities_zscore_df <- read.delim(test_path("fixtures", "test_intensities_zscore_df.txt")) test_zscore_patients_df <- test_intensities_zscore_df %>% select(HMDB_code, HMDB_name, any_of(test_patient_cols)) - expect_type(combine_metab_info_zscores(test_metab_list_all, test_zscore_patients_df), "list") - expect_identical(names(combine_metab_info_zscores(test_metab_list_all, test_zscore_patients_df)), - c("test_acyl_carnitines", "test_crea_gua")) - - expect_identical(colnames(combine_metab_info_zscores(test_metab_list_all, test_zscore_patients_df)$test_acyl_carnitines), - c("HMDB_name", "Sample", "Z_score")) - expect_identical((combine_metab_info_zscores(test_metab_list_all, - test_zscore_patients_df)$test_acyl_carnitines$Z_score), - c(0.31, 2.34, 2.45, 1.45, 2.14, -1.44, 12.18, -0.18, 3.22, -3.18)) - expect_identical(as.character(combine_metab_info_zscores(test_metab_list_all, - test_zscore_patients_df)$test_acyl_carnitines$Sample), - c("P2025M1", "P2025M1", "P2025M2", "P2025M2", "P2025M3", - "P2025M3", "P2025M4", "P2025M4", "P2025M5", "P2025M5")) - expect_equal(nchar(combine_metab_info_zscores(test_metab_list_all, - test_zscore_patients_df)$test_acyl_carnitines$HMDB_name[1]), - 45) - - expect_identical(colnames(combine_metab_info_zscores(test_metab_list_all, test_zscore_patients_df)$test_crea_gua), - c("HMDB_name", "Sample", "Z_score")) - expect_identical((combine_metab_info_zscores(test_metab_list_all, - test_zscore_patients_df)$test_crea_gua$Z_score), - c(0.84, -0.46, -0.15, -1.51, -0.78, 1.68, 0.84, 1.48, 0.47, 1.18)) - expect_identical(as.character(combine_metab_info_zscores(test_metab_list_all, - test_zscore_patients_df)$test_crea_gua$Sample), - c("P2025M1", "P2025M1", "P2025M2", "P2025M2", "P2025M3", - "P2025M3", "P2025M4", "P2025M4", "P2025M5", "P2025M5")) - expect_equal(nchar(combine_metab_info_zscores(test_metab_list_all, - test_zscore_patients_df)$test_crea_gua$HMDB_name[1]), - 45) + expect_type(merge_metabolite_info_zscores(test_metab_list_all, test_zscore_patients_df), "list") + expect_identical( + names(merge_metabolite_info_zscores(test_metab_list_all, test_zscore_patients_df)), + c("test_acyl_carnitines", "test_crea_gua") + ) + + expect_identical( + colnames(merge_metabolite_info_zscores(test_metab_list_all, test_zscore_patients_df)$test_acyl_carnitines), + c("HMDB_name", "Sample", "Z_score") + ) + expect_identical( + (merge_metabolite_info_zscores( + test_metab_list_all, + test_zscore_patients_df + )$test_acyl_carnitines$Z_score), + c(0.31, 2.34, 2.45, 1.45, 2.14, -1.44, 12.18, -0.18, 3.22, -3.18) + ) + expect_identical( + as.character(merge_metabolite_info_zscores( + test_metab_list_all, + test_zscore_patients_df + )$test_acyl_carnitines$Sample), + c( + "P2025M1_Zscore", "P2025M1_Zscore", "P2025M2_Zscore", "P2025M2_Zscore", "P2025M3_Zscore", + "P2025M3_Zscore", "P2025M4_Zscore", "P2025M4_Zscore", "P2025M5_Zscore", "P2025M5_Zscore" + ) + ) + expect_equal( + nchar(merge_metabolite_info_zscores( + test_metab_list_all, + test_zscore_patients_df + )$test_acyl_carnitines$HMDB_name[1]), + 45 + ) + + expect_identical( + colnames(merge_metabolite_info_zscores(test_metab_list_all, test_zscore_patients_df)$test_crea_gua), + c("HMDB_name", "Sample", "Z_score") + ) + expect_identical( + (merge_metabolite_info_zscores( + test_metab_list_all, + test_zscore_patients_df + )$test_crea_gua$Z_score), + c(0.84, -0.46, -0.15, -1.51, -0.78, 1.68, 0.84, 1.48, 0.47, 1.18) + ) + expect_identical( + as.character(merge_metabolite_info_zscores( + test_metab_list_all, + test_zscore_patients_df + )$test_crea_gua$Sample), + c( + "P2025M1_Zscore", "P2025M1_Zscore", "P2025M2_Zscore", "P2025M2_Zscore", "P2025M3_Zscore", + "P2025M3_Zscore", "P2025M4_Zscore", "P2025M4_Zscore", "P2025M5_Zscore", "P2025M5_Zscore" + ) + ) + expect_equal( + nchar(merge_metabolite_info_zscores( + test_metab_list_all, + test_zscore_patients_df + )$test_crea_gua$HMDB_name[1]), + 45 + ) }) -testthat::test_that("Combine patient and control data for each page of the violinplot pdf", { +testthat::test_that("get_data_per_metabolite_class: Combine patient and control data for each page of the violinplot pdf", { test_acyl_carnitines_pat <- read.delim(test_path("fixtures/", "test_acyl_carnitines_patients.txt")) test_acyl_carnitines_ctrl <- read.delim(test_path("fixtures/", "test_acyl_carnitines_controls.txt")) @@ -143,45 +203,83 @@ testthat::test_that("Combine patient and control data for each page of the violi test_nr_pat <- 5 test_nr_contr <- 5 - expect_type(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, - test_nr_plots_perpage, test_nr_pat, test_nr_contr), - "list") - expect_equal(length(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, - test_nr_plots_perpage, test_nr_pat, test_nr_contr)), - 4) - expect_identical(names(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, - test_nr_plots_perpage, test_nr_pat, test_nr_contr)), - c("test_acyl_carnitines_1", "test_acyl_carnitines_2", "test_crea_gua_1", "test_crea_gua_2")) - expect_identical(unique(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, - test_nr_plots_perpage, test_nr_pat, - test_nr_contr)$test_acyl_carnitines_1$HMDB_name), - c("metab1 ")) - expect_identical(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, - test_nr_plots_perpage, test_nr_pat, - test_nr_contr)$test_acyl_carnitines_1$Sample, - c("P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5", "C101.1", "C102.1", "C103.1", "C104.1", "C105.1")) + expect_type( + get_data_per_metabolite_class( + test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, test_nr_contr + ), + "list" + ) + expect_equal( + length(get_data_per_metabolite_class( + test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, test_nr_contr + )), + 4 + ) + expect_identical( + names(get_data_per_metabolite_class( + test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, test_nr_contr + )), + c("test_acyl_carnitines_1", "test_acyl_carnitines_2", "test_crea_gua_1", "test_crea_gua_2") + ) + expect_identical( + unique(get_data_per_metabolite_class( + test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, + test_nr_contr + )$test_acyl_carnitines_1$HMDB_name), + c("metab1 ") + ) + expect_identical( + get_data_per_metabolite_class( + test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, + test_nr_contr + )$test_acyl_carnitines_1$Sample, + c("P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5", "C101.1", "C102.1", "C103.1", "C104.1", "C105.1") + ) test_nr_plots_perpage <- 2 - expect_equal(length(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, - test_nr_plots_perpage, test_nr_pat, test_nr_contr)), - 2) - expect_identical(names(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, - test_nr_plots_perpage, test_nr_pat, test_nr_contr)), - c("test_acyl_carnitines_1", "test_crea_gua_1")) - expect_identical(unique(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, - test_nr_plots_perpage, test_nr_pat, - test_nr_contr)$test_acyl_carnitines_1$HMDB_name), - c("metab1 ", "metab3 ")) - expect_identical(prepare_data_perpage(test_metab_interest_sorted, test_metab_interest_contr, - test_nr_plots_perpage, test_nr_pat, - test_nr_contr)$test_acyl_carnitines_1$Sample, - c("P2025M1", "P2025M1", "P2025M2", "P2025M2", "P2025M3", - "P2025M3", "P2025M4", "P2025M4", "P2025M5", "P2025M5", - "C101.1", "C101.1", "C102.1", "C102.1", "C103.1", "C103.1", "C104.1", "C104.1", "C105.1", "C105.1")) + expect_equal( + length(get_data_per_metabolite_class( + test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, test_nr_contr + )), + 2 + ) + expect_identical( + names(get_data_per_metabolite_class( + test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, test_nr_contr + )), + c("test_acyl_carnitines_1", "test_crea_gua_1") + ) + expect_identical( + unique(get_data_per_metabolite_class( + test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, + test_nr_contr + )$test_acyl_carnitines_1$HMDB_name), + c("metab1 ", "metab3 ") + ) + expect_identical( + get_data_per_metabolite_class( + test_metab_interest_sorted, test_metab_interest_contr, + test_nr_plots_perpage, test_nr_pat, + test_nr_contr + )$test_acyl_carnitines_1$Sample, + c( + "P2025M1", "P2025M1", "P2025M2", "P2025M2", "P2025M3", + "P2025M3", "P2025M4", "P2025M4", "P2025M5", "P2025M5", + "C101.1", "C101.1", "C102.1", "C102.1", "C103.1", "C103.1", "C104.1", "C104.1", "C105.1", "C105.1" + ) + ) }) -testthat::test_that("Generate a dataframe with information for Helix", { +testthat::test_that("prepare_helix_patient_data: Generate a dataframe with information for Helix", { test_acyl_carnitines_pat <- read.delim(test_path("fixtures/", "test_acyl_carnitines_patients.txt")) test_crea_gua_pat <- read.delim(test_path("fixtures/", "test_crea_gua_patients.txt")) @@ -194,25 +292,33 @@ testthat::test_that("Generate a dataframe with information for Helix", { test_metab_list_all <- list(test_acyl_carnitines_df, test_crea_gua_df) names(test_metab_list_all) <- c("test_acyl_carnitines", "test_crea_gua") - expect_identical(colnames(get_patient_data_to_helix(test_metab_interest_sorted, test_metab_list_all)), - c("HMDB_name", "Sample", "Z_score", "Helix_naam", "high_zscore", "low_zscore")) - expect_equal(dim(get_patient_data_to_helix(test_metab_interest_sorted, test_metab_list_all)), - c(15, 6)) - expect_identical(unique(get_patient_data_to_helix(test_metab_interest_sorted, test_metab_list_all)$HMDB_name), - c("metab1", "metab4", "metab11")) - expect_false("ratio1" %in% get_patient_data_to_helix(test_metab_interest_sorted, test_metab_list_all)$HMDB_name) - expect_equal(get_patient_data_to_helix(test_metab_interest_sorted, test_metab_list_all)$Z_score, - c(0.31, 2.45, 2.14, 12.18, 3.22, 0.84, -0.46, -0.15, -1.51, -0.78, 1.68, 0.84, 1.48, 0.47, 1.18)) + expect_identical( + colnames(prepare_helix_patient_data(test_metab_interest_sorted, test_metab_list_all)), + c("HMDB_name", "Sample", "Z_score", "Helix_naam", "high_zscore", "low_zscore") + ) + expect_equal( + dim(prepare_helix_patient_data(test_metab_interest_sorted, test_metab_list_all)), + c(15, 6) + ) + expect_identical( + unique(prepare_helix_patient_data(test_metab_interest_sorted, test_metab_list_all)$HMDB_name), + c("metab1", "metab4", "metab11") + ) + expect_false("ratio1" %in% prepare_helix_patient_data(test_metab_interest_sorted, test_metab_list_all)$HMDB_name) + expect_equal( + prepare_helix_patient_data(test_metab_interest_sorted, test_metab_list_all)$Z_score, + c(0.31, 2.45, 2.14, 12.18, 3.22, 0.84, -0.46, -0.15, -1.51, -0.78, 1.68, 0.84, 1.48, 0.47, 1.18) + ) }) -testthat::test_that("Check for diagnostic patients", { +testthat::test_that("is_diagnostic_patients: Check for diagnostic patients", { test_patient_column <- c("P2025M1", "P2025M2", "P2025M3", "P2025M4", "C101.1", "C102.1", "P2025D1", "P225M1") - expect_equal(is_diagnostic_patient(test_patient_column), c(TRUE, TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE)) - expect_equal(length(is_diagnostic_patient(test_patient_column)), 8) + expect_equal(is_diagnostic_patients(test_patient_column), c(TRUE, TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE)) + expect_equal(length(is_diagnostic_patients(test_patient_column)), 8) }) -testthat::test_that("Adding labnummer and Onderzoeksnummer to the Helix dataframe", { +testthat::test_that("add_lab_id_and_onderzoeksnr:Adding labnummer and Onderzoeksnummer to the Helix dataframe", { test_df_metabs_helix <- read.delim(test_path("fixtures/", "test_df_metabs_helix.txt")) test_df_metabs_helix <- test_df_metabs_helix %>% @@ -222,83 +328,163 @@ testthat::test_that("Adding labnummer and Onderzoeksnummer to the Helix datafram expect_true("labnummer" %in% colnames(add_lab_id_and_onderzoeksnr(test_df_metabs_helix))) expect_true("Onderzoeksnummer" %in% colnames(add_lab_id_and_onderzoeksnr(test_df_metabs_helix))) - expect_identical(unique(add_lab_id_and_onderzoeksnr(test_df_metabs_helix)$labnummer), - c("2025M1", "2025M2", "2025M3", "2025M4", "2025M5")) - expect_identical(unique(add_lab_id_and_onderzoeksnr(test_df_metabs_helix)$Onderzoeksnummer), - c("MB2025/1", "MB2025/2", "MB2025/3", "MB2025/4", "MB2025/5")) + expect_identical( + unique(add_lab_id_and_onderzoeksnr(test_df_metabs_helix)$labnummer), + c("2025M1", "2025M2", "2025M3", "2025M4", "2025M5") + ) + expect_identical( + unique(add_lab_id_and_onderzoeksnr(test_df_metabs_helix)$Onderzoeksnummer), + c("MB2025/1", "MB2025/2", "MB2025/3", "MB2025/4", "MB2025/5") + ) }) -testthat::test_that("Make the output for Helix", { +testthat::test_that("transform_metab_df_to_helix_df: Make the output for Helix", { test_protocol_name <- "test_protocol_name" test_df_metabs_helix <- read.delim(test_path("fixtures/", "test_df_metabs_helix.txt")) - expect_equal(dim(output_for_helix(test_protocol_name, test_df_metabs_helix)), - c(15, 6)) - expect_identical(colnames(output_for_helix(test_protocol_name, test_df_metabs_helix)), - c("Vial", "labnummer", "Onderzoeksnummer", "Protocol", "Name", "Amount")) - expect_identical(unique(output_for_helix(test_protocol_name, test_df_metabs_helix)$Protocol), - "test_protocol_name") - expect_identical(unique(output_for_helix(test_protocol_name, test_df_metabs_helix)$labnummer), - c("2025M1", "2025M2", "2025M3", "2025M4", "2025M5")) - expect_identical(unique(output_for_helix(test_protocol_name, test_df_metabs_helix)$Onderzoeksnummer), - c("MB2025/1", "MB2025/2", "MB2025/3", "MB2025/4", "MB2025/5")) - expect_equal(output_for_helix(test_protocol_name, test_df_metabs_helix)$Amount, - c(0.31, 2.45, 2.14, 12.18, 3.22, 0.84, -0.46, -0.15, -1.51, -0.78, 1.68, 0.84, 1.48, 0.47, 1.18)) + expect_equal( + dim(transform_metab_df_to_helix_df(test_protocol_name, test_df_metabs_helix)), + c(15, 6) + ) + expect_identical( + colnames(transform_metab_df_to_helix_df(test_protocol_name, test_df_metabs_helix)), + c("Vial", "labnummer", "Onderzoeksnummer", "Protocol", "Name", "Amount") + ) + expect_identical( + unique(transform_metab_df_to_helix_df(test_protocol_name, test_df_metabs_helix)$Protocol), + "test_protocol_name" + ) + expect_identical( + unique(transform_metab_df_to_helix_df(test_protocol_name, test_df_metabs_helix)$labnummer), + c("2025M1", "2025M2", "2025M3", "2025M4", "2025M5") + ) + expect_identical( + unique(transform_metab_df_to_helix_df(test_protocol_name, test_df_metabs_helix)$Onderzoeksnummer), + c("MB2025/1", "MB2025/2", "MB2025/3", "MB2025/4", "MB2025/5") + ) + expect_equal( + transform_metab_df_to_helix_df(test_protocol_name, test_df_metabs_helix)$Amount, + c(0.31, 2.45, 2.14, 12.18, 3.22, 0.84, -0.46, -0.15, -1.51, -0.78, 1.68, 0.84, 1.48, 0.47, 1.18) + ) }) -testthat::test_that("Create a dataframe with all metabolites that exceed the min and max Z-score cutoff", { +testthat::test_that("get_top_metabolites_df: Create a dataframe with all metabolites that exceed the min and max Z-score cutoff", { test_df_metabs_helix <- read.delim(test_path("fixtures/", "test_df_metabs_helix.txt")) test_patient_id <- "P2025M1" - expect_equal(dim(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)), - c(2, 2)) - expect_equal(colnames(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)), - c("Metabolite", "Z-score")) + expect_equal( + dim(get_top_metabolites_df(test_patient_id, test_df_metabs_helix)), + c(2, 2) + ) + expect_equal( + colnames(get_top_metabolites_df(test_patient_id, test_df_metabs_helix)), + c("Metabolite", "Z-score") + ) test_patient_id <- "P2025M2" - expect_equal(dim(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)), - c(4, 2)) - expect_equal(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)$`Z-score`, - c("", "2.45", "", "-1.51")) + expect_equal( + dim(get_top_metabolites_df(test_patient_id, test_df_metabs_helix)), + c(4, 2) + ) + expect_equal( + get_top_metabolites_df(test_patient_id, test_df_metabs_helix)$`Z-score`, + c("", "2.45", "", "-1.51") + ) test_patient_id <- "P2025M4" - expect_equal(dim(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)), - c(3, 2)) - expect_equal(prepare_alarmvalues(test_patient_id, test_df_metabs_helix)$`Z-score`, - c("", "12.18", "")) + expect_equal( + dim(get_top_metabolites_df(test_patient_id, test_df_metabs_helix)), + c(3, 2) + ) + expect_equal( + get_top_metabolites_df(test_patient_id, test_df_metabs_helix)$`Z-score`, + c("", "12.18", "") + ) }) -testthat::test_that("Create a dataframe with the top 20 highest and top 10 lowest metabolites", { +testthat::test_that("prepare_toplist: Create a dataframe with the top 20 highest and top 10 lowest metabolites", { test_zscore_patient_df <- read.delim(test_path("fixtures/", "test_zscore_patient_df.txt")) test_patient_id <- "P2025M1" - - expect_equal(dim(prepare_toplist(test_patient_id, test_zscore_patient_df)), - c(32, 3)) - expect_equal(colnames(prepare_toplist(test_patient_id, test_zscore_patient_df)), - c("HMDB_ID", "Metabolite", "Z-score")) - expect_equal(prepare_toplist(test_patient_id, test_zscore_patient_df)$HMDB_ID, - c("Increased", "HMDB030", "HMDB029", "HMDB028", "HMDB027", "HMDB026", "HMDB025", "HMDB024", "HMDB023", - "HMDB022", "HMDB021", "HMDB020", "HMDB019", "HMDB018", "HMDB017", "HMDB016", "HMDB015", "HMDB014", "HMDB013", - "HMDB012", "HMDB011", "Decreased", "HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB005", "HMDB006", - "HMDB007", "HMDB008", "HMDB009", "HMDB010")) - expect_equal(prepare_toplist(test_patient_id, test_zscore_patient_df)$`Z-score`, - c("", "30", "29", "28", "27", "26", "25", "24", "23", "22", "21", "20", "19", "18", "17", "16", "15", - "14", "13", "12", "11", "", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10")) + test_num_of_highest_metabolites <- 20 + test_num_of_lowest_metabolites <- 10 + + expect_equal( + dim(prepare_toplist( + test_patient_id, + test_zscore_patient_df, + test_num_of_highest_metabolites, + test_num_of_lowest_metabolites + )), + c(32, 3) + ) + expect_equal( + colnames(prepare_toplist( + test_patient_id, + test_zscore_patient_df, + test_num_of_highest_metabolites, + test_num_of_lowest_metabolites + )), + c("HMDB_ID", "Metabolite", "Z-score") + ) + expect_equal( + prepare_toplist( + test_patient_id, + test_zscore_patient_df, + test_num_of_highest_metabolites, + test_num_of_lowest_metabolites + )$HMDB_ID, + c( + "Increased", "HMDB030", "HMDB029", "HMDB028", "HMDB027", "HMDB026", "HMDB025", "HMDB024", "HMDB023", + "HMDB022", "HMDB021", "HMDB020", "HMDB019", "HMDB018", "HMDB017", "HMDB016", "HMDB015", "HMDB014", "HMDB013", + "HMDB012", "HMDB011", "Decreased", "HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB005", "HMDB006", + "HMDB007", "HMDB008", "HMDB009", "HMDB010" + ) + ) + expect_equal( + prepare_toplist( + test_patient_id, + test_zscore_patient_df, + test_num_of_highest_metabolites, + test_num_of_lowest_metabolites + )$`Z-score`, + c( + "", "30", "29", "28", "27", "26", "25", "24", "23", "22", "21", "20", "19", "18", "17", "16", "15", + "14", "13", "12", "11", "", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" + ) + ) test_patient_id <- "P2025M2" - expect_equal(prepare_toplist(test_patient_id, test_zscore_patient_df)$Metabolite, - c("", "metab1", "metab2", "metab3", "metab4", "metab5", "metab6", "metab7", "metab8", "metab9", "metab10", - "metab11", "metab12", "metab13", "metab14", "metab15", "metab16", "metab17", "metab18", "metab19", "metab20", - "", "metab30", "metab29", "metab28", "metab27", "metab26", "metab25", "metab24", "metab23", "metab22", - "metab21")) - expect_equal(prepare_toplist(test_patient_id, test_zscore_patient_df)$`Z-score`, - c("", "-1", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9", "-10", "-11", "-12", "-13", "-14", "-15", - "-16", "-17", "-18", "-19", "-20", "", "-30", "-29", "-28", "-27", "-26", "-25", "-24", "-23", "-22", "-21")) + expect_equal( + prepare_toplist( + test_patient_id, + test_zscore_patient_df, + test_num_of_highest_metabolites, + test_num_of_lowest_metabolites + )$Metabolite, + c( + "", "metab1", "metab2", "metab3", "metab4", "metab5", "metab6", "metab7", "metab8", "metab9", "metab10", + "metab11", "metab12", "metab13", "metab14", "metab15", "metab16", "metab17", "metab18", "metab19", "metab20", + "", "metab30", "metab29", "metab28", "metab27", "metab26", "metab25", "metab24", "metab23", "metab22", + "metab21" + ) + ) + expect_equal( + prepare_toplist( + test_patient_id, + test_zscore_patient_df, + test_num_of_highest_metabolites, + test_num_of_lowest_metabolites + )$`Z-score`, + c( + "", "-1", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9", "-10", "-11", "-12", "-13", "-14", "-15", + "-16", "-17", "-18", "-19", "-20", "", "-30", "-29", "-28", "-27", "-26", "-25", "-24", "-23", "-22", "-21" + ) + ) }) -testthat::test_that("Create a pdf with a table of top metabolites and violin plots", { +testthat::test_that("create_pdf_violin_plots: Create a pdf with a table of top metabolites and violin plots", { local_edition(3) temp_dir <- "./" dir.create(paste0(temp_dir, "violin_plots/")) @@ -320,8 +506,13 @@ testthat::test_that("Create a pdf with a table of top metabolites and violin plo `Z-score` = c("", "2.45", "", "-1.51") ) - expect_silent(create_pdf_violin_plots(test_pdf_dir, test_patient_id, - test_metab_perpage, test_top_metab_pt, test_explanation)) + expect_silent(create_pdf_violin_plots( + test_pdf_dir, + test_patient_id, + test_metab_perpage, + test_top_metab_pt, + test_explanation + )) out_pdf_violinplots <- file.path(test_pdf_dir, "R_P2025M1.pdf") expect_true(file.exists(out_pdf_violinplots)) @@ -331,7 +522,7 @@ testthat::test_that("Create a pdf with a table of top metabolites and violin plo unlink(test_pdf_dir, recursive = TRUE) }) -testthat::test_that("Create a violin plot", { +testthat::test_that("create_violin_plot: Create a violin plot", { test_patient_id <- "P2025M1" test_sub_perpage <- "test acyl carnitines" @@ -344,58 +535,391 @@ testthat::test_that("Create a violin plot", { expect_silent(create_violin_plot(test_metab_zscores_df, test_patient_zscore_df, test_sub_perpage, test_patient_id)) - expect_doppelganger("violin_plot_P2025M1", create_violin_plot(test_metab_zscores_df, test_patient_zscore_df, - test_sub_perpage, test_patient_id)) + expect_doppelganger("violin_plot_P2025M1", create_violin_plot( + test_metab_zscores_df, test_patient_zscore_df, + test_sub_perpage, test_patient_id + )) }) -testthat::test_that("Run dIEM algorithm", { +testthat::test_that("run_diem_algorithm :Run dIEM algorithm", { test_expected_biomarkers_df <- read.delim(test_path("fixtures/", "test_expected_biomarkers_df.txt")) test_zscore_patient_df <- read.delim(test_path("fixtures/", "test_zscore_patient_df.txt")) test_sample_cols <- c("P2025M1", "P2025M2", "P2025M3", "P2025M4") - expect_equal(dim(run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)), - c(7, 5)) - expect_identical(colnames(run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)), - c("Disease", "P2025M1", "P2025M2", "P2025M3", "P2025M4")) - expect_identical(run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)$Disease, - c("Disease A", "Disease B", "Disease C", "Disease D", "Disease E", "Disease F", "Disease G")) + expect_equal( + dim(run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)), + c(7, 5) + ) + expect_identical( + colnames(run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)), + c("Disease", "P2025M1", "P2025M2", "P2025M3", "P2025M4") + ) + expect_identical( + run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)$Disease, + c("Disease A", "Disease B", "Disease C", "Disease D", "Disease E", "Disease F", "Disease G") + ) expect_equal(run_diem_algorithm(test_expected_biomarkers_df, test_zscore_patient_df, test_sample_cols)$P2025M1, - c(10.94172, 0.95343, 12.12121, 0.00000, 44.28850, 0.00000, -38.70370), tolerance = 0.0001) + c(10.94172, 0.95343, 12.12121, 0.00000, 44.28850, 0.00000, -38.70370), + tolerance = 0.0001 + ) }) -testthat::test_that("Ranking Z-scores for a patient", { +testthat::test_that("rank_patient_zscores: Ranking Z-scores for a patient", { test_zscore_col <- c(1, 5, 6, 2, 7, -2, 3) expect_equal(length(rank_patient_zscores(test_zscore_col)), 7) - expect_identical(rank_patient_zscores(test_zscore_col), - c(6, 3, 2, 5, 1, 1, 4)) + expect_identical( + rank_patient_zscores(test_zscore_col), + c(6, 3, 2, 5, 1, 1, 4) + ) test_zscore_col <- c(3, 2, 1, 3) - expect_identical(rank_patient_zscores(test_zscore_col), - c(1, 2, 3, 1)) + expect_identical( + rank_patient_zscores(test_zscore_col), + c(1, 2, 3, 1) + ) test_zscore_col <- c(-1, -2, -3, -4) - expect_identical(rank_patient_zscores(test_zscore_col), - c(4, 3, 2, 1)) + expect_identical( + rank_patient_zscores(test_zscore_col), + c(4, 3, 2, 1) + ) }) -testthat::test_that("Saving the probability score dataframe as an Excel file", { +testthat::test_that("save_prob_scores_to_excel: Saving the probability score dataframe as an Excel file", { local_edition(3) test_probability_score_df <- read.delim(test_path("fixtures/", "test_probability_score_df.txt")) - test_output_dir <- "./test_excel" - dir.create(test_output_dir) test_run_name <- "test_run" - out_excel_file <- file.path(test_output_dir, paste0("/dIEM_algoritme_output_", test_run_name, ".xlsx")) + out_excel_file <- file.path(paste0("dIEM_algoritme_output_", test_run_name, ".xlsx")) - expect_silent(save_prob_scores_to_excel(test_probability_score_df, test_output_dir, test_run_name)) + expect_silent(save_prob_scores_to_excel(test_probability_score_df, test_run_name)) expect_true(file.exists(out_excel_file)) content_excel_file <- openxlsx::read.xlsx(out_excel_file, sheet = 1) expect_snapshot_output(content_excel_file) - unlink(test_output_dir, recursive = TRUE) + file.remove(out_excel_file, recursive = TRUE) +}) + +testthat::test_that("prepare_intensities_zscore_df: Preparing the intensities and Z-score dataframe", { + test_intensities_zscore_df <- read.delim(test_path("fixtures/", "test_outlist.txt")) + test_intensities_zscore_df$nr_ctrls <- c(28, 27, 30, 25) + test_intensities_zscore_df$avg_ctrls <- c(16129.17, 1150, 1231.25, 4015.833) + test_intensities_zscore_df$sd_ctrls <- c(51606.27, 349.6752, 313.7249, 6152.479) + + prepare_intensities_zscore_df(test_intensities_zscore_df) + + expect_equal( + colnames(prepare_intensities_zscore_df(test_intensities_zscore_df)), + c( + "HMDB_code", "HMDB_name", "C101.1", "C102.1", "C103.1", "C104.1", "C105.1", "C106.1", "C107.1", "C108.1", + "C109.1", "C110.1", "C111.1", "C112.1", "P2.1", "P3.1", "mean_controls", "sd_controls" + ) + ) + + expect_equal( + unname(sapply(prepare_intensities_zscore_df(test_intensities_zscore_df), class)), + c( + "character", "character", "numeric", "numeric", "numeric", "numeric", "numeric", "numeric", "numeric", + "numeric", "numeric", "numeric", "numeric", "numeric", "numeric", "numeric", "numeric", "numeric" + ) + ) +}) + +testthat::test_that("get_colnames_samples: Get all column names containing a specific prefix", { + test_colnames <- c( + "HMDB_name", "HMDB_code", "P1001", "P1001_Zscore", "P1002", + "P1003", "P1004", "C101_Zscore", "C102", "C103" + ) + test_intensities_zscore_df <- read.delim(test_path("fixtures/", "test_intensities_zscore_df.txt")) + + expect_equal(get_colnames_samples(test_intensities_zscore_df, "P"), c("P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5")) + expect_equal(get_colnames_samples(test_intensities_zscore_df, "C"), c("C101.1", "C102.1", "C103.1", "C104.1", "C105.1")) +}) + +testthat::test_that("add_zscores_ratios_to_df: Add Zscores for multiple ratios to the dataframe", { + test_outlist_df <- read.delim(test_path("fixtures/GenerateViolinPlots", "test_outlist_df.txt")) + + test_metabolites_ratios_df <- data.frame( + HMDB.code = c("HMDB000TT1", "HMDB000TT2", "HMDB000TT3"), + Ratio_name = c("Test_ratio1", "Test_ratio2", "Test_ratio3"), + HMDB_numerator = c("HMDB001", "HMDB003plusHMDB003", "HMDB001plusHMDB003plusHMDB004"), + HMDB_denominator = c("HMDB002", "HMDB004", "one") + ) + + test_all_sample_ids <- c( + "C101.1", "C102.1", "C103.1", "C104.1", "C105.1", "P2025M1", "P2025M2", "P2025M3", + "P2025M4", "P2025M5" + ) + + expect_equal( + add_zscores_ratios_to_df(test_outlist_df, test_metabolites_ratios_df, test_all_sample_ids)$HMDB_code, + c("HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012", "HMDB000TT1", "HMDB000TT2", "HMDB000TT3") + ) + expect_equal( + add_zscores_ratios_to_df( + test_outlist_df, + test_metabolites_ratios_df, + test_all_sample_ids + )$C101.1, + c(1000, 1200, 1300, 1400, 1500, 1600, -0.2630344, -0.1069152, 11.8533096) + ) + expect_equal( + add_zscores_ratios_to_df( + test_outlist_df, + test_metabolites_ratios_df, + test_all_sample_ids + )$C101.1_Zscore, + c(0.45, 1.67, -1.86, 0.58, 2.47, -0.56, -0.5899371, 0.4858991, -0.4552026), + tolerance = 0.0001 + ) +}) + +testthat::test_that("calculate_zscore_ratios: Calculate Zscores for ratios", { + test_outlist_df <- read.delim(test_path("fixtures/GenerateViolinPlots", "test_outlist_df.txt")) + test_intensities_zscores_df <- prepare_intensities_zscore_df(test_outlist_df) + + test_metabolites_ratios_df <- data.frame( + HMDB.code = c("HMDB000TT1", "HMDB000TT2", "HMDB000TT3"), + Ratio_name = c("Test_ratio1", "Test_ratio2", "Test_ratio3"), + HMDB_numerator = c("HMDB001", "HMDB003plusHMDB003", "HMDB001plusHMDB003plusHMDB004"), + HMDB_denominator = c("HMDB002", "HMDB004", "one") + ) + + test_all_sample_ids <- c( + "C101.1", "C102.1", "C103.1", "C104.1", "C105.1", "P2025M1", "P2025M2", "P2025M3", + "P2025M4", "P2025M5" + ) + + expect_equal( + calculate_zscore_ratios(test_metabolites_ratios_df, test_outlist_df, test_all_sample_ids)$HMDB_code, + c("HMDB000TT1", "HMDB000TT2", "HMDB000TT3") + ) + expect_equal( + calculate_zscore_ratios(test_metabolites_ratios_df, test_outlist_df, test_all_sample_ids)$C101.1, + c(-0.2630344, -0.1069152, 11.8533096) + ) + expect_equal( + calculate_zscore_ratios(test_metabolites_ratios_df, test_outlist_df, test_all_sample_ids)$C101.1_Zscore, + c(-0.5899371, 0.4858991, -0.4552026), + tolerance = 0.0001 + ) +}) + +testthat::test_that("get_list_page_plot_data: Get a list of dataframes for each chunk", { + test_metabolite_class_patients_df <- read.delim(test_path("fixtures/GenerateViolinPlots", + "test_metabolite_class_patients_df.txt")) + test_metabolite_class_controls_df <- read.delim(test_path("fixtures/GenerateViolinPlots", + "test_metabolite_class_controls_df.txt")) + test_nr_plots_perpage <- 2 + test_metabolite_in_chunks <- list( + c("HMDB001", "HMDB002", "HMDB003"), + c("HMDB004", "HMDB011", "HMDB012"), + c("HMDB000TT1", "HMDB000TT2", "HMDB000TT3") + ) + + t <- get_list_page_plot_data( + test_metabolite_in_chunks, + test_metabolite_class_patients_df, + test_metabolite_class_controls_df, + test_nr_plots_perpage + ) + + expect_type(get_list_page_plot_data( + test_metabolite_in_chunks, + test_metabolite_class_patients_df, + test_metabolite_class_controls_df, + test_nr_plots_perpage + ), "list") + + expect_equal(length(get_list_page_plot_data( + test_metabolite_in_chunks, + test_metabolite_class_patients_df, + test_metabolite_class_controls_df, + test_nr_plots_perpage + )), 3) + + test_plot_data_list <- get_list_page_plot_data( + test_metabolite_in_chunks, + test_metabolite_class_patients_df, + test_metabolite_class_controls_df, + test_nr_plots_perpage + ) + for (num_chunk in seq_along(test_metabolite_in_chunks)) { + expect_equal(unique(test_plot_data_list[[num_chunk]]$HMDB_name), test_metabolite_in_chunks[[num_chunk]]) + expect_equal(unique(test_plot_data_list[[num_chunk]]$Sample), + c("P2025M1", "P2025M2", "P2025M3", "C101.1", "C102.1", "C103.1")) + } + +}) + +testthat::test_that("make_and_save_violin_plot_pdfs: Make and save violin plots for each patient in a PDF", { + test_zscore_patients_df <- read.delim(test_path("fixtures/GenerateViolinPlots", "test_zscore_patients_df.txt")) + test_zscore_controls_df <- read.delim(test_path("fixtures/GenerateViolinPlots", "test_zscore_controls_df.txt")) + test_path_metabolite_groups <- test_path("fixtures/test_metabolite_groups") + test_nr_plots_perpage <- 2 + test_number_of_samples <- list( + controls = 5, + patients = 5 + ) + test_run_name <- "unit_test" + test_protocol_name <- "UNIT_TEST_PROTOCOL" + test_explanation_violin_plot <- "Unit test violin plot pdfs" + test_number_of_metabolites <- list( + highest = 2, + lowest = 1 + ) + + expect_silent(make_and_save_violin_plot_pdfs( + test_zscore_patients_df, + test_zscore_controls_df, + test_path_metabolite_groups, + test_nr_plots_perpage, + test_number_of_samples, + test_run_name, + test_protocol_name, + test_explanation_violin_plot, + test_number_of_metabolites + )) + + patient_ids <- c("P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5") + for (patient_id in patient_ids) { + pdf_file_name_diagnotics <- file.path(paste0("Diagnostics/MB", gsub("^P|M", "", patient_id), "_DIMS_PL_DIAG.pdf")) + pdf_file_name_other <- file.path(paste0("Other/R_", patient_id, ".pdf")) + + expect_true(file.exists(pdf_file_name_diagnotics)) + expect_true(file.exists(pdf_file_name_other)) + } + expect_true(file.exists("output_Helix_unit_test.csv")) + + unlink(c("Diagnostics", 'Other'), recursive = TRUE) + file.remove("output_Helix_unit_test.csv") +}) + +testthat::test_that("get_probabilities_top_iems: Get the IEM probabilities for a patient for all diseases", { + test_patient_top_iems_probs <- data.frame( + Disease = c("Disease A", "Disease B", "Disease C", "Disease D"), + P2025M1 = c(100, 75, 50, 25) + ) + test_expected_biomarkers_df <- read.delim(test_path("fixtures", "test_expected_biomarkers_df.txt")) + test_patient_id <- "P2025M1" + + + expect_type(get_probabilities_top_iems(test_patient_top_iems_probs, test_expected_biomarkers_df, test_patient_id), + "list") + + expect_equal(length(get_probabilities_top_iems(test_patient_top_iems_probs, test_expected_biomarkers_df, test_patient_id)), + 4) + expect_equal(names(get_probabilities_top_iems(test_patient_top_iems_probs, test_expected_biomarkers_df, test_patient_id)), + c("Disease A, probability score 100", "Disease B, probability score 75", "Disease C, probability score 50", + "Disease D, probability score 25")) + + expect_equal(get_probabilities_top_iems(test_patient_top_iems_probs, + test_expected_biomarkers_df, + test_patient_id)$"Disease A, probability score 100", + data.frame( + HMDB_code = c("HMDB002", "HMDB012"), + HMDB_name = c("metab2", "metab12") + )) + +}) + +testthat::test_that("make_and_save_diem_plots: Make and save dIEM plots", { + test_diem_probability_score <- data.frame( + Disease = c("Disease A", "Disease B", "Disease C", "Disease D", "Disease E"), + P2025M1 = c(100, 75, 50, 25, 12.5), + P2025M2 = c(25, 0, 2, 8, 3), + P2025M3 = c(0, 1, 2, 3, 4) + ) + test_patient_ids <- c("P2025M1", "P2025M2", "P2025M3") + test_expected_biomarkers_df <- read.delim(test_path("fixtures", "test_expected_biomarkers_df.txt")) + test_zscore_patients_df <- read.delim(test_path("fixtures/GenerateViolinPlots", "test_zscore_patients_df.txt")) + test_zscore_controls_df <- read.delim(test_path("fixtures/GenerateViolinPlots", "test_zscore_controls_df.txt")) + test_nr_plots_perpage <- 2 + test_number_of_samples <- list( + controls = 5, + patients = 5 + ) + test_number_of_metabolites <- list( + highest = 2, + lowest = 1 + ) + test_iem_variables <- list( + top_number_iem_diseases = 5, + threshold_iem = 5 + ) + + expect_silent(make_and_save_diem_plots( + test_diem_probability_score, + test_patient_ids, + test_expected_biomarkers_df, + test_zscore_patients_df, + test_zscore_controls_df, + test_nr_plots_perpage, + test_number_of_samples, + test_number_of_metabolites, + test_iem_variables + )) + + expect_equal(make_and_save_diem_plots( + test_diem_probability_score, + test_patient_ids, + test_expected_biomarkers_df, + test_zscore_patients_df, + test_zscore_controls_df, + test_nr_plots_perpage, + test_number_of_samples, + test_number_of_metabolites, + test_iem_variables + ), "P2025M3") + + expect_true(file.exists("dIEM_plots/IEM_P2025M1.pdf")) + expect_true(file.exists("dIEM_plots/IEM_P2025M2.pdf")) + + unlink("dIEM_plots/", recursive = TRUE) +}) + +testthat::test_that("make_metabolite_order: Make the order of metabolites for the violin plots", { + test_metabolites_vector <- c("metab1", "metab2", "metab3") + test_num_plots_per_page <- 5 + + make_metabolite_order(test_num_plots_per_page, test_metabolites_vector) + + expect_equal(length(make_metabolite_order(test_num_plots_per_page, test_metabolites_vector)), 5) + expect_equal(make_metabolite_order(test_num_plots_per_page, test_metabolites_vector), + c("metab1", "metab2", "metab3", " ", " ")) +}) + +testthat::test_that("pad_truncate_hmdb_names: Pad or truncate HMDB names to a fixed width", { + test_dataframe <- data.frame( + HMDB_name = c("metab1", "metabolite2", "metabo3") + ) + test_width <- 10 + test_pad_character <- "+" + + expect_equal(nrow(pad_truncate_hmdb_names(test_dataframe, test_width, test_pad_character)), + 3) + expect_equal(pad_truncate_hmdb_names(test_dataframe, test_width, test_pad_character)$HMDB_name, + c("metab1++++", "metabol...", "metabo3+++")) + + test_df <- pad_truncate_hmdb_names(test_dataframe, test_width, test_pad_character) + for (row in seq(nrow(test_df))) { + expect_equal(nchar(test_df[row, "HMDB_name"]), 10) + } +}) + +testthat::test_that("save_patient_no_iem: Save a list of patient IDs to a text file", { + local_edition(3) + test_threshold_iem <- 5 + test_patient_no_iem <- c("Patient1", "Patient2") + + expect_silent(save_patient_no_iem(test_threshold_iem, test_patient_no_iem)) + + expect_true(file.exists("missing_probability_scores.txt")) + + expect_snapshot(save_patient_no_iem(test_threshold_iem, test_patient_no_iem)) }) From 19ed90786a17845c1ddf189c95a109211b758c66 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 26 Feb 2026 15:56:57 +0100 Subject: [PATCH 130/161] Changed and added new test dataframes --- .../make_test_data_GenerateViolinPlots.R | 108 ++++++++++++++++++ .../testthat/fixtures/make_test_outlist_df.R | 2 +- 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/DIMS/tests/testthat/fixtures/make_test_data_GenerateViolinPlots.R b/DIMS/tests/testthat/fixtures/make_test_data_GenerateViolinPlots.R index 0664e00b..e435fa35 100644 --- a/DIMS/tests/testthat/fixtures/make_test_data_GenerateViolinPlots.R +++ b/DIMS/tests/testthat/fixtures/make_test_data_GenerateViolinPlots.R @@ -1,5 +1,52 @@ ### Functions used to create mock dataframes used for unit testing of GenerateViolinPlots ### +make_outlist_df <- function() { + test_outlist_df <- data.frame( + plots = NA, + C101.1 = c(1000, 1200, 1300, 1400, 1500, 1600), + C102.1 = c(1100, 1700, 925, 1125, 1200, 1050), + C103.1 = c(1300, 750, 1000, 1220, 1100, 1200), + C104.1 = c(1650, 925, 1600, 1650, 1025, 1150), + C105.1 = c(180000, 1950, 750, 15050, 1100, 1300), + P2025M1 = c(1000, 1200, 1300, 1400, 1100, 975), + P2025M2 = c(1100, 1700, 925, 1125, 1050, 1175), + P2025M3 = c(1300, 750, 1000, 1220, 975, 1100), + P2025M4 = c(1650, 925, 1600, 1650, 1700, 1750), + P2025M5 = c(180000, 1950, 750, 15050, 10000, 1500), + HMDB_name = c("metab1", "metab2", "metab3", "metab4", "metab5", "metab6"), + HMDB_name_all = NA, + HMDB_ID_all = NA, + sec_HMDB_ID = NA, + HMDB_key = NA, + sec_HMDB_ID_rlvnc = NA, + name = NA, + relevance = NA, + descr = NA, + origin = NA, + fluids = NA, + tissue = NA, + disease = NA, + pathway = NA, + HMDB_code = c("HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012"), + avg_ctrls = c(37010, 1305, 1115, 4089, 1185, 1260), + sd_ctrls = c(79934.23, 508.80, 336.15, 6130.65, 186.75, 210.36), + nr_ctrls = c(25, 26, 27, 28, 29, 30), + C101.1_Zscore = c(0.45, 1.67, -1.86, 0.58, 2.47, -0.56), + C102.1_Zscore = c(2.89, 0.79, -1.88, 5.46, -0.68, 1.65), + C103.1_Zscore = c(0.54, -0.85, 1.58, 3.84, 0.84, -1.11), + C104.1_Zscore = c(0.53, 1.84, 0.35, -0.54, 1.48, 0.43), + C105.1_Zscore = c(3.46, -1.31, 0.14, -0.15, 1.48, 0.36), + P2025M1_Zscore = c(0.31, 1.84, 2.34, 0.84, -0.46, 0.14), + P2025M2_Zscore = c(2.45, 0.48, 1.45, -0.15, -1.51, 3.56), + P2025M3_Zscore = c(2.14, 0.15, -1.44, -0.78, 1.68, 0.51), + P2025M4_Zscore = c(12.18, 2.48, -0.18, 0.84, 1.48, -2.45), + P2025M5_Zscore = c(3.22, 0.48, -3.18, 0.47, 1.18, 2.14) + ) + rownames(test_outlist_df) <- c("HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012") + + write.table(test_outlist_df, file = "tests/testthat/fixtures/GenerateViolinPlots/test_outlist_df.txt", sep = "\t") +} + make_intensities_zscore_df <- function() { test_intensities_zscore_df <- data.frame( HMDB_code = c("HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012"), @@ -258,6 +305,8 @@ make_test_expected_biomark_df <- function() { test_expected_biomarkers_df <- data.frame( HMDB_code = c("HMDB002", "HMDB002", "HMDB005", "HMDB005", "HMDB005", "HMDB009", "HMDB012", "HMDB012", "HMDB020", "HMDB020", "HMDB025", "HMDB025", "HMDB025", "HMDB028", "HMDB028"), + HMDB_name = c("metab2", "metab2", "metab5", "metab5", "metab5", "metab9", "metab12", "metab12", "metab20", "metab20", + "metab25", "metab25", "metab25", "metab28", "metab28"), Disease = c("Disease A", "Disease B", "Disease B", "Disease B", "Disease C", "Disease D", "Disease A", "Disease E", "Disease F", "Disease C", "Disease F", "Disease G", "Disease D", "Disease G", "Disease E"), M.z = c("1.2", "1.2", "2.5", "2.5", "2.5", "3.0", "4.5", "4.5", "2.5", "2.5", "5.1", "5.1", "5.1", "5.8", "5.8"), @@ -288,3 +337,62 @@ make_test_probability_score_df <- function() { write.table(test_probability_score_df, sep = "\t", quote = FALSE, row.names = FALSE, file = "tests/testthat/fixtures/test_probability_score_df.txt") } + +make_zscore_dfs <- function() { + test_zscore_patients_df <- data.frame( + HMDB_code = c("HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012", "HMDB000TT1", "HMDB000TT2", "HMDB000TT3"), + HMDB_name = c("metab1", "metab2", "metab3", "metab4", "metab5", "metab6", "Test_ratio1", "Test_ratio2", "Test_ratio3"), + P2025M1 = c(0.31, 1.84, 2.34, 0.84, -0.46, 0.14, -0.58, 0.48, -0.45), + P2025M2 = c(2.45, 0.48, 1.45, -0.15, -1.51, 3.56, -0.71, 0.39, -0.54), + P2025M3 = c(2.14, 0.15, -1.44, -0.78, 1.68, 0.51, -0.22, 0.38, -0.48), + P2025M4 = c(12.18, 2.48, -0.18, 0.84, 1.48, -2.45, -0.21, 0.51, -0.29), + P2025M5 = c(3.22, 0.48, -3.18, 0.47, 1.18, 2.14, 1.74, -1.78, 1.78) + ) + + test_zscore_controls_df <- data.frame( + HMDB_code = c("HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012", "HMDB000TT1", "HMDB000TT2", "HMDB000TT3"), + HMDB_name = c("metab1", "metab2", "metab3", "metab4", "metab5", "metab6", "Test_ratio1", "Test_ratio2", "Test_ratio3"), + C101.1 = c(0.45, 1.67, -1.86, 0.58, 2.47, -0.56, -0.58, 0.48, -0.45), + C102.1 = c(2.89, 0.79, -1.88, 5.46, -0.68, 1.65, -0.71, 0.39, -0.54), + C103.1 = c(0.54, -0.85, 1.58, 3.84, 0.84, -1.11, -0.22, 0.38, -0.48), + C104.1 = c(0.53, 1.84, 0.35, -0.54, 1.48, 0.43, -0.21, 0.51, -0.29), + C105.1 = c(3.46, -1.31, 0.14, -0.15, 1.48, 0.36, 1.74, -1.78, 1.78) + ) + + write.table(test_zscore_patients_df, sep = "\t", quote = FALSE, row.names = FALSE, + file = "tests/testthat/fixtures/GenerateViolinPlots/test_zscore_patients_df.txt") + write.table(test_zscore_controls_df, sep = "\t", quote = FALSE, row.names = FALSE, + file = "tests/testthat/fixtures/GenerateViolinPlots/test_zscore_controls_df.txt") +} + +make_test_metabolite_class_dfs <- function() { + test_metabolite_class_patients_df <- data.frame( + HMDB_name = c("HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012", "HMDB000TT1", "HMDB000TT2", "HMDB000TT3", + "HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012", "HMDB000TT1", "HMDB000TT2", "HMDB000TT3", + "HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012", "HMDB000TT1", "HMDB000TT2", "HMDB000TT3"), + Sample = c("P2025M1", "P2025M1", "P2025M1", "P2025M1", "P2025M1", "P2025M1", "P2025M1", "P2025M1", "P2025M1", + "P2025M2", "P2025M2", "P2025M2", "P2025M2", "P2025M2", "P2025M2", "P2025M2", "P2025M2", "P2025M2", + "P2025M3", "P2025M3", "P2025M3", "P2025M3", "P2025M3", "P2025M3", "P2025M3", "P2025M3", "P2025M3"), + Z_score = c(0.31, 1.84, 2.34, 0.84, -0.46, 0.14, -0.58, 0.48, -0.45, + 2.45, 0.48, 1.45, -0.15, -1.51, 3.56, -0.71, 0.39, -0.54, + 2.14, 0.15, -1.44, -0.78, 1.68, 0.51, -0.22, 0.38, -0.48) + ) + + test_metabolite_class_controls_df <- data.frame( + HMDB_name = c("HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012", "HMDB000TT1", "HMDB000TT2", "HMDB000TT3", + "HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012", "HMDB000TT1", "HMDB000TT2", "HMDB000TT3", + "HMDB001", "HMDB002", "HMDB003", "HMDB004", "HMDB011", "HMDB012", "HMDB000TT1", "HMDB000TT2", "HMDB000TT3"), + Sample = c("C101.1", "C101.1", "C101.1", "C101.1", "C101.1", "C101.1", "C101.1", "C101.1", "C101.1", + "C102.1", "C102.1", "C102.1", "C102.1", "C102.1", "C102.1", "C102.1", "C102.1", "C102.1", + "C103.1", "C103.1", "C103.1", "C103.1", "C103.1", "C103.1", "C103.1", "C103.1", "C103.1"), + Z_score = c(0.45, 1.67, -1.86, 0.58, 2.47, -0.56, -0.58, 0.48, -0.45, + 2.89, 0.79, -1.88, 5.46, -0.68, 1.65, -0.71, 0.39, -0.54, + 0.54, -0.85, 1.58, 3.84, 0.84, -1.11, -0.22, 0.38, -0.48) + ) + + write.table(test_metabolite_class_patients_df, sep = "\t", quote = FALSE, row.names = FALSE, + file = "tests/testthat/fixtures/GenerateViolinPlots/test_metabolite_class_patients_df.txt") + write.table(test_metabolite_class_controls_df, sep = "\t", quote = FALSE, row.names = FALSE, + file = "tests/testthat/fixtures/GenerateViolinPlots/test_metabolite_class_controls_df.txt") + +} diff --git a/DIMS/tests/testthat/fixtures/make_test_outlist_df.R b/DIMS/tests/testthat/fixtures/make_test_outlist_df.R index f5ee8b47..d1b451a0 100644 --- a/DIMS/tests/testthat/fixtures/make_test_outlist_df.R +++ b/DIMS/tests/testthat/fixtures/make_test_outlist_df.R @@ -24,7 +24,7 @@ make_test_outlist_df <- function() { HMDB_ID_all = c("HMDB001;HMDB011", "HMDB002", "HMDB003;HMDB013", "HMDB004"), sec_HMDB_ID = c("HMDB1;HMDB11", "", "HMDB3;HMDB13", "HMDB4"), HMDB_key = c("HMDB001", "HMDB002", "HMDB003", "HMDB004"), - sec_HMDB_ID_rlvc = c(c("HMDB1 | HMDB11", "HMDB2", "HMDB3", "HMDB4")), + sec_HMDB_ID_rlvnc = c(c("HMDB1 | HMDB11", "HMDB2", "HMDB3", "HMDB4")), name = c("metab_1 | metab_11", "metab_2", "metab_3", "metab_4"), relevance = c("Endogenous, relevant", "Endogenous, relevant | Exogenous", "Endogenous, relevant", "Endogenous, relevant"), descr = c("descr1", "descr2", "descr3", "descr4"), From fb40e09a6084238b166d75ed3b1dacd942aad134 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 26 Feb 2026 15:57:41 +0100 Subject: [PATCH 131/161] Changed and added new test files --- .../test_metabolite_class_controls_df.txt | 28 ++++++++++++++++ .../test_metabolite_class_patients_df.txt | 28 ++++++++++++++++ .../GenerateViolinPlots/test_outlist_df.txt | 7 ++++ .../test_zscore_controls_df.txt | 10 ++++++ .../test_zscore_patients_df.txt | 10 ++++++ .../fixtures/test_expected_biomarkers_df.txt | 32 +++++++++---------- .../test_acyl_carnitines.txt | 2 +- .../{ => Diagnostics}/test_crea_gua.txt | 0 DIMS/tests/testthat/fixtures/test_outlist.txt | 2 +- 9 files changed, 101 insertions(+), 18 deletions(-) create mode 100644 DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_metabolite_class_controls_df.txt create mode 100644 DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_metabolite_class_patients_df.txt create mode 100644 DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_outlist_df.txt create mode 100644 DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_zscore_controls_df.txt create mode 100644 DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_zscore_patients_df.txt rename DIMS/tests/testthat/fixtures/test_metabolite_groups/{ => Diagnostics}/test_acyl_carnitines.txt (77%) rename DIMS/tests/testthat/fixtures/test_metabolite_groups/{ => Diagnostics}/test_crea_gua.txt (100%) diff --git a/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_metabolite_class_controls_df.txt b/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_metabolite_class_controls_df.txt new file mode 100644 index 00000000..9b04311f --- /dev/null +++ b/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_metabolite_class_controls_df.txt @@ -0,0 +1,28 @@ +HMDB_name Sample Z_score +HMDB001 C101.1 0.45 +HMDB002 C101.1 1.67 +HMDB003 C101.1 -1.86 +HMDB004 C101.1 0.58 +HMDB011 C101.1 2.47 +HMDB012 C101.1 -0.56 +HMDB000TT1 C101.1 -0.58 +HMDB000TT2 C101.1 0.48 +HMDB000TT3 C101.1 -0.45 +HMDB001 C102.1 2.89 +HMDB002 C102.1 0.79 +HMDB003 C102.1 -1.88 +HMDB004 C102.1 5.46 +HMDB011 C102.1 -0.68 +HMDB012 C102.1 1.65 +HMDB000TT1 C102.1 -0.71 +HMDB000TT2 C102.1 0.39 +HMDB000TT3 C102.1 -0.54 +HMDB001 C103.1 0.54 +HMDB002 C103.1 -0.85 +HMDB003 C103.1 1.58 +HMDB004 C103.1 3.84 +HMDB011 C103.1 0.84 +HMDB012 C103.1 -1.11 +HMDB000TT1 C103.1 -0.22 +HMDB000TT2 C103.1 0.38 +HMDB000TT3 C103.1 -0.48 diff --git a/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_metabolite_class_patients_df.txt b/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_metabolite_class_patients_df.txt new file mode 100644 index 00000000..63e79a69 --- /dev/null +++ b/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_metabolite_class_patients_df.txt @@ -0,0 +1,28 @@ +HMDB_name Sample Z_score +HMDB001 P2025M1 0.31 +HMDB002 P2025M1 1.84 +HMDB003 P2025M1 2.34 +HMDB004 P2025M1 0.84 +HMDB011 P2025M1 -0.46 +HMDB012 P2025M1 0.14 +HMDB000TT1 P2025M1 -0.58 +HMDB000TT2 P2025M1 0.48 +HMDB000TT3 P2025M1 -0.45 +HMDB001 P2025M2 2.45 +HMDB002 P2025M2 0.48 +HMDB003 P2025M2 1.45 +HMDB004 P2025M2 -0.15 +HMDB011 P2025M2 -1.51 +HMDB012 P2025M2 3.56 +HMDB000TT1 P2025M2 -0.71 +HMDB000TT2 P2025M2 0.39 +HMDB000TT3 P2025M2 -0.54 +HMDB001 P2025M3 2.14 +HMDB002 P2025M3 0.15 +HMDB003 P2025M3 -1.44 +HMDB004 P2025M3 -0.78 +HMDB011 P2025M3 1.68 +HMDB012 P2025M3 0.51 +HMDB000TT1 P2025M3 -0.22 +HMDB000TT2 P2025M3 0.38 +HMDB000TT3 P2025M3 -0.48 diff --git a/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_outlist_df.txt b/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_outlist_df.txt new file mode 100644 index 00000000..3441ebaa --- /dev/null +++ b/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_outlist_df.txt @@ -0,0 +1,7 @@ +"plots" "C101.1" "C102.1" "C103.1" "C104.1" "C105.1" "P2025M1" "P2025M2" "P2025M3" "P2025M4" "P2025M5" "HMDB_name" "HMDB_name_all" "HMDB_ID_all" "sec_HMDB_ID" "HMDB_key" "sec_HMDB_ID_rlvnc" "name" "relevance" "descr" "origin" "fluids" "tissue" "disease" "pathway" "HMDB_code" "avg_ctrls" "sd_ctrls" "nr_ctrls" "C101.1_Zscore" "C102.1_Zscore" "C103.1_Zscore" "C104.1_Zscore" "C105.1_Zscore" "P2025M1_Zscore" "P2025M2_Zscore" "P2025M3_Zscore" "P2025M4_Zscore" "P2025M5_Zscore" +"HMDB001" NA 1000 1100 1300 1650 180000 1000 1100 1300 1650 180000 "metab1" NA NA NA NA NA NA NA NA NA NA NA NA NA "HMDB001" 37010 79934.23 25 0.45 2.89 0.54 0.53 3.46 0.31 2.45 2.14 12.18 3.22 +"HMDB002" NA 1200 1700 750 925 1950 1200 1700 750 925 1950 "metab2" NA NA NA NA NA NA NA NA NA NA NA NA NA "HMDB002" 1305 508.8 26 1.67 0.79 -0.85 1.84 -1.31 1.84 0.48 0.15 2.48 0.48 +"HMDB003" NA 1300 925 1000 1600 750 1300 925 1000 1600 750 "metab3" NA NA NA NA NA NA NA NA NA NA NA NA NA "HMDB003" 1115 336.15 27 -1.86 -1.88 1.58 0.35 0.14 2.34 1.45 -1.44 -0.18 -3.18 +"HMDB004" NA 1400 1125 1220 1650 15050 1400 1125 1220 1650 15050 "metab4" NA NA NA NA NA NA NA NA NA NA NA NA NA "HMDB004" 4089 6130.65 28 0.58 5.46 3.84 -0.54 -0.15 0.84 -0.15 -0.78 0.84 0.47 +"HMDB011" NA 1500 1200 1100 1025 1100 1100 1050 975 1700 10000 "metab5" NA NA NA NA NA NA NA NA NA NA NA NA NA "HMDB011" 1185 186.75 29 2.47 -0.68 0.84 1.48 1.48 -0.46 -1.51 1.68 1.48 1.18 +"HMDB012" NA 1600 1050 1200 1150 1300 975 1175 1100 1750 1500 "metab6" NA NA NA NA NA NA NA NA NA NA NA NA NA "HMDB012" 1260 210.36 30 -0.56 1.65 -1.11 0.43 0.36 0.14 3.56 0.51 -2.45 2.14 diff --git a/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_zscore_controls_df.txt b/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_zscore_controls_df.txt new file mode 100644 index 00000000..b6bf5c1c --- /dev/null +++ b/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_zscore_controls_df.txt @@ -0,0 +1,10 @@ +HMDB_code HMDB_name C101.1 C102.1 C103.1 C104.1 C105.1 +HMDB001 metab1 0.45 2.89 0.54 0.53 3.46 +HMDB002 metab2 1.67 0.79 -0.85 1.84 -1.31 +HMDB004 metab4 0.58 5.46 3.84 -0.54 -0.15 +HMDB005 metab5 2.47 -0.68 0.84 1.48 1.48 +HMDB009 metab9 -1.86 -1.88 1.58 0.35 0.14 +HMDB012 metab12 -0.56 1.65 -1.11 0.43 0.36 +HMDB000TT1 Test_ratio1 -0.58 -0.71 -0.22 -0.21 1.74 +HMDB000TT2 Test_ratio2 0.48 0.39 0.38 0.51 -1.78 +HMDB000TT3 Test_ratio3 -0.45 -0.54 -0.48 -0.29 1.78 diff --git a/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_zscore_patients_df.txt b/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_zscore_patients_df.txt new file mode 100644 index 00000000..b457af02 --- /dev/null +++ b/DIMS/tests/testthat/fixtures/GenerateViolinPlots/test_zscore_patients_df.txt @@ -0,0 +1,10 @@ +HMDB_code HMDB_name P2025M1 P2025M2 P2025M3 P2025M4 P2025M5 +HMDB001 metab1 0.31 2.45 2.14 12.18 3.22 +HMDB002 metab2 1.84 0.48 0.15 2.48 0.48 +HMDB004 metab4 0.84 -0.15 -0.78 0.84 0.47 +HMDB005 metab5 -0.46 -1.51 1.68 1.48 1.18 +HMDB009 metab9 2.34 1.45 -1.44 -0.18 -3.18 +HMDB012 metab12 0.14 3.56 0.51 -2.45 2.14 +HMDB000TT1 Test_ratio1 -0.58 -0.71 -0.22 -0.21 1.74 +HMDB000TT2 Test_ratio2 0.48 0.39 0.38 0.51 -1.78 +HMDB000TT3 Test_ratio3 -0.45 -0.54 -0.48 -0.29 1.78 diff --git a/DIMS/tests/testthat/fixtures/test_expected_biomarkers_df.txt b/DIMS/tests/testthat/fixtures/test_expected_biomarkers_df.txt index 9fb0bb0b..4038e7f9 100644 --- a/DIMS/tests/testthat/fixtures/test_expected_biomarkers_df.txt +++ b/DIMS/tests/testthat/fixtures/test_expected_biomarkers_df.txt @@ -1,16 +1,16 @@ -HMDB_code Disease M.z Change Total_Weight Absolute_Weight Dispensability -HMDB002 Disease A 1.2 Increase 10 10 Dispensable -HMDB002 Disease B 1.2 Decrease -1.5 1.5 Dispensable -HMDB005 Disease B 2.5 Increase 2 2 Indispensable -HMDB005 Disease B 2.5 Increase 5 5 Dispensable -HMDB005 Disease C 2.5 Decrease -2.5 2.5 Dispensable -HMDB009 Disease D 3.0 Decrease -3 3 Indispensable -HMDB012 Disease A 4.5 Increase 14.5 14.5 Dispensable -HMDB012 Disease E 4.5 Increase 4 4 Dispensable -HMDB020 Disease F 2.5 Decrease -7.5 7.5 Indispensable -HMDB020 Disease C 2.5 Increase 6 6 Indispensable -HMDB025 Disease F 5.1 Increase 20 20 Dispensable -HMDB025 Disease G 5.1 Decrease -5 5 Dispensable -HMDB025 Disease D 5.1 Increase 3 3 Dispensable -HMDB028 Disease G 5.8 Decrease -1.5 1.5 Dispensable -HMDB028 Disease E 5.8 Increase 4 4 Indispensable +HMDB_code HMDB_name Disease M.z Change Total_Weight Absolute_Weight Dispensability +HMDB002 metab2 Disease A 1.2 Increase 10 10 Dispensable +HMDB002 metab2 Disease B 1.2 Decrease -1.5 1.5 Dispensable +HMDB005 metab5 Disease B 2.5 Increase 2 2 Indispensable +HMDB005 metab5 Disease B 2.5 Increase 5 5 Dispensable +HMDB005 metab5 Disease C 2.5 Decrease -2.5 2.5 Dispensable +HMDB009 metab9 Disease D 3.0 Decrease -3 3 Indispensable +HMDB012 metab12 Disease A 4.5 Increase 14.5 14.5 Dispensable +HMDB012 metab12 Disease E 4.5 Increase 4 4 Dispensable +HMDB020 metab20 Disease F 2.5 Decrease -7.5 7.5 Indispensable +HMDB020 metab20 Disease C 2.5 Increase 6 6 Indispensable +HMDB025 metab25 Disease F 5.1 Increase 20 20 Dispensable +HMDB025 metab25 Disease G 5.1 Decrease -5 5 Dispensable +HMDB025 metab25 Disease D 5.1 Increase 3 3 Dispensable +HMDB028 metab28 Disease G 5.8 Decrease -1.5 1.5 Dispensable +HMDB028 metab28 Disease E 5.8 Increase 4 4 Indispensable diff --git a/DIMS/tests/testthat/fixtures/test_metabolite_groups/test_acyl_carnitines.txt b/DIMS/tests/testthat/fixtures/test_metabolite_groups/Diagnostics/test_acyl_carnitines.txt similarity index 77% rename from DIMS/tests/testthat/fixtures/test_metabolite_groups/test_acyl_carnitines.txt rename to DIMS/tests/testthat/fixtures/test_metabolite_groups/Diagnostics/test_acyl_carnitines.txt index 03a6ad6b..51c9852c 100644 --- a/DIMS/tests/testthat/fixtures/test_metabolite_groups/test_acyl_carnitines.txt +++ b/DIMS/tests/testthat/fixtures/test_metabolite_groups/Diagnostics/test_acyl_carnitines.txt @@ -1,4 +1,4 @@ HMDB_code HMDB_name Helix Helix_naam high_zscore low_zscore HMDB001 metab1 ja Metab_1 2 -1.5 HMDB003 metab3 nee Metab_3 2 -1.5 -HMDBAA1 ratio1 ja Ratio_1 2 -1.5 +HMDB000TT1 ratio1 ja Ratio_1 2 -1.5 diff --git a/DIMS/tests/testthat/fixtures/test_metabolite_groups/test_crea_gua.txt b/DIMS/tests/testthat/fixtures/test_metabolite_groups/Diagnostics/test_crea_gua.txt similarity index 100% rename from DIMS/tests/testthat/fixtures/test_metabolite_groups/test_crea_gua.txt rename to DIMS/tests/testthat/fixtures/test_metabolite_groups/Diagnostics/test_crea_gua.txt diff --git a/DIMS/tests/testthat/fixtures/test_outlist.txt b/DIMS/tests/testthat/fixtures/test_outlist.txt index 957adc6a..43a638ac 100644 --- a/DIMS/tests/testthat/fixtures/test_outlist.txt +++ b/DIMS/tests/testthat/fixtures/test_outlist.txt @@ -1,4 +1,4 @@ -"plots" "C101.1" "C102.1" "C103.1" "C104.1" "C105.1" "C106.1" "C107.1" "C108.1" "C109.1" "C110.1" "C111.1" "C112.1" "P2.1" "P3.1" "HMDB_name" "HMDB_name_all" "HMDB_ID_all" "sec_HMDB_ID" "HMDB_key" "sec_HMDB_ID_rlvc" "name" "relevance" "descr" "origin" "fluids" "tissue" "disease" "pathway" "HMDB_code" +"plots" "C101.1" "C102.1" "C103.1" "C104.1" "C105.1" "C106.1" "C107.1" "C108.1" "C109.1" "C110.1" "C111.1" "C112.1" "P2.1" "P3.1" "HMDB_name" "HMDB_name_all" "HMDB_ID_all" "sec_HMDB_ID" "HMDB_key" "sec_HMDB_ID_rlvnc" "name" "relevance" "descr" "origin" "fluids" "tissue" "disease" "pathway" "HMDB_code" "HMDB001" NA 1000 1100 1300 1650 180000 1050 1150 1350 1450 1200 1050 1250 3000 5750 "metab_1" "metab_1;metab_11" "HMDB001;HMDB011" "HMDB1;HMDB11" "HMDB001" "HMDB1 | HMDB11" "metab_1 | metab_11" "Endogenous, relevant" "descr1" "Endogenous" "Blood" "Muscle" "disease 1" "pathway1" "HMDB001" "HMDB002" NA 1200 1700 750 925 1950 1100 1250 850 1025 950 1125 975 12500 2750 "metab_2" "metab_2" "HMDB002" "" "HMDB002" "HMDB2" "metab_2" "Endogenous, relevant | Exogenous" "descr2" "Endogenous | Exogenous" "Blood" "" "disease2" "pathway2" "HMDB002" "HMDB003" NA 1300 925 1000 1600 750 1200 825 1175 1500 1750 1300 1450 5500 6750 "metab_3" "metab_3;metab_13" "HMDB003;HMDB013" "HMDB3;HMDB13" "HMDB003" "HMDB3" "metab_3" "Endogenous, relevant" "descr3" "Endogenous" "Blood" "Prostate" "" "pathway3" "HMDB003" From 56c467d5cd11d46cac3a63c943e60460c14eedf7 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 26 Feb 2026 16:06:45 +0100 Subject: [PATCH 132/161] Fixed issues occuring during testing --- DIMS/GenerateQCOutput.R | 6 +++--- DIMS/GenerateViolinPlots.R | 2 +- DIMS/export/generate_violin_plots_functions.R | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DIMS/GenerateQCOutput.R b/DIMS/GenerateQCOutput.R index 78d815f3..15865a8c 100644 --- a/DIMS/GenerateQCOutput.R +++ b/DIMS/GenerateQCOutput.R @@ -12,8 +12,8 @@ cmd_args <- commandArgs(trailingOnly = TRUE) init_file <- cmd_args[1] project <- cmd_args[2] dims_matrix <- cmd_args[3] -sst_components_file <- cmd_args[5] -export_scripts_dir <- cmd_args[6] +sst_components_file <- cmd_args[4] +export_scripts_dir <- cmd_args[5] outdir <- "./" @@ -273,7 +273,7 @@ patterns <- c("^(P1002\\.)[[:digit:]]+_", "^(P1003\\.)[[:digit:]]+_", "^(P1005\\ positive_controls_index <- grepl(pattern = paste(patterns, collapse = "|"), column_list) positive_control_list <- column_list[positive_controls_index] -if (z_score == 1) { +if (positive_controls_index > 0) { # find if one or more positive control samples are missing pos_contr_warning <- c() if (all(sapply(c("^P1002", "^P1003", "^P1005"), diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index cad5d83d..e49b942c 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -81,7 +81,7 @@ make_and_save_violin_plot_pdfs( ) #### Run the IEM algorithm ######### -diem_probability_score <- run_diem_algorithm(expected_biomarkers_df, zscore_patients_df, patient_col_names) +diem_probability_score <- run_diem_algorithm(expected_biomarkers_df, zscore_patients_df, patient_ids) save_prob_scores_to_excel(diem_probability_score, run_name) diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index 83ad7cd7..33723c70 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -27,7 +27,7 @@ prepare_intensities_zscore_df <- function(intensities_zscore_df) { #' #' @returns sample_colnames: a vector of column names all containing the prefix. get_colnames_samples <- function(dataframe, sample_label) { - sample_colnames <- unique(gsub("_Zscore", "", grepv(paste0("^", sample_label), colnames(dataframe)))) + sample_colnames <- unique(gsub("_Zscore", "", grep(paste0("^", sample_label), colnames(dataframe), value = TRUE))) return(sample_colnames) } From 0d8a5efc60b8080e65395bd3584b9ead5047001b Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 26 Feb 2026 16:15:15 +0100 Subject: [PATCH 133/161] Fixed missing comma --- DIMS/GenerateQCOutput.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DIMS/GenerateQCOutput.R b/DIMS/GenerateQCOutput.R index 15865a8c..59dd697b 100644 --- a/DIMS/GenerateQCOutput.R +++ b/DIMS/GenerateQCOutput.R @@ -213,7 +213,7 @@ if (nrow(is_below_threshold) > 0) { row.names = FALSE, sep = "\t") } else { write.table("no internal standards are below threshold", - file = "internal_standards_below_threshold.txt" + file = "internal_standards_below_threshold.txt", row.names = FALSE, col.names = FALSE ) } From df0edf31d29f5be118f24a06539b5d598872535d Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 26 Feb 2026 16:32:31 +0100 Subject: [PATCH 134/161] Fixed missing variable and testing issues --- DIMS/GenerateViolinPlots.R | 3 ++- DIMS/export/generate_violin_plots_functions.R | 3 ++- .../testthat/test_generate_violin_plots.R | 24 ++++++++++++------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index e49b942c..3e20de41 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -95,7 +95,8 @@ patient_no_iem <- make_and_save_diem_plots( nr_plots_perpage, number_of_samples, number_of_metabolites, - iem_variables + iem_variables, + explanation_violin_plot ) if (length(patient_no_iem) > 0) { diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index 33723c70..154da4b2 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -877,7 +877,8 @@ make_and_save_diem_plots <- function( nr_plots_perpage, number_of_samples, number_of_metabolites, - iem_variables) { + iem_variables, + explanation_violin_plot) { diem_plot_dir <- paste("./dIEM_plots", sep = "/") dir.create(diem_plot_dir) diff --git a/DIMS/tests/testthat/test_generate_violin_plots.R b/DIMS/tests/testthat/test_generate_violin_plots.R index 5d0a818a..61c18672 100644 --- a/DIMS/tests/testthat/test_generate_violin_plots.R +++ b/DIMS/tests/testthat/test_generate_violin_plots.R @@ -11,7 +11,8 @@ source("../../export/generate_violin_plots_functions.R") testthat::test_that("get_intensities_fraction_side: Get intensities for calculating the ratios", { test_intensities_zscore_df <- read.delim(test_path("fixtures", "test_intensities_zscore_df.txt")) - + test_intensity_cols <- c("C101.1", "C102.1", "C103.1", "C104.1", "C105.1", + "P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5") test_ratios_metabs_df <- data.frame( HMDB.code = c("HMDBAA1", "HMDBAA2", "HMDBAB1"), Ratio_name = c("ratio1", "ratio2", "ratio3"), @@ -111,8 +112,8 @@ testthat::test_that("get_list_dataframes_from_dir: Get a list with dataframes fr }) testthat::test_that("merge_metabolite_info_zscores: Combine metabolite info dataframe and Z-score dataframe", { - test_acyl_carnitines_df <- read.delim(test_path("fixtures/test_metabolite_groups/", "test_acyl_carnitines.txt")) - test_crea_gua_df <- read.delim(test_path("fixtures/test_metabolite_groups/", "test_crea_gua.txt")) + test_acyl_carnitines_df <- read.delim(test_path("fixtures/test_metabolite_groups/Diagnostics", "test_acyl_carnitines.txt")) + test_crea_gua_df <- read.delim(test_path("fixtures/test_metabolite_groups/Diagnostics", "test_crea_gua.txt")) test_metab_list_all <- list(test_acyl_carnitines_df, test_crea_gua_df) names(test_metab_list_all) <- c("test_acyl_carnitines", "test_crea_gua") @@ -280,14 +281,14 @@ testthat::test_that("get_data_per_metabolite_class: Combine patient and control }) testthat::test_that("prepare_helix_patient_data: Generate a dataframe with information for Helix", { - test_acyl_carnitines_pat <- read.delim(test_path("fixtures/", "test_acyl_carnitines_patients.txt")) - test_crea_gua_pat <- read.delim(test_path("fixtures/", "test_crea_gua_patients.txt")) + test_acyl_carnitines_pat <- read.delim(test_path("fixtures", "test_acyl_carnitines_patients.txt")) + test_crea_gua_pat <- read.delim(test_path("fixtures", "test_crea_gua_patients.txt")) test_metab_interest_sorted <- list(test_acyl_carnitines_pat, test_crea_gua_pat) names(test_metab_interest_sorted) <- c("test_acyl_carnitines", "test_crea_gua") - test_acyl_carnitines_df <- read.delim(test_path("fixtures/test_metabolite_groups/", "test_acyl_carnitines.txt")) - test_crea_gua_df <- read.delim(test_path("fixtures/test_metabolite_groups/", "test_crea_gua.txt")) + test_acyl_carnitines_df <- read.delim(test_path("fixtures/test_metabolite_groups/Diagnostics", "test_acyl_carnitines.txt")) + test_crea_gua_df <- read.delim(test_path("fixtures/test_metabolite_groups/Diagnostics", "test_crea_gua.txt")) test_metab_list_all <- list(test_acyl_carnitines_df, test_crea_gua_df) names(test_metab_list_all) <- c("test_acyl_carnitines", "test_crea_gua") @@ -852,6 +853,7 @@ testthat::test_that("make_and_save_diem_plots: Make and save dIEM plots", { top_number_iem_diseases = 5, threshold_iem = 5 ) + test_explanation_violin_plot <- "Unit test violin plot pdfs" expect_silent(make_and_save_diem_plots( test_diem_probability_score, @@ -862,8 +864,10 @@ testthat::test_that("make_and_save_diem_plots: Make and save dIEM plots", { test_nr_plots_perpage, test_number_of_samples, test_number_of_metabolites, - test_iem_variables + test_iem_variables, + test_explanation_violin_plot )) + unlink("dIEM_plots/", recursive = TRUE) expect_equal(make_and_save_diem_plots( test_diem_probability_score, @@ -874,7 +878,8 @@ testthat::test_that("make_and_save_diem_plots: Make and save dIEM plots", { test_nr_plots_perpage, test_number_of_samples, test_number_of_metabolites, - test_iem_variables + test_iem_variables, + test_explanation_violin_plot ), "P2025M3") expect_true(file.exists("dIEM_plots/IEM_P2025M1.pdf")) @@ -922,4 +927,5 @@ testthat::test_that("save_patient_no_iem: Save a list of patient IDs to a text f expect_true(file.exists("missing_probability_scores.txt")) expect_snapshot(save_patient_no_iem(test_threshold_iem, test_patient_no_iem)) + file.remove("missing_probability_scores.txt") }) From 0a1eb5ce2f4c3fd0ad1174b4913cf19fcd8e0f44 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 26 Feb 2026 19:38:31 +0100 Subject: [PATCH 135/161] Fixed unit test issues --- DIMS/tests/testthat/test_generate_violin_plots.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DIMS/tests/testthat/test_generate_violin_plots.R b/DIMS/tests/testthat/test_generate_violin_plots.R index 61c18672..b709b656 100644 --- a/DIMS/tests/testthat/test_generate_violin_plots.R +++ b/DIMS/tests/testthat/test_generate_violin_plots.R @@ -926,6 +926,6 @@ testthat::test_that("save_patient_no_iem: Save a list of patient IDs to a text f expect_true(file.exists("missing_probability_scores.txt")) - expect_snapshot(save_patient_no_iem(test_threshold_iem, test_patient_no_iem)) + expect_snapshot_file("missing_probability_scores.txt") file.remove("missing_probability_scores.txt") }) From 34b26f77a62494276c05383ade7579074c18e08a Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 26 Feb 2026 19:38:46 +0100 Subject: [PATCH 136/161] Test files and snapshots --- .../testthat/_snaps/generate_violin_plots.md | 4 +-- .../missing_probability_scores.txt | 3 +++ .../Other/test_other.txt | 4 +++ DIMS/tests/testthat/test_evaluate_tics.R | 26 +++++++++---------- 4 files changed, 22 insertions(+), 15 deletions(-) create mode 100644 DIMS/tests/testthat/_snaps/generate_violin_plots/missing_probability_scores.txt create mode 100644 DIMS/tests/testthat/fixtures/test_metabolite_groups/Other/test_other.txt diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots.md b/DIMS/tests/testthat/_snaps/generate_violin_plots.md index 5df67e7a..4930649a 100644 --- a/DIMS/tests/testthat/_snaps/generate_violin_plots.md +++ b/DIMS/tests/testthat/_snaps/generate_violin_plots.md @@ -1,4 +1,4 @@ -# Create a pdf with a table of top metabolites and violin plots +# create_pdf_violin_plots: Create a pdf with a table of top metabolites and violin plots Code content_pdf_violinplots @@ -8,7 +8,7 @@ [3] " Results for patient P2025M1\n test crea gua\n metab4 Z=−0.46\nMetabolites\n metab11 Z=0.84\n −5 0 5 10 15 20\n Z−scores\n" [4] " Unit test Generate Violin Plots\nUnit test Generate Violin Plots\n" -# Saving the probability score dataframe as an Excel file +# save_prob_scores_to_excel: Saving the probability score dataframe as an Excel file Disease P2025M1 P2025M2 P2025M3 P2025M4 1 Disease A 10.900 -10.9 49.90 -49.9 diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots/missing_probability_scores.txt b/DIMS/tests/testthat/_snaps/generate_violin_plots/missing_probability_scores.txt new file mode 100644 index 00000000..f7e33571 --- /dev/null +++ b/DIMS/tests/testthat/_snaps/generate_violin_plots/missing_probability_scores.txt @@ -0,0 +1,3 @@ +The following patient(s) did not have dIEM probability scores higher than 5 : +Patient1 +Patient2 diff --git a/DIMS/tests/testthat/fixtures/test_metabolite_groups/Other/test_other.txt b/DIMS/tests/testthat/fixtures/test_metabolite_groups/Other/test_other.txt new file mode 100644 index 00000000..76013216 --- /dev/null +++ b/DIMS/tests/testthat/fixtures/test_metabolite_groups/Other/test_other.txt @@ -0,0 +1,4 @@ +HMDB_code HMDB_name Helix Helix_naam high_zscore low_zscore +HMDB004 metab4 ja Metab_4 2 -1.5 +HMDB011 metab11 ja Metab_11 2 -1.5 +HMDB000TT1 ratio1 ja Ratio_1 2 -1.5 diff --git a/DIMS/tests/testthat/test_evaluate_tics.R b/DIMS/tests/testthat/test_evaluate_tics.R index b474a81b..2682c398 100644 --- a/DIMS/tests/testthat/test_evaluate_tics.R +++ b/DIMS/tests/testthat/test_evaluate_tics.R @@ -7,15 +7,15 @@ source("../../preprocessing/evaluate_tics_functions.R") testthat::test_that("TICS are correctly accepted or rejected", { # It's necessary to copy/symlink the files to the current location for the combine_sum_adducts_parts function # local: setwd("~/Development/DIMS_refactor_PeakFinding_codereview/CustomModules/DIMS/tests/testthat") - test_files <- list.files("fixtures/", "test_evaluate_tics", full.names = TRUE) + test_files <- list.files("fixtures", "test_evaluate_tics", full.names = TRUE) file.symlink(file.path(test_files), getwd()) - + # create replication pattern to test on: technical_replicates <- paste0("test_evaluate_tics_file", 1:3) test_repl_pattern <- list(technical_replicates) names(test_repl_pattern) <- "sample1" test_thresh2remove <- 10^9 - + # test that output has two entries expect_equal(length(find_bad_replicates(test_repl_pattern, test_thresh2remove)), 2) # test that first technical replicate is removed in positive scan mode @@ -24,11 +24,12 @@ testthat::test_that("TICS are correctly accepted or rejected", { expect_equal(find_bad_replicates(test_repl_pattern, test_thresh2remove)$neg, "test_evaluate_tics_file3", TRUE) # test that output files are generated expect_equal(sum(grepl("miss_infusions_", list.files("./"))), 2) - + # Remove symlinked files - files_remove <- list.files("./", "SummedAdducts_test.RData", full.names = TRUE) + files_remove_tics <- list.files("./", "test_evaluate_tics_file", full.names = TRUE) + files_remove_miss <- list.files("./", "miss_infusions_", full.names = TRUE) + files_remove <- c(files_remove_tics, files_remove_miss) file.remove(files_remove) - }) # test remove_from_repl_pattern @@ -39,7 +40,7 @@ testthat::test_that("technical replicates are correctly removed from replication names(test_repl_pattern) <- "sample1" test_bad_samples <- "test_evaluate_tics_file2" test_nr_replicates <- 3 - + # test that the output contains 1 sample expect_equal(length(remove_from_repl_pattern(test_bad_samples, test_repl_pattern, test_nr_replicates)), 1) # test that the output for the sample contains 2 technical replicates @@ -57,10 +58,9 @@ testthat::test_that("overview of technical replicates is correctly created", { test_scanmode <- "positive" # test that overview is correctly created - expect_equal(get_overview_tech_reps(test_repl_pattern_filtered, test_scanmode)[ ,1], "sample1") - expect_equal(get_overview_tech_reps(test_repl_pattern_filtered, test_scanmode)[ ,3], "positive") - expect_true(get_overview_tech_reps(test_repl_pattern_filtered, test_scanmode)[ ,2] == - paste0(technical_replicates, collapse = ";"), TRUE) - -}) + expect_equal(get_overview_tech_reps(test_repl_pattern_filtered, test_scanmode)[, 1], "sample1") + expect_equal(get_overview_tech_reps(test_repl_pattern_filtered, test_scanmode)[, 3], "positive") + expect_true(get_overview_tech_reps(test_repl_pattern_filtered, test_scanmode)[, 2] == + paste0(technical_replicates, collapse = ";"), TRUE) +}) From b56fdfcdd9e069755e00bc38821a5f63e2ee931d Mon Sep 17 00:00:00 2001 From: ALuesink Date: Mon, 2 Mar 2026 10:52:15 +0100 Subject: [PATCH 137/161] Styling and linting --- DIMS/GenerateViolinPlots.R | 6 +- DIMS/export/generate_violin_plots_functions.R | 4 +- .../testthat/test_generate_violin_plots.R | 176 ++++++++++-------- 3 files changed, 106 insertions(+), 80 deletions(-) diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 3e20de41..0007881b 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -27,8 +27,10 @@ rm(outlist) metabolites_ratios_df <- read.csv(file_ratios_metabolites, sep = ";", stringsAsFactors = FALSE) expected_biomarkers_df <- read.csv(file_expected_biomarkers_iem, sep = ";", stringsAsFactors = FALSE) expected_biomarkers_df <- expected_biomarkers_df %>% - rename(HMDB_code = HMDB.code, - HMDB_name = Metabolite) + rename( + HMDB_code = HMDB.code, + HMDB_name = Metabolite + ) explanation_violin_plot <- readLines(file_explanation) # Set global variables diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index 154da4b2..378148e4 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -430,7 +430,7 @@ get_list_page_plot_data <- function( #' Create the order of metabolites and add empty strings if the number of metabolites is lower than #' the number of plots per page. #' -#' @param number_of_plots_per_page: integer containing the number of metabolites per plot per page +#' @param number_of_plots_per_page: integer containing the number of metabolites per plot per page #' @param metabolite_names_chunk: list of vectors, each containing metabolites #' #' @returns metabolite_order: a vector containing all metabolites and possibly empty strings @@ -963,7 +963,7 @@ get_probabilities_top_iems <- function(patient_top_iems_probs, expected_biomarke #' Save a list of patient IDs to a text file #' #' @param threshold_iem: integer containing the IEM threshold -#' @param patient_no_iem: vector containing patient IDs +#' @param patient_no_iem: vector containing patient IDs save_patient_no_iem <- function(threshold_iem, patient_no_iem) { patient_no_iem <- c( paste0( diff --git a/DIMS/tests/testthat/test_generate_violin_plots.R b/DIMS/tests/testthat/test_generate_violin_plots.R index b709b656..fd48cbe8 100644 --- a/DIMS/tests/testthat/test_generate_violin_plots.R +++ b/DIMS/tests/testthat/test_generate_violin_plots.R @@ -11,8 +11,10 @@ source("../../export/generate_violin_plots_functions.R") testthat::test_that("get_intensities_fraction_side: Get intensities for calculating the ratios", { test_intensities_zscore_df <- read.delim(test_path("fixtures", "test_intensities_zscore_df.txt")) - test_intensity_cols <- c("C101.1", "C102.1", "C103.1", "C104.1", "C105.1", - "P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5") + test_intensity_cols <- c( + "C101.1", "C102.1", "C103.1", "C104.1", "C105.1", + "P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5" + ) test_ratios_metabs_df <- data.frame( HMDB.code = c("HMDBAA1", "HMDBAA2", "HMDBAB1"), Ratio_name = c("ratio1", "ratio2", "ratio3"), @@ -370,7 +372,7 @@ testthat::test_that("transform_metab_df_to_helix_df: Make the output for Helix", ) }) -testthat::test_that("get_top_metabolites_df: Create a dataframe with all metabolites that exceed the min and max Z-score cutoff", { +testthat::test_that("get_top_metabolites_df: Create a dataframe with the top metabolites", { test_df_metabs_helix <- read.delim(test_path("fixtures/", "test_df_metabs_helix.txt")) test_patient_id <- "P2025M1" @@ -407,15 +409,15 @@ testthat::test_that("get_top_metabolites_df: Create a dataframe with all metabol testthat::test_that("prepare_toplist: Create a dataframe with the top 20 highest and top 10 lowest metabolites", { test_zscore_patient_df <- read.delim(test_path("fixtures/", "test_zscore_patient_df.txt")) test_patient_id <- "P2025M1" - test_num_of_highest_metabolites <- 20 - test_num_of_lowest_metabolites <- 10 + test_num_of_highest_metabs <- 20 + test_num_of_lowest_metabs <- 10 expect_equal( dim(prepare_toplist( test_patient_id, test_zscore_patient_df, - test_num_of_highest_metabolites, - test_num_of_lowest_metabolites + test_num_of_highest_metabs, + test_num_of_lowest_metabs )), c(32, 3) ) @@ -423,8 +425,8 @@ testthat::test_that("prepare_toplist: Create a dataframe with the top 20 highest colnames(prepare_toplist( test_patient_id, test_zscore_patient_df, - test_num_of_highest_metabolites, - test_num_of_lowest_metabolites + test_num_of_highest_metabs, + test_num_of_lowest_metabs )), c("HMDB_ID", "Metabolite", "Z-score") ) @@ -432,8 +434,8 @@ testthat::test_that("prepare_toplist: Create a dataframe with the top 20 highest prepare_toplist( test_patient_id, test_zscore_patient_df, - test_num_of_highest_metabolites, - test_num_of_lowest_metabolites + test_num_of_highest_metabs, + test_num_of_lowest_metabs )$HMDB_ID, c( "Increased", "HMDB030", "HMDB029", "HMDB028", "HMDB027", "HMDB026", "HMDB025", "HMDB024", "HMDB023", @@ -446,8 +448,8 @@ testthat::test_that("prepare_toplist: Create a dataframe with the top 20 highest prepare_toplist( test_patient_id, test_zscore_patient_df, - test_num_of_highest_metabolites, - test_num_of_lowest_metabolites + test_num_of_highest_metabs, + test_num_of_lowest_metabs )$`Z-score`, c( "", "30", "29", "28", "27", "26", "25", "24", "23", "22", "21", "20", "19", "18", "17", "16", "15", @@ -461,8 +463,8 @@ testthat::test_that("prepare_toplist: Create a dataframe with the top 20 highest prepare_toplist( test_patient_id, test_zscore_patient_df, - test_num_of_highest_metabolites, - test_num_of_lowest_metabolites + test_num_of_highest_metabs, + test_num_of_lowest_metabs )$Metabolite, c( "", "metab1", "metab2", "metab3", "metab4", "metab5", "metab6", "metab7", "metab8", "metab9", "metab10", @@ -475,8 +477,8 @@ testthat::test_that("prepare_toplist: Create a dataframe with the top 20 highest prepare_toplist( test_patient_id, test_zscore_patient_df, - test_num_of_highest_metabolites, - test_num_of_lowest_metabolites + test_num_of_highest_metabs, + test_num_of_lowest_metabs )$`Z-score`, c( "", "-1", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9", "-10", "-11", "-12", "-13", "-14", "-15", @@ -712,50 +714,55 @@ testthat::test_that("calculate_zscore_ratios: Calculate Zscores for ratios", { }) testthat::test_that("get_list_page_plot_data: Get a list of dataframes for each chunk", { - test_metabolite_class_patients_df <- read.delim(test_path("fixtures/GenerateViolinPlots", - "test_metabolite_class_patients_df.txt")) - test_metabolite_class_controls_df <- read.delim(test_path("fixtures/GenerateViolinPlots", - "test_metabolite_class_controls_df.txt")) + test_metab_class_patients_df <- read.delim(test_path( + "fixtures/GenerateViolinPlots", + "test_metabolite_class_patients_df.txt" + )) + test_metab_class_controls_df <- read.delim(test_path( + "fixtures/GenerateViolinPlots", + "test_metabolite_class_controls_df.txt" + )) test_nr_plots_perpage <- 2 test_metabolite_in_chunks <- list( c("HMDB001", "HMDB002", "HMDB003"), c("HMDB004", "HMDB011", "HMDB012"), c("HMDB000TT1", "HMDB000TT2", "HMDB000TT3") ) - + t <- get_list_page_plot_data( test_metabolite_in_chunks, - test_metabolite_class_patients_df, - test_metabolite_class_controls_df, + test_metab_class_patients_df, + test_metab_class_controls_df, test_nr_plots_perpage ) - + expect_type(get_list_page_plot_data( test_metabolite_in_chunks, - test_metabolite_class_patients_df, - test_metabolite_class_controls_df, + test_metab_class_patients_df, + test_metab_class_controls_df, test_nr_plots_perpage ), "list") - + expect_equal(length(get_list_page_plot_data( test_metabolite_in_chunks, - test_metabolite_class_patients_df, - test_metabolite_class_controls_df, + test_metab_class_patients_df, + test_metab_class_controls_df, test_nr_plots_perpage )), 3) - + test_plot_data_list <- get_list_page_plot_data( test_metabolite_in_chunks, - test_metabolite_class_patients_df, - test_metabolite_class_controls_df, + test_metab_class_patients_df, + test_metab_class_controls_df, test_nr_plots_perpage ) for (num_chunk in seq_along(test_metabolite_in_chunks)) { expect_equal(unique(test_plot_data_list[[num_chunk]]$HMDB_name), test_metabolite_in_chunks[[num_chunk]]) - expect_equal(unique(test_plot_data_list[[num_chunk]]$Sample), - c("P2025M1", "P2025M2", "P2025M3", "C101.1", "C102.1", "C103.1")) + expect_equal( + unique(test_plot_data_list[[num_chunk]]$Sample), + c("P2025M1", "P2025M2", "P2025M3", "C101.1", "C102.1", "C103.1") + ) } - }) testthat::test_that("make_and_save_violin_plot_pdfs: Make and save violin plots for each patient in a PDF", { @@ -786,18 +793,18 @@ testthat::test_that("make_and_save_violin_plot_pdfs: Make and save violin plots test_explanation_violin_plot, test_number_of_metabolites )) - + patient_ids <- c("P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5") for (patient_id in patient_ids) { pdf_file_name_diagnotics <- file.path(paste0("Diagnostics/MB", gsub("^P|M", "", patient_id), "_DIMS_PL_DIAG.pdf")) pdf_file_name_other <- file.path(paste0("Other/R_", patient_id, ".pdf")) - + expect_true(file.exists(pdf_file_name_diagnotics)) expect_true(file.exists(pdf_file_name_other)) } expect_true(file.exists("output_Helix_unit_test.csv")) - - unlink(c("Diagnostics", 'Other'), recursive = TRUE) + + unlink(c("Diagnostics", "Other"), recursive = TRUE) file.remove("output_Helix_unit_test.csv") }) @@ -808,25 +815,36 @@ testthat::test_that("get_probabilities_top_iems: Get the IEM probabilities for a ) test_expected_biomarkers_df <- read.delim(test_path("fixtures", "test_expected_biomarkers_df.txt")) test_patient_id <- "P2025M1" - - - expect_type(get_probabilities_top_iems(test_patient_top_iems_probs, test_expected_biomarkers_df, test_patient_id), - "list") - - expect_equal(length(get_probabilities_top_iems(test_patient_top_iems_probs, test_expected_biomarkers_df, test_patient_id)), - 4) - expect_equal(names(get_probabilities_top_iems(test_patient_top_iems_probs, test_expected_biomarkers_df, test_patient_id)), - c("Disease A, probability score 100", "Disease B, probability score 75", "Disease C, probability score 50", - "Disease D, probability score 25")) - - expect_equal(get_probabilities_top_iems(test_patient_top_iems_probs, - test_expected_biomarkers_df, - test_patient_id)$"Disease A, probability score 100", - data.frame( - HMDB_code = c("HMDB002", "HMDB012"), - HMDB_name = c("metab2", "metab12") - )) - + + + expect_type( + get_probabilities_top_iems(test_patient_top_iems_probs, test_expected_biomarkers_df, test_patient_id), + "list" + ) + + expect_equal( + length(get_probabilities_top_iems(test_patient_top_iems_probs, test_expected_biomarkers_df, test_patient_id)), + 4 + ) + expect_equal( + names(get_probabilities_top_iems(test_patient_top_iems_probs, test_expected_biomarkers_df, test_patient_id)), + c( + "Disease A, probability score 100", "Disease B, probability score 75", "Disease C, probability score 50", + "Disease D, probability score 25" + ) + ) + + expect_equal( + get_probabilities_top_iems( + test_patient_top_iems_probs, + test_expected_biomarkers_df, + test_patient_id + )$"Disease A, probability score 100", + data.frame( + HMDB_code = c("HMDB002", "HMDB012"), + HMDB_name = c("metab2", "metab12") + ) + ) }) testthat::test_that("make_and_save_diem_plots: Make and save dIEM plots", { @@ -868,7 +886,7 @@ testthat::test_that("make_and_save_diem_plots: Make and save dIEM plots", { test_explanation_violin_plot )) unlink("dIEM_plots/", recursive = TRUE) - + expect_equal(make_and_save_diem_plots( test_diem_probability_score, test_patient_ids, @@ -881,22 +899,24 @@ testthat::test_that("make_and_save_diem_plots: Make and save dIEM plots", { test_iem_variables, test_explanation_violin_plot ), "P2025M3") - + expect_true(file.exists("dIEM_plots/IEM_P2025M1.pdf")) expect_true(file.exists("dIEM_plots/IEM_P2025M2.pdf")) - + unlink("dIEM_plots/", recursive = TRUE) }) testthat::test_that("make_metabolite_order: Make the order of metabolites for the violin plots", { test_metabolites_vector <- c("metab1", "metab2", "metab3") test_num_plots_per_page <- 5 - + make_metabolite_order(test_num_plots_per_page, test_metabolites_vector) - + expect_equal(length(make_metabolite_order(test_num_plots_per_page, test_metabolites_vector)), 5) - expect_equal(make_metabolite_order(test_num_plots_per_page, test_metabolites_vector), - c("metab1", "metab2", "metab3", " ", " ")) + expect_equal( + make_metabolite_order(test_num_plots_per_page, test_metabolites_vector), + c("metab1", "metab2", "metab3", " ", " ") + ) }) testthat::test_that("pad_truncate_hmdb_names: Pad or truncate HMDB names to a fixed width", { @@ -905,14 +925,18 @@ testthat::test_that("pad_truncate_hmdb_names: Pad or truncate HMDB names to a fi ) test_width <- 10 test_pad_character <- "+" - - expect_equal(nrow(pad_truncate_hmdb_names(test_dataframe, test_width, test_pad_character)), - 3) - expect_equal(pad_truncate_hmdb_names(test_dataframe, test_width, test_pad_character)$HMDB_name, - c("metab1++++", "metabol...", "metabo3+++")) - + + expect_equal( + nrow(pad_truncate_hmdb_names(test_dataframe, test_width, test_pad_character)), + 3 + ) + expect_equal( + pad_truncate_hmdb_names(test_dataframe, test_width, test_pad_character)$HMDB_name, + c("metab1++++", "metabol...", "metabo3+++") + ) + test_df <- pad_truncate_hmdb_names(test_dataframe, test_width, test_pad_character) - for (row in seq(nrow(test_df))) { + for (row in seq_len(nrow(test_df))) { expect_equal(nchar(test_df[row, "HMDB_name"]), 10) } }) @@ -921,11 +945,11 @@ testthat::test_that("save_patient_no_iem: Save a list of patient IDs to a text f local_edition(3) test_threshold_iem <- 5 test_patient_no_iem <- c("Patient1", "Patient2") - + expect_silent(save_patient_no_iem(test_threshold_iem, test_patient_no_iem)) - + expect_true(file.exists("missing_probability_scores.txt")) - + expect_snapshot_file("missing_probability_scores.txt") file.remove("missing_probability_scores.txt") }) From 78285ff2dd5cb27483ea2d4316aaf662d0ee9493 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Mon, 2 Mar 2026 14:06:12 +0100 Subject: [PATCH 138/161] Add theormz_HMDB to columns to remove --- DIMS/export/generate_violin_plots_functions.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index 378148e4..165c6bb0 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -10,7 +10,7 @@ prepare_intensities_zscore_df <- function(intensities_zscore_df) { intensities_zscore_df <- intensities_zscore_df %>% select(-c( - plots, HMDB_name_all, HMDB_ID_all, sec_HMDB_ID, HMDB_key, sec_HMDB_ID_rlvnc, name, + plots, HMDB_name_all, HMDB_ID_all, sec_HMDB_ID, HMDB_key, theormz_HMDB, sec_HMDB_ID_rlvnc, name, relevance, descr, origin, fluids, tissue, disease, pathway, nr_ctrls )) %>% relocate(c(HMDB_code, HMDB_name)) %>% From a02deba62dd34a03a4275c969aca91a23d46f3fd Mon Sep 17 00:00:00 2001 From: ALuesink Date: Mon, 2 Mar 2026 14:20:44 +0100 Subject: [PATCH 139/161] Revert "Add theormz_HMDB to columns to remove" This reverts commit 78285ff2dd5cb27483ea2d4316aaf662d0ee9493. Changes to the wrong feature branch. --- DIMS/export/generate_violin_plots_functions.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index 165c6bb0..378148e4 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -10,7 +10,7 @@ prepare_intensities_zscore_df <- function(intensities_zscore_df) { intensities_zscore_df <- intensities_zscore_df %>% select(-c( - plots, HMDB_name_all, HMDB_ID_all, sec_HMDB_ID, HMDB_key, theormz_HMDB, sec_HMDB_ID_rlvnc, name, + plots, HMDB_name_all, HMDB_ID_all, sec_HMDB_ID, HMDB_key, sec_HMDB_ID_rlvnc, name, relevance, descr, origin, fluids, tissue, disease, pathway, nr_ctrls )) %>% relocate(c(HMDB_code, HMDB_name)) %>% From eb62557e276f8be0b8159ba6700ffc4a3d95b7fe Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 12 Mar 2026 10:59:25 +0100 Subject: [PATCH 140/161] Changed get_colnames_samples to 2 functions and fixed unit tests --- DIMS/export/generate_violin_plots_functions.R | 18 ++++++++++++---- .../testthat/test_generate_violin_plots.R | 21 ++++++++++++------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index 378148e4..c8e6a04f 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -20,17 +20,27 @@ prepare_intensities_zscore_df <- function(intensities_zscore_df) { } #' Get all column names containing a specific prefix. -#' Find all column names containing a specific prefix, e.g. "P", and remove the _Zscore suffix from the names #' #' @param dataframe: dataframe containing multiple columns with Z-scores #' @param sample_label: a string of a prefix to be searched in the column names, e.g. "P" or "C". #' #' @returns sample_colnames: a vector of column names all containing the prefix. -get_colnames_samples <- function(dataframe, sample_label) { - sample_colnames <- unique(gsub("_Zscore", "", grep(paste0("^", sample_label), colnames(dataframe), value = TRUE))) +get_colnames_by_prefix <- function(dataframe, prefix) { + sample_colnames <- grep(paste0("^", prefix), colnames(dataframe), value = TRUE) return(sample_colnames) } +#' Remove the suffix from a vector of names +#' +#' @param vector_names: vector containing names with or without a suffix +#' @param suffix: string containing the suffix to be removed +#' +#' @returns names_no_suffix: a vector of unique names without the suffix +remove_suffix_from_items <- function(vector_names, suffix) { + names_no_suffix <- unique(gsub("_Zscore", "", vector_names)) + return(names_no_suffix) +} + #' Add Zscores for multiple ratios to the dataframe #' #' @param outlist: dataframe containing intensities and Z-scores for all controls and patients @@ -127,7 +137,7 @@ make_and_save_violin_plot_pdfs <- function( explanation_violin_plot, number_of_metabolites) { # Get all patient IDs - patient_col_names <- get_colnames_samples(zscore_patients_df, "P") + patient_col_names <- remove_suffix_from_items(get_colnames_by_prefix(zscore_patients_df, 'P'), "_Zscore") # get all files from metabolite_groups directory metabolite_dirs <- list.files(path = path_metabolite_groups, full.names = FALSE, recursive = FALSE) for (metabolite_dir in metabolite_dirs) { diff --git a/DIMS/tests/testthat/test_generate_violin_plots.R b/DIMS/tests/testthat/test_generate_violin_plots.R index fd48cbe8..d5fe3a8e 100644 --- a/DIMS/tests/testthat/test_generate_violin_plots.R +++ b/DIMS/tests/testthat/test_generate_violin_plots.R @@ -633,15 +633,22 @@ testthat::test_that("prepare_intensities_zscore_df: Preparing the intensities an ) }) -testthat::test_that("get_colnames_samples: Get all column names containing a specific prefix", { - test_colnames <- c( - "HMDB_name", "HMDB_code", "P1001", "P1001_Zscore", "P1002", - "P1003", "P1004", "C101_Zscore", "C102", "C103" - ) +testthat::test_that("get_colnames_by_prefix: Get all column names containing a specific prefix", { test_intensities_zscore_df <- read.delim(test_path("fixtures/", "test_intensities_zscore_df.txt")) - expect_equal(get_colnames_samples(test_intensities_zscore_df, "P"), c("P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5")) - expect_equal(get_colnames_samples(test_intensities_zscore_df, "C"), c("C101.1", "C102.1", "C103.1", "C104.1", "C105.1")) + expect_equal(get_colnames_by_prefix(test_intensities_zscore_df, "P"), + c("P2025M1", "P2025M2", "P2025M3", "P2025M4", "P2025M5", + "P2025M1_Zscore", "P2025M2_Zscore", "P2025M3_Zscore", "P2025M4_Zscore", "P2025M5_Zscore")) + expect_equal(get_colnames_by_prefix(test_intensities_zscore_df, "C"), + c("C101.1", "C102.1", "C103.1", "C104.1", "C105.1", + "C101.1_Zscore", "C102.1_Zscore", "C103.1_Zscore", "C104.1_Zscore", "C105.1_Zscore")) +}) + +testthat::test_that("remove_suffix_from_items: Remove the suffix from a vector of names", { + test_colnames <- c("P1001", "P1001_Zscore", "P1002", "P1003", "P1004", "C101_Zscore", "C102", "C103") + + expect_equal(remove_suffix_from_items(test_colnames, "_Zscore"), + c("P1001", "P1002", "P1003", "P1004", "C101", "C102", "C103")) }) testthat::test_that("add_zscores_ratios_to_df: Add Zscores for multiple ratios to the dataframe", { From b355166a41b0324cbb9237dcc8c7fd2b2411ab94 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Thu, 12 Mar 2026 11:00:45 +0100 Subject: [PATCH 141/161] Fixed linting errors --- DIMS/export/generate_violin_plots_functions.R | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index c8e6a04f..e2160f9c 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -137,7 +137,7 @@ make_and_save_violin_plot_pdfs <- function( explanation_violin_plot, number_of_metabolites) { # Get all patient IDs - patient_col_names <- remove_suffix_from_items(get_colnames_by_prefix(zscore_patients_df, 'P'), "_Zscore") + patient_col_names <- remove_suffix_from_items(get_colnames_by_prefix(zscore_patients_df, "P"), "_Zscore") # get all files from metabolite_groups directory metabolite_dirs <- list.files(path = path_metabolite_groups, full.names = FALSE, recursive = FALSE) for (metabolite_dir in metabolite_dirs) { @@ -199,10 +199,7 @@ get_list_dataframes_from_dir <- function(dir_with_subdirs) { # get a list of all metabolite files txt_files_paths <- list.files(dir_with_subdirs, pattern = "*.txt", recursive = FALSE, full.names = TRUE) # put all metabolites into one list - list_of_dataframes <- lapply(txt_files_paths, - read.table, - sep = "\t", header = TRUE, quote = "" - ) + list_of_dataframes <- lapply(txt_files_paths, read.table, sep = "\t", header = TRUE, quote = "") names(list_of_dataframes) <- gsub(".txt", "", basename(txt_files_paths)) return(list_of_dataframes) From ac061ace274f61d7c83bfe88b89d9b32d4da6cbe Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Thu, 12 Mar 2026 16:10:37 +0100 Subject: [PATCH 142/161] renamed function collapse in DIMS/preprocessing/collect_filled_functions.R --- DIMS/preprocessing/collect_filled_functions.R | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/DIMS/preprocessing/collect_filled_functions.R b/DIMS/preprocessing/collect_filled_functions.R index fae06fca..96b704f5 100644 --- a/DIMS/preprocessing/collect_filled_functions.R +++ b/DIMS/preprocessing/collect_filled_functions.R @@ -1,6 +1,6 @@ # CollectFilled functions -collapse <- function(column_label, peakgroup_list, index_dup) { +collapse_information <- function(column_label, peakgroup_list, index_dup) { #' Collapse identification info for peak groups with the same mass #' #' @param column_label: Name of column in peakgroup_list (string) @@ -37,12 +37,12 @@ merge_duplicate_rows <- function(peakgroup_list) { peaklist_index <- which(peakgroup_list[, "mzmed.pgrp"] == peakgroup_list[index_dup[1], "mzmed.pgrp"]) single_peakgroup <- peakgroup_list[peaklist_index[1], , drop = FALSE] - # use function collapse to concatenate info - single_peakgroup[, "assi_HMDB"] <- collapse("assi_HMDB", peakgroup_list, peaklist_index) - single_peakgroup[, "iso_HMDB"] <- collapse("iso_HMDB", peakgroup_list, peaklist_index) - single_peakgroup[, "HMDB_code"] <- collapse("HMDB_code", peakgroup_list, peaklist_index) - single_peakgroup[, "all_hmdb_ids"] <- collapse("all_hmdb_ids", peakgroup_list, peaklist_index) - single_peakgroup[, "sec_hmdb_ids"] <- collapse("sec_hmdb_ids", peakgroup_list, peaklist_index) + # use function collapse_information to concatenate info + single_peakgroup[, "assi_HMDB"] <- collapse_information("assi_HMDB", peakgroup_list, peaklist_index) + single_peakgroup[, "iso_HMDB"] <- collapse_information("iso_HMDB", peakgroup_list, peaklist_index) + single_peakgroup[, "HMDB_code"] <- collapse_information("HMDB_code", peakgroup_list, peaklist_index) + single_peakgroup[, "all_hmdb_ids"] <- collapse_information("all_hmdb_ids", peakgroup_list, peaklist_index) + single_peakgroup[, "sec_hmdb_ids"] <- collapse_information("sec_hmdb_ids", peakgroup_list, peaklist_index) if (single_peakgroup[, "sec_hmdb_ids"] == ";") single_peakgroup[, "sec_hmdb_ids"] < NA # keep track of deduplicated entries From ad2232c7ecb8ca3868a94b5f787cf4f776a610c3 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Thu, 12 Mar 2026 16:44:01 +0100 Subject: [PATCH 143/161] refactored function calculate_ppm_deviation in DIMS/preprocessing/collect_filled_functions --- DIMS/preprocessing/collect_filled_functions.R | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/DIMS/preprocessing/collect_filled_functions.R b/DIMS/preprocessing/collect_filled_functions.R index 96b704f5..80883173 100644 --- a/DIMS/preprocessing/collect_filled_functions.R +++ b/DIMS/preprocessing/collect_filled_functions.R @@ -106,20 +106,18 @@ calculate_ppm_deviation <- function(peakgroup_list) { #' @param peakgroup_list: Peak group list (matrix) #' #' @return peakgroup_list_ppm: peak group list with ppm column (matrix) - - # calculate ppm deviation + + # make sure values in columns mzmed.pgrp and theormz_HMDB are numeric + peakgroup_list$mzmed.pgrp <- as.numeric(peakgroup_list$mzmed.pgrp) + peakgroup_list$theormz_HMDB <- as.numeric(peakgroup_list$theormz_HMDB) + + # calculate ppm deviation for (row_index in seq_len(nrow(peakgroup_list))) { - if (!is.na(peakgroup_list$theormz_HMDB[row_index]) && - !is.null(peakgroup_list$theormz_HMDB[row_index]) && - (peakgroup_list$theormz_HMDB[row_index] != "")) { - peakgroup_list$ppmdev[row_index] <- 10^6 * (as.numeric(as.vector(peakgroup_list$mzmed.pgrp[row_index])) - - as.numeric(as.vector(peakgroup_list$theormz_HMDB[row_index]))) / - as.numeric(as.vector(peakgroup_list$theormz_HMDB[row_index])) - } else { - peakgroup_list$ppmdev[row_index] <- NA - } + observed_mz <- peakgroup_list$mzmed.pgrp[row_index] + theor_mz <- peakgroup_list$theormz_HMDB[row_index] + peakgroup_list$ppmdev[row_index] <- 10^6 * (observed_mz - theor_mz) / theor_mz } - + return(peakgroup_list) } From 20c6539eb5f5f6f85bc438584760361bb45b0557 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Thu, 12 Mar 2026 16:46:29 +0100 Subject: [PATCH 144/161] Code review suggestion applied in DIMS/CollectFilled --- DIMS/CollectFilled.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DIMS/CollectFilled.R b/DIMS/CollectFilled.R index 0a2ff983..45c37bf3 100644 --- a/DIMS/CollectFilled.R +++ b/DIMS/CollectFilled.R @@ -38,6 +38,6 @@ for (scanmode in scanmodes) { remove_columns <- c("mzmin.pgrp", "mzmax.pgrp") outlist_ident <- outlist_ident[, -which(colnames(outlist_ident) %in% remove_columns)] write.table(outlist_ident, file = paste0("outlist_identified_", scanmode, ".txt"), sep = "\t", row.names = FALSE) - # generate output in RData format + # export output in RData format save(outlist_ident, file = paste0("outlist_identified_", scanmode, ".RData")) } From 3cb60add0055af657ed9f12fa994bbccc91a6f34 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 13 Mar 2026 10:35:58 +0100 Subject: [PATCH 145/161] fixed typo in DIMS/CollectFilled.R --- DIMS/CollectFilled.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DIMS/CollectFilled.R b/DIMS/CollectFilled.R index 45c37bf3..8737a83a 100644 --- a/DIMS/CollectFilled.R +++ b/DIMS/CollectFilled.R @@ -30,7 +30,7 @@ for (scanmode in scanmodes) { outlist_stats <- calculate_zscores_peakgrouplist(outlist_total) } # calculate ppm deviation - outlist_withppm <- calculate_ppmdeviation(outlist_stats) + outlist_withppm <- calculate_ppm_deviation(outlist_stats) # put columns in correct order outlist_ident <- order_columns_peakgrouplist(outlist_withppm) From 7180353b7b35a0469b20d3747fab5918b68dbd8c Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 13 Mar 2026 10:36:47 +0100 Subject: [PATCH 146/161] removed obsolete line from DIMS/preprocessing/collect_filled_functions.R --- DIMS/preprocessing/collect_filled_functions.R | 1 - 1 file changed, 1 deletion(-) diff --git a/DIMS/preprocessing/collect_filled_functions.R b/DIMS/preprocessing/collect_filled_functions.R index eea00328..4686a8b7 100644 --- a/DIMS/preprocessing/collect_filled_functions.R +++ b/DIMS/preprocessing/collect_filled_functions.R @@ -115,7 +115,6 @@ calculate_ppm_deviation <- function(peakgroup_list) { observed_mz <- peakgroup_list$mzmed.pgrp[row_index] theor_mz <- peakgroup_list$theormz_HMDB[row_index] peakgroup_list$ppmdev[row_index] <- 10^6 * (observed_mz - theor_mz) / theor_mz - peakgroup_list <- cbind(peakgroup_list, zscores_1col) } return(peakgroup_list) From 4d2e6d4c15f3c58267b02080f0a66b4b8fde0209 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 13 Mar 2026 11:01:28 +0100 Subject: [PATCH 147/161] changed function name for collapse in DIMS test_collect_filled.R --- DIMS/tests/testthat/test_collect_filled.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DIMS/tests/testthat/test_collect_filled.R b/DIMS/tests/testthat/test_collect_filled.R index 3927af49..1e88c6d3 100644 --- a/DIMS/tests/testthat/test_collect_filled.R +++ b/DIMS/tests/testthat/test_collect_filled.R @@ -1,5 +1,5 @@ # unit tests for CollectFilled -# functions: collapse, merge_duplicate_rows, calculate_zscores_peakgrouplist, +# functions: collapse_information, merge_duplicate_rows, calculate_zscores_peakgrouplist, # calculate_ppm_deviation, order_columns_peakgrouplist source("../../preprocessing/collect_filled_functions.R") @@ -7,7 +7,7 @@ testthat::test_that("Values for duplicate rows are correctly collapsed", { test_matrix <- matrix(letters[1:8], nrow = 2, ncol = 4) colnames(test_matrix) <- paste0("column", 1:4) - expect_equal(collapse("column1", test_matrix, c(1,2)), "a;b", TRUE) + expect_equal(collapse_information("column1", test_matrix, c(1,2)), "a;b", TRUE) }) testthat::test_that("Duplicate rows in a peak group list are correctly merged", { From fc5bfb4bac930fc413199bffc962162ee09618d8 Mon Sep 17 00:00:00 2001 From: ALuesink Date: Fri, 13 Mar 2026 14:05:00 +0100 Subject: [PATCH 148/161] New snapshot for unit testing --- .../testthat/_snaps/generate_violin_plots.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/DIMS/tests/testthat/_snaps/generate_violin_plots.md b/DIMS/tests/testthat/_snaps/generate_violin_plots.md index 4930649a..fc917a91 100644 --- a/DIMS/tests/testthat/_snaps/generate_violin_plots.md +++ b/DIMS/tests/testthat/_snaps/generate_violin_plots.md @@ -10,12 +10,12 @@ # save_prob_scores_to_excel: Saving the probability score dataframe as an Excel file - Disease P2025M1 P2025M2 P2025M3 P2025M4 - 1 Disease A 10.900 -10.9 49.90 -49.9 - 2 Disease B 0.953 0.0 2.29 0.0 - 3 Disease C 12.100 0.0 0.00 12.1 - 4 Disease D 0.000 -12.5 0.00 18.2 - 5 Disease E 44.300 0.0 0.00 28.1 - 6 Disease F 0.000 -77.4 -77.40 0.0 - 7 Disease G -38.700 38.7 38.70 -38.7 + Disease P2025M1 P2025M2 P2025M3 P2025M4 + 1 Disease A 10.900 -10.90000000000000 49.90000000000000 -49.9 + 2 Disease B 0.953 0.00000000000000 2.29000000000000 0.0 + 3 Disease C 12.100 0.00000000000000 0.00000000000000 12.1 + 4 Disease D 0.000 -12.50000000000000 0.00000000000000 18.2 + 5 Disease E 44.300 0.00000000000000 0.00000000000000 28.1 + 6 Disease F 0.000 -77.40000000000001 -77.40000000000001 0.0 + 7 Disease G -38.700 38.70000000000000 38.70000000000000 -38.7 From 6948bd1c8233e343652ff4551278e216cff1594e Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 13 Mar 2026 17:26:07 +0100 Subject: [PATCH 149/161] removed adding scanmode column in DIMS/GenerateQCOutput.R --- DIMS/GenerateQCOutput.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DIMS/GenerateQCOutput.R b/DIMS/GenerateQCOutput.R index 59dd697b..b4583ca8 100644 --- a/DIMS/GenerateQCOutput.R +++ b/DIMS/GenerateQCOutput.R @@ -208,7 +208,7 @@ if (dims_matrix == "Plasma") { } if (nrow(is_below_threshold) > 0) { - write.table(cbind(is_below_threshold, scanmode = scanmode_is), + write.table(is_below_threshold), file = "internal_standards_below_threshold.txt", row.names = FALSE, sep = "\t") } else { From 31c640dbdbfd7e9050b1af4642409ddb5ba15f34 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 17 Mar 2026 11:46:45 +0100 Subject: [PATCH 150/161] bug fixes for DIMS/GenerateQCOutput.R --- DIMS/GenerateQCOutput.R | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/DIMS/GenerateQCOutput.R b/DIMS/GenerateQCOutput.R index b4583ca8..9075c56b 100644 --- a/DIMS/GenerateQCOutput.R +++ b/DIMS/GenerateQCOutput.R @@ -208,7 +208,7 @@ if (dims_matrix == "Plasma") { } if (nrow(is_below_threshold) > 0) { - write.table(is_below_threshold), + write.table(is_below_threshold, file = "internal_standards_below_threshold.txt", row.names = FALSE, sep = "\t") } else { @@ -221,28 +221,28 @@ if (nrow(is_below_threshold) > 0) { # bar plot either with or without minimal intensity lines if (add_min_intens_lines) { save_internal_standard_plot( - is_neg_selection, "barplot", "Interne Standaard (Neg)", outdir, + is_neg_selection_subset, "barplot", "Interne Standaard (Neg)", outdir, "IS_bar_select_neg", plot_width, plot_height, hline_data_neg ) save_internal_standard_plot( - is_pos_selection, "barplot", "Interne Standaard (Pos)", outdir, + is_pos_selection_subset, "barplot", "Interne Standaard (Pos)", outdir, "IS_bar_select_pos", plot_width, plot_height, hline_data_pos ) save_internal_standard_plot( - is_sum_selection, "barplot", "Interne Standaard (Sum)", outdir, + is_sum_selection_subset, "barplot", "Interne Standaard (Sum)", outdir, "IS_bar_select_sum", plot_width, plot_height, hline_data_sum ) } else { save_internal_standard_plot( - is_neg_selection, "barplot", "Interne Standaard (Neg)", outdir, + is_neg_selection_subset, "barplot", "Interne Standaard (Neg)", outdir, "IS_bar_select_neg", plot_width, plot_height ) save_internal_standard_plot( - is_pos_selection, "barplot", "Interne Standaard (Pos)", outdir, + is_pos_selection_subset, "barplot", "Interne Standaard (Pos)", outdir, "IS_bar_select_pos", plot_width, plot_height ) save_internal_standard_plot( - is_sum_selection, "barplot", "Interne Standaard (Sum)", outdir, + is_sum_selection_subset, "barplot", "Interne Standaard (Sum)", outdir, "IS_bar_select_sum", plot_width, plot_height ) } @@ -252,15 +252,15 @@ plot_width <- 8 + 0.2 * sample_count plot_height <- plot_width / 2.0 save_internal_standard_plot( - is_neg_selection, "lineplot", "Interne Standaard (Neg)", outdir, + is_neg_selection_subset, "lineplot", "Interne Standaard (Neg)", outdir, "IS_line_select_neg", plot_width, plot_height ) save_internal_standard_plot( - is_pos_selection, "lineplot", "Interne Standaard (Pos)", outdir, + is_pos_selection_subset, "lineplot", "Interne Standaard (Pos)", outdir, "IS_line_select_pos", plot_width, plot_height ) save_internal_standard_plot( - is_sum_selection, "lineplot", "Interne Standaard (Sum)", outdir, + is_sum_selection_subset, "lineplot", "Interne Standaard (Sum)", outdir, "IS_line_select_sum", plot_width, plot_height ) @@ -400,11 +400,11 @@ openxlsx::saveWorkbook(wb, xlsx_name, overwrite = TRUE) rm(wb) # generate text file for workflow completed mail for components with Z-score < 2 -if (sum(grepl("P1001", colnames(sst_list_intensities))) > 0) { - zscore_column <- grep("_Zscore", colnames(sst_list_intensities))[1] - sst_list_intensities_qc <- sst_list_intensities[sst_list_intensities[, zscore_column] < 2, ] - sst_list_intensities_qc <- select(sst_list_intensities_qc, -c("CV_controls")) - write.table(sst_list_intensities_qc, file = paste(outdir, "sst_qc.txt", sep = "/"), row.names = FALSE, sep = "\t") +if (sum(grepl("P1001", colnames(sst_intensities_df))) > 0) { + zscore_column <- grep("_Zscore", colnames(sst_intensities_df))[1] + sst_intensities_df_qc <- sst_intensities_df[sst_intensities_df[, zscore_column] < 2, ] + sst_intensities_df_qc <- select(sst_intensities_df_qc, -c("CV_controls")) + write.table(sst_intensities_df_qc, file = paste(outdir, "sst_qc.txt", sep = "/"), row.names = FALSE, sep = "\t") } else { write.table("no SST sample present", file = paste(outdir, "sst_qc.txt", sep = "/"), row.names = FALSE, col.names = FALSE) } From 9b5070da3c6083e79a30ae55e6a316914f198b14 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 17 Mar 2026 11:47:33 +0100 Subject: [PATCH 151/161] bug fixes for DIMS/GenerateViolinPlots.R --- DIMS/GenerateViolinPlots.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DIMS/GenerateViolinPlots.R b/DIMS/GenerateViolinPlots.R index 0007881b..9d1f3e70 100644 --- a/DIMS/GenerateViolinPlots.R +++ b/DIMS/GenerateViolinPlots.R @@ -48,8 +48,8 @@ number_of_metabolites <- list( lowest = 10 ) -control_ids <- get_colnames_samples(intensities_zscore_df, "C") -patient_ids <- get_colnames_samples(intensities_zscore_df, "P") +control_ids <- get_colnames_by_prefix(intensities_zscore_df, "C") +patient_ids <- get_colnames_by_prefix(intensities_zscore_df, "P") all_sample_ids <- c(control_ids, patient_ids) number_of_samples <- list( controls = length(control_ids), @@ -58,7 +58,7 @@ number_of_samples <- list( # Add Z-scores for ratios to intensities_zscore_df dataframe intensities_zscore_ratios_df <- add_zscores_ratios_to_df(intensities_zscore_df, metabolites_ratios_df, all_sample_ids) -# for debugging: +# Save ratios to file: save(intensities_zscore_ratios_df, file = "./outlist_with_ratios.RData") # Select only the cols with zscores of the patients From a350fc645902d0041a65db95ecb5ac25cffdfd48 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 17 Mar 2026 11:48:51 +0100 Subject: [PATCH 152/161] bug fixes for DIMS/export/generate_violin_plots_functions.R --- DIMS/export/generate_violin_plots_functions.R | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index e2160f9c..499ad73c 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -22,7 +22,7 @@ prepare_intensities_zscore_df <- function(intensities_zscore_df) { #' Get all column names containing a specific prefix. #' #' @param dataframe: dataframe containing multiple columns with Z-scores -#' @param sample_label: a string of a prefix to be searched in the column names, e.g. "P" or "C". +#' @param prefix: a string of a prefix to be searched in the column names, e.g. "P" or "C". #' #' @returns sample_colnames: a vector of column names all containing the prefix. get_colnames_by_prefix <- function(dataframe, prefix) { @@ -760,6 +760,7 @@ create_violin_plot <- function(metab_zscores_df, patient_zscore_df, sub_perpage, #' #' @param expected_biomarkers_df: dataframe with information for HMDB codes about IEMs (dataframe) #' @param zscore_patients: dataframe containing Z-scores for patient (dataframe) +#' @param sample_cols: vector containing column names with intensities and Z-scores for patients (vector) #' #' @returns probability_score: a dataframe with probability scores for IEMs for each patient (dataframe) run_diem_algorithm <- function(expected_biomarkers_df, zscore_patients_df, sample_cols) { @@ -777,6 +778,12 @@ run_diem_algorithm <- function(expected_biomarkers_df, zscore_patients_df, sampl by.x = c("HMDB_code"), by.y = c("HMDB_code") ) + # Reduce sample_cols to contain only the patient names + if (sum(grepl("Zscore", sample_cols)) > 0) { + sample_cols <- sample_cols[-grep("Zscore", sample_cols)] + } + print(sample_cols) + # Change Z-score to zero for specific cases zscore_expected_df <- zscore_expected_df %>% mutate(across( all_of(sample_cols), @@ -889,6 +896,11 @@ make_and_save_diem_plots <- function( diem_plot_dir <- paste("./dIEM_plots", sep = "/") dir.create(diem_plot_dir) + # reduce patient_col_names to contain only the patient names + if (sum(grepl("Zscore", patient_col_names)) > 0) { + patient_col_names <- patient_col_names[-grep("Zscore", patient_col_names)] + } + patient_no_iem <- c() for (patient_id in patient_col_names) { From 160fd0af8df6a9bb74cc31daa0e6135ce31a3c82 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Thu, 19 Mar 2026 10:52:50 +0100 Subject: [PATCH 153/161] fixed bugs in DIMS/GenerateQCOutput.R --- DIMS/GenerateQCOutput.R | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/DIMS/GenerateQCOutput.R b/DIMS/GenerateQCOutput.R index 9075c56b..979b31b6 100644 --- a/DIMS/GenerateQCOutput.R +++ b/DIMS/GenerateQCOutput.R @@ -273,16 +273,16 @@ patterns <- c("^(P1002\\.)[[:digit:]]+_", "^(P1003\\.)[[:digit:]]+_", "^(P1005\\ positive_controls_index <- grepl(pattern = paste(patterns, collapse = "|"), column_list) positive_control_list <- column_list[positive_controls_index] -if (positive_controls_index > 0) { +if (sum(positive_controls_index) > 0) { # find if one or more positive control samples are missing pos_contr_warning <- c() if (all(sapply(c("^P1002", "^P1003", "^P1005"), function(x) any(grepl(x, positive_control_list))))) { - cat("All three positive controls are present") + pos_contr_warning <- "All three positive controls are present" } else { pos_contr_warning <- paste( "positive controls list is not complete. Only", - paste(positive_control_list, collapse = ", "), "is/are present" + paste(positive_control_list, collapse = ", "), " present" ) } if (length(positive_control_list) > 0) { @@ -326,13 +326,14 @@ if (positive_controls_index > 0) { file = paste0(outdir, "/", project, "_positive_control.xlsx"), sheetName = "Sheet1", col.names = TRUE, row.names = TRUE, append = FALSE ) + } + if (length(pos_contr_warning) == 0) { + pos_contr_warning <- "No positive controls found" } - if (length(pos_contr_warning) != 0) { - write.table(pos_contr_warning, - file = paste(outdir, "positive_controls_warning.txt", sep = "/"), - row.names = FALSE, col.names = FALSE, quote = FALSE - ) - } + write.table(pos_contr_warning, + file = paste(outdir, "positive_controls_warning.txt", sep = "/"), + row.names = FALSE, col.names = FALSE, quote = FALSE + ) } ### SST components output #### From 5dc850a3a68255cd3a7dba34bb08e765bbfb2475 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Thu, 19 Mar 2026 11:11:48 +0100 Subject: [PATCH 154/161] modified sst components output for empty table in DIMS/GenerateQCOutput.R --- DIMS/GenerateQCOutput.R | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DIMS/GenerateQCOutput.R b/DIMS/GenerateQCOutput.R index 979b31b6..7383391b 100644 --- a/DIMS/GenerateQCOutput.R +++ b/DIMS/GenerateQCOutput.R @@ -406,6 +406,10 @@ if (sum(grepl("P1001", colnames(sst_intensities_df))) > 0) { sst_intensities_df_qc <- sst_intensities_df[sst_intensities_df[, zscore_column] < 2, ] sst_intensities_df_qc <- select(sst_intensities_df_qc, -c("CV_controls")) write.table(sst_intensities_df_qc, file = paste(outdir, "sst_qc.txt", sep = "/"), row.names = FALSE, sep = "\t") + # in case of an empty table, the column header doesn't need to appear in the mail + if (nrow(sst_intensities_df_qc) == 0) { + write.table("none", file = paste(outdir, "sst_qc.txt", sep = "/"), row.names = FALSE, col.names = FALSE) + } } else { write.table("no SST sample present", file = paste(outdir, "sst_qc.txt", sep = "/"), row.names = FALSE, col.names = FALSE) } From 035b1b9e7277474cd10437bdb89f4572ff987918 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Thu, 19 Mar 2026 11:20:20 +0100 Subject: [PATCH 155/161] fixed double entries in case of two of the same pos ctrl in DIMS/GenerateQCOutput.R --- DIMS/GenerateQCOutput.R | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/DIMS/GenerateQCOutput.R b/DIMS/GenerateQCOutput.R index 7383391b..f83bb3fc 100644 --- a/DIMS/GenerateQCOutput.R +++ b/DIMS/GenerateQCOutput.R @@ -289,22 +289,23 @@ if (sum(positive_controls_index) > 0) { # make positive control excel with specific HMDB_codes in combination with specific control samples positive_control <- NULL for (pos_ctrl in positive_control_list) { + pos_ctrl_samplename <- gsub("_Zscore", "", pos_ctrl) if (any(grepl("^P1002", pos_ctrl))) { - pa_sample_name <- positive_control_list[grepl("P1002", positive_control_list)] + pa_sample_name <- positive_control_list[grepl(pos_ctrl_samplename, positive_control_list)] pa_codes <- c("HMDB0000824", "HMDB0000725", "HMDB0000123") pa_names <- c("Propionylcarnitine", "Propionylglycine", "Glycine") pa_data <- get_pos_ctrl_data(outlist, pa_sample_name, pa_codes, pa_names) positive_control <- rbind(positive_control, pa_data) } if (any(grepl("^P1003", pos_ctrl))) { - pku_sample_name <- positive_control_list[grepl("P1003", positive_control_list)] + pku_sample_name <- positive_control_list[grepl(pos_ctrl_samplename, positive_control_list)] pku_codes <- c("HMDB0000159") pku_names <- c("L-Phenylalanine") pku_data <- get_pos_ctrl_data(outlist, pku_sample_name, pku_codes, pku_names) positive_control <- rbind(positive_control, pku_data) } if (any(grepl("^P1005", pos_ctrl))) { - lpi_sample_name <- positive_control_list[grepl("P1005", positive_control_list)] + lpi_sample_name <- positive_control_list[grepl(pos_ctrl_samplename, positive_control_list)] lpi_codes <- c("HMDB0000904", "HMDB0000641", "HMDB0000182") lpi_names <- c("Citrulline", "L-Glutamine", "L-Lysine") lpi_data <- get_pos_ctrl_data(outlist, lpi_sample_name, lpi_codes, lpi_names) From 4bcc8eb091a0d3f8dd3f01c50f29bb6b15dc4dc3 Mon Sep 17 00:00:00 2001 From: Bas Kasemir <20270816+BasMonkey@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:18:09 +0100 Subject: [PATCH 156/161] Revert "fix indentation env (line 13)" --- .github/workflows/kinship_test.yml | 6 +++--- .github/workflows/moisaichunter_test.yml | 6 +++--- .github/workflows/utils_test.yml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/kinship_test.yml b/.github/workflows/kinship_test.yml index 67537cd4..bb7725dc 100644 --- a/.github/workflows/kinship_test.yml +++ b/.github/workflows/kinship_test.yml @@ -10,8 +10,8 @@ on: jobs: pytest: runs-on: ubuntu-latest - env: - working-directory: Kinship/ + env: + working-directory: Kinship/ defaults: run: working-directory: ${{ env.working-directory }} @@ -25,7 +25,7 @@ jobs: id: setup-python uses: actions/setup-python@v5 with: - python-version: "3.11.5" + python-version: '3.11.5' #---------------------------------------------- # install & configure poetry #---------------------------------------------- diff --git a/.github/workflows/moisaichunter_test.yml b/.github/workflows/moisaichunter_test.yml index 14549208..c38b2fab 100644 --- a/.github/workflows/moisaichunter_test.yml +++ b/.github/workflows/moisaichunter_test.yml @@ -10,8 +10,8 @@ on: jobs: pytest: runs-on: ubuntu-latest - env: - working-directory: MosaicHunter/1.0.0/ + env: + working-directory: MosaicHunter/1.0.0/ defaults: run: working-directory: ${{ env.working-directory }} @@ -25,7 +25,7 @@ jobs: id: setup-python uses: actions/setup-python@v5 with: - python-version: "3.11.5" + python-version: '3.11.5' #---------------------------------------------- # install & configure poetry #---------------------------------------------- diff --git a/.github/workflows/utils_test.yml b/.github/workflows/utils_test.yml index be457b81..9490573f 100644 --- a/.github/workflows/utils_test.yml +++ b/.github/workflows/utils_test.yml @@ -10,8 +10,8 @@ on: jobs: pytest: runs-on: ubuntu-latest - env: - working-directory: Utils/ + env: + working-directory: Utils/ defaults: run: working-directory: ${{ env.working-directory }} @@ -25,7 +25,7 @@ jobs: id: setup-python uses: actions/setup-python@v5 with: - python-version: "3.11.5" + python-version: '3.11.5' #---------------------------------------------- # install & configure poetry #---------------------------------------------- From df25d5bbea53b3a27ab53f72643b02fe846d0a27 Mon Sep 17 00:00:00 2001 From: Bas Kasemir <20270816+BasMonkey@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:21:33 +0100 Subject: [PATCH 157/161] Revert "Feature/add GitHub actions per tool" --- .github/workflows/checkqc_lint.yml | 17 - .github/workflows/checkqc_test.yml | 64 -- .github/workflows/gendercheck_lint.yml | 17 - .github/workflows/gendercheck_test.yml | 65 -- .github/workflows/kinship_test.yml | 6 +- .github/workflows/moisaichunter_lint.yml | 17 - .github/workflows/moisaichunter_test.yml | 64 -- .github/workflows/utils_test.yml | 9 +- .pre-commit-config.yaml | 11 - CheckQC/CheckQC.nf | 2 +- CheckQC/Dockerfile | 34 +- CheckQC/poetry.lock | 580 ------------------ CheckQC/pyproject.toml | 31 - CheckQC/requirements.txt | 12 + GenderCheck/CompareGender.nf | 2 +- GenderCheck/Dockerfile | 34 +- GenderCheck/poetry.lock | 124 ---- GenderCheck/pyproject.toml | 26 - GenderCheck/requirements.txt | 5 + .../{test_calculate_gender => }/test_bam.bam | Bin .../test_bam.bam.bai | Bin GenderCheck/test_calculate_gender.py | 26 +- MosaicHunter/1.0.0/MosaicHunter.nf | 2 +- MosaicHunter/1.0.0/poetry.lock | 124 ---- MosaicHunter/1.0.0/pyproject.toml | 26 - MosaicHunter/1.0.0/requirements.txt | 5 + .../test_bam.bam | Bin .../test_bam.bam.bai | Bin .../1.0.0/test_get_gender_from_bam_chrx.py | 22 +- Utils/create_hsmetrics_summary.py | 2 +- Utils/get_stats_from_flagstat.py | 26 +- poetry.lock | 253 -------- pyproject.toml | 22 - 33 files changed, 65 insertions(+), 1563 deletions(-) delete mode 100644 .github/workflows/checkqc_lint.yml delete mode 100644 .github/workflows/checkqc_test.yml delete mode 100644 .github/workflows/gendercheck_lint.yml delete mode 100644 .github/workflows/gendercheck_test.yml delete mode 100644 .github/workflows/moisaichunter_lint.yml delete mode 100644 .github/workflows/moisaichunter_test.yml delete mode 100644 .pre-commit-config.yaml delete mode 100644 CheckQC/poetry.lock delete mode 100644 CheckQC/pyproject.toml create mode 100644 CheckQC/requirements.txt delete mode 100644 GenderCheck/poetry.lock delete mode 100644 GenderCheck/pyproject.toml create mode 100644 GenderCheck/requirements.txt rename GenderCheck/{test_calculate_gender => }/test_bam.bam (100%) rename GenderCheck/{test_calculate_gender => }/test_bam.bam.bai (100%) delete mode 100644 MosaicHunter/1.0.0/poetry.lock delete mode 100644 MosaicHunter/1.0.0/pyproject.toml create mode 100644 MosaicHunter/1.0.0/requirements.txt rename MosaicHunter/1.0.0/{test_get_gender_from_bam_chrx => }/test_bam.bam (100%) rename MosaicHunter/1.0.0/{test_get_gender_from_bam_chrx => }/test_bam.bam.bai (100%) delete mode 100644 poetry.lock delete mode 100644 pyproject.toml diff --git a/.github/workflows/checkqc_lint.yml b/.github/workflows/checkqc_lint.yml deleted file mode 100644 index 6c153c07..00000000 --- a/.github/workflows/checkqc_lint.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: CheckQC Lint -on: - pull_request: - paths: [CheckQC/**] - push: - branches: [master, develop] - paths: [CheckQC/**] - -jobs: - ruff: - runs-on: ubuntu-latest - defaults: - run: - working-directory: CheckQC/ - steps: - - uses: actions/checkout@v4 - - uses: chartboost/ruff-action@v1 diff --git a/.github/workflows/checkqc_test.yml b/.github/workflows/checkqc_test.yml deleted file mode 100644 index 032de651..00000000 --- a/.github/workflows/checkqc_test.yml +++ /dev/null @@ -1,64 +0,0 @@ -# Source: https://github.com/marketplace/actions/install-poetry-action -name: CheckQC Test -on: - pull_request: - paths: [CheckQC/**] - push: - branches: [master, develop] - paths: [CheckQC/**] - -jobs: - pytest: - runs-on: ubuntu-latest - env: - working-directory: CheckQC/ - defaults: - run: - working-directory: ${{ env.working-directory }} - steps: - #---------------------------------------------- - # check-out repo and set-up python - #---------------------------------------------- - - name: Check out repository - uses: actions/checkout@v4 - - name: Set up python - id: setup-python - uses: actions/setup-python@v5 - with: - python-version: '3.11.5' - #---------------------------------------------- - # install & configure poetry - #---------------------------------------------- - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - virtualenvs-create: true - virtualenvs-in-project: true - installer-parallel: true - - #---------------------------------------------- - # load cached venv if cache exists - #---------------------------------------------- - - name: Load cached venv - id: cached-poetry-dependencies - uses: actions/cache@v4 - with: - path: ${{ env.working-directory }}/.venv - key: venv_checkqc-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} - #---------------------------------------------- - # install dependencies if cache does not exist - #---------------------------------------------- - - name: Install dependencies - if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction --no-root - #---------------------------------------------- - # install root project - #---------------------------------------------- - - name: Install project - run: poetry install --no-interaction - #---------------------------------------------- - # run pytest - #---------------------------------------------- - - name: Run tests - run: | - poetry run pytest . diff --git a/.github/workflows/gendercheck_lint.yml b/.github/workflows/gendercheck_lint.yml deleted file mode 100644 index 79a11506..00000000 --- a/.github/workflows/gendercheck_lint.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: GenderCheck Lint -on: - pull_request: - paths: [GenderCheck/**] - push: - branches: [master, develop] - paths: [GenderCheck/**] - -jobs: - ruff: - runs-on: ubuntu-latest - defaults: - run: - working-directory: GenderCheck/ - steps: - - uses: actions/checkout@v4 - - uses: chartboost/ruff-action@v1 diff --git a/.github/workflows/gendercheck_test.yml b/.github/workflows/gendercheck_test.yml deleted file mode 100644 index e8b0058c..00000000 --- a/.github/workflows/gendercheck_test.yml +++ /dev/null @@ -1,65 +0,0 @@ -# Source: https://github.com/marketplace/actions/install-poetry-action -name: GenderCheck Test -on: - pull_request: - paths: [GenderCheck/**] - push: - branches: [master, develop] - paths: [GenderCheck/**] - - -jobs: - pytest: - runs-on: ubuntu-latest - env: - working-directory: GenderCheck/ - defaults: - run: - working-directory: ${{ env.working-directory }} - steps: - #---------------------------------------------- - # check-out repo and set-up python - #---------------------------------------------- - - name: Check out repository - uses: actions/checkout@v4 - - name: Set up python - id: setup-python - uses: actions/setup-python@v5 - with: - python-version: '3.11.5' - #---------------------------------------------- - # install & configure poetry - #---------------------------------------------- - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - virtualenvs-create: true - virtualenvs-in-project: true - installer-parallel: true - - #---------------------------------------------- - # load cached venv if cache exists - #---------------------------------------------- - - name: Load cached venv - id: cached-poetry-dependencies - uses: actions/cache@v4 - with: - path: ${{ env.working-directory }}/.venv - key: venv_gendercheck-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} - #---------------------------------------------- - # install dependencies if cache does not exist - #---------------------------------------------- - - name: Install dependencies - if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction --no-root - #---------------------------------------------- - # install root project - #---------------------------------------------- - - name: Install project - run: poetry install --no-interaction - #---------------------------------------------- - # run pytest - #---------------------------------------------- - - name: Run tests - run: | - poetry run pytest . diff --git a/.github/workflows/kinship_test.yml b/.github/workflows/kinship_test.yml index bb7725dc..25acb4e7 100644 --- a/.github/workflows/kinship_test.yml +++ b/.github/workflows/kinship_test.yml @@ -10,11 +10,9 @@ on: jobs: pytest: runs-on: ubuntu-latest - env: - working-directory: Kinship/ defaults: run: - working-directory: ${{ env.working-directory }} + working-directory: Kinship/ steps: #---------------------------------------------- # check-out repo and set-up python @@ -44,7 +42,7 @@ jobs: id: cached-poetry-dependencies uses: actions/cache@v4 with: - path: ${{ env.working-directory }}/.venv + path: .venv key: venv_kinship-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} #---------------------------------------------- # install dependencies if cache does not exist diff --git a/.github/workflows/moisaichunter_lint.yml b/.github/workflows/moisaichunter_lint.yml deleted file mode 100644 index dd00c088..00000000 --- a/.github/workflows/moisaichunter_lint.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: MosaicHunter Lint -on: - pull_request: - paths: [MosaicHunter/**] - push: - branches: [master, develop] - paths: [MosaicHunter/**] - -jobs: - ruff: - runs-on: ubuntu-latest - defaults: - run: - working-directory: MosaicHunter/1.0.0// - steps: - - uses: actions/checkout@v4 - - uses: chartboost/ruff-action@v1 diff --git a/.github/workflows/moisaichunter_test.yml b/.github/workflows/moisaichunter_test.yml deleted file mode 100644 index c38b2fab..00000000 --- a/.github/workflows/moisaichunter_test.yml +++ /dev/null @@ -1,64 +0,0 @@ -# Source: https://github.com/marketplace/actions/install-poetry-action -name: MosaicHunter Test -on: - pull_request: - paths: [MosaicHunter/**] - push: - branches: [master, develop] - paths: [MosaicHunter/**] - -jobs: - pytest: - runs-on: ubuntu-latest - env: - working-directory: MosaicHunter/1.0.0/ - defaults: - run: - working-directory: ${{ env.working-directory }} - steps: - #---------------------------------------------- - # check-out repo and set-up python - #---------------------------------------------- - - name: Check out repository - uses: actions/checkout@v4 - - name: Set up python - id: setup-python - uses: actions/setup-python@v5 - with: - python-version: '3.11.5' - #---------------------------------------------- - # install & configure poetry - #---------------------------------------------- - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - virtualenvs-create: true - virtualenvs-in-project: true - installer-parallel: true - - #---------------------------------------------- - # load cached venv if cache exists - #---------------------------------------------- - - name: Load cached venv - id: cached-poetry-dependencies - uses: actions/cache@v4 - with: - path: ${{ env.working-directory }}/.venv - key: venv_mosaichunter-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} - #---------------------------------------------- - # install dependencies if cache does not exist - #---------------------------------------------- - - name: Install dependencies - if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction --no-root - #---------------------------------------------- - # install root project - #---------------------------------------------- - - name: Install project - run: poetry install --no-interaction - #---------------------------------------------- - # run pytest - #---------------------------------------------- - - name: Run tests - run: | - poetry run pytest . diff --git a/.github/workflows/utils_test.yml b/.github/workflows/utils_test.yml index 9490573f..8fa18c2b 100644 --- a/.github/workflows/utils_test.yml +++ b/.github/workflows/utils_test.yml @@ -10,11 +10,9 @@ on: jobs: pytest: runs-on: ubuntu-latest - env: - working-directory: Utils/ defaults: run: - working-directory: ${{ env.working-directory }} + working-directory: Utils/ steps: #---------------------------------------------- # check-out repo and set-up python @@ -43,7 +41,7 @@ jobs: id: cached-poetry-dependencies uses: actions/cache@v4 with: - path: ${{ env.working-directory }}/.venv + path: .venv key: venv_utils-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} #---------------------------------------------- # install dependencies if cache does not exist @@ -61,4 +59,5 @@ jobs: #---------------------------------------------- - name: Run tests run: | - poetry run pytest . + source .venv/bin/activate + pytest . diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 8f4b8b59..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# See https://pre-commit.com for more information -# See https://pre-commit.com/hooks.html for more hooks -repos: - - repo: https://github.com/astral-sh/ruff-pre-commit - # Ruff version. - rev: v0.5.1 - hooks: - # Run the linter. - - id: ruff - # Run the formatter. - - id: ruff-format diff --git a/CheckQC/CheckQC.nf b/CheckQC/CheckQC.nf index b920865b..25f5ca88 100644 --- a/CheckQC/CheckQC.nf +++ b/CheckQC/CheckQC.nf @@ -1,7 +1,7 @@ process CheckQC { tag {"CheckQC ${identifier}"} label 'CheckQC' - container = 'docker.io/umcugenbioinf/checkqc:1.0.1' + container = 'docker.io/umcugenbioinf/checkqc:1.0.0' shell = ['/bin/bash', '-euo', 'pipefail'] input: diff --git a/CheckQC/Dockerfile b/CheckQC/Dockerfile index 1b510b50..c7763aeb 100644 --- a/CheckQC/Dockerfile +++ b/CheckQC/Dockerfile @@ -3,37 +3,9 @@ FROM --platform=linux/amd64 python:3.9 ################## METADATA ###################### LABEL base_image="python:3.9" -LABEL version="1.0.1" +LABEL version="1.0.0" LABEL extra.binaries="pandas,PyYAML,pytest" ################## INSTALLATIONS ###################### -# Use poetry to install virtualenv. -ENV POETRY_VERSION=1.8.3 -ENV POETRY_HOME=/opt/poetry -ENV POETRY_VENV=/opt/poetry-venv - -# Tell Poetry where to place its cache and virtual environment -ENV POETRY_CACHE_DIR=/tmp/poetry_cache - -# Do not ask any interactive question -ENV POETRY_NO_INTERACTION=1 - -# Make poetry create the virtual environment in the project's root -# it gets named `.venv` -ENV POETRY_VIRTUALENVS_IN_PROJECT=1 -ENV POETRY_VIRTUALENVS_CREATE=1 - -# Set virtual_env variable -ENV VIRTUAL_ENV=/.venv -# Prepend virtual environments path -ENV PATH="${VIRTUAL_ENV}/bin:${POETRY_VENV}/bin:${PATH}" - -# Creating a virtual environment just for poetry and install it with pip -RUN python3 -m venv $POETRY_VENV \ - && $POETRY_VENV/bin/pip3 install poetry==${POETRY_VERSION} - -# Copy project requirement files here to ensure they will be cached. -COPY pyproject.toml poetry.lock ./ - -# Install dependencies. -RUN --mount=type=cache,target=$POETRY_CACHE_DIR poetry install --without dev --no-interaction --no-cache +COPY requirements.txt requirements.txt +RUN pip install -r requirements.txt \ No newline at end of file diff --git a/CheckQC/poetry.lock b/CheckQC/poetry.lock deleted file mode 100644 index 5222856b..00000000 --- a/CheckQC/poetry.lock +++ /dev/null @@ -1,580 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "coverage" -version = "7.6.4" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"}, - {file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"}, - {file = "coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72"}, - {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51"}, - {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491"}, - {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b"}, - {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea"}, - {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a"}, - {file = "coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa"}, - {file = "coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172"}, - {file = "coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b"}, - {file = "coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25"}, - {file = "coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546"}, - {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b"}, - {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e"}, - {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718"}, - {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db"}, - {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522"}, - {file = "coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf"}, - {file = "coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19"}, - {file = "coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2"}, - {file = "coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117"}, - {file = "coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613"}, - {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27"}, - {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52"}, - {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2"}, - {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1"}, - {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5"}, - {file = "coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17"}, - {file = "coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08"}, - {file = "coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9"}, - {file = "coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba"}, - {file = "coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c"}, - {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06"}, - {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f"}, - {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b"}, - {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21"}, - {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a"}, - {file = "coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e"}, - {file = "coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963"}, - {file = "coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f"}, - {file = "coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806"}, - {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11"}, - {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3"}, - {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a"}, - {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc"}, - {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70"}, - {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef"}, - {file = "coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e"}, - {file = "coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1"}, - {file = "coverage-7.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3"}, - {file = "coverage-7.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c"}, - {file = "coverage-7.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076"}, - {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376"}, - {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0"}, - {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858"}, - {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111"}, - {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901"}, - {file = "coverage-7.6.4-cp39-cp39-win32.whl", hash = "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09"}, - {file = "coverage-7.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f"}, - {file = "coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e"}, - {file = "coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73"}, -] - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "flake8" -version = "7.1.1" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = ">=3.8.1" -files = [ - {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"}, - {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.12.0,<2.13.0" -pyflakes = ">=3.2.0,<3.3.0" - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - -[[package]] -name = "numpy" -version = "1.26.4" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, - {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, - {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, - {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, - {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, - {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, - {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, - {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, - {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, - {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, - {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, -] - -[[package]] -name = "packaging" -version = "24.1" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, -] - -[[package]] -name = "pandas" -version = "2.1.4" -description = "Powerful data structures for data analysis, time series, and statistics" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pandas-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bdec823dc6ec53f7a6339a0e34c68b144a7a1fd28d80c260534c39c62c5bf8c9"}, - {file = "pandas-2.1.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:294d96cfaf28d688f30c918a765ea2ae2e0e71d3536754f4b6de0ea4a496d034"}, - {file = "pandas-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b728fb8deba8905b319f96447a27033969f3ea1fea09d07d296c9030ab2ed1d"}, - {file = "pandas-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00028e6737c594feac3c2df15636d73ace46b8314d236100b57ed7e4b9ebe8d9"}, - {file = "pandas-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:426dc0f1b187523c4db06f96fb5c8d1a845e259c99bda74f7de97bd8a3bb3139"}, - {file = "pandas-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:f237e6ca6421265643608813ce9793610ad09b40154a3344a088159590469e46"}, - {file = "pandas-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b7d852d16c270e4331f6f59b3e9aa23f935f5c4b0ed2d0bc77637a8890a5d092"}, - {file = "pandas-2.1.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd7d5f2f54f78164b3d7a40f33bf79a74cdee72c31affec86bfcabe7e0789821"}, - {file = "pandas-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0aa6e92e639da0d6e2017d9ccff563222f4eb31e4b2c3cf32a2a392fc3103c0d"}, - {file = "pandas-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d797591b6846b9db79e65dc2d0d48e61f7db8d10b2a9480b4e3faaddc421a171"}, - {file = "pandas-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2d3e7b00f703aea3945995ee63375c61b2e6aa5aa7871c5d622870e5e137623"}, - {file = "pandas-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:dc9bf7ade01143cddc0074aa6995edd05323974e6e40d9dbde081021ded8510e"}, - {file = "pandas-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:482d5076e1791777e1571f2e2d789e940dedd927325cc3cb6d0800c6304082f6"}, - {file = "pandas-2.1.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8a706cfe7955c4ca59af8c7a0517370eafbd98593155b48f10f9811da440248b"}, - {file = "pandas-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0513a132a15977b4a5b89aabd304647919bc2169eac4c8536afb29c07c23540"}, - {file = "pandas-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9f17f2b6fc076b2a0078862547595d66244db0f41bf79fc5f64a5c4d635bead"}, - {file = "pandas-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:45d63d2a9b1b37fa6c84a68ba2422dc9ed018bdaa668c7f47566a01188ceeec1"}, - {file = "pandas-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:f69b0c9bb174a2342818d3e2778584e18c740d56857fc5cdb944ec8bbe4082cf"}, - {file = "pandas-2.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3f06bda01a143020bad20f7a85dd5f4a1600112145f126bc9e3e42077c24ef34"}, - {file = "pandas-2.1.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab5796839eb1fd62a39eec2916d3e979ec3130509930fea17fe6f81e18108f6a"}, - {file = "pandas-2.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edbaf9e8d3a63a9276d707b4d25930a262341bca9874fcb22eff5e3da5394732"}, - {file = "pandas-2.1.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ebfd771110b50055712b3b711b51bee5d50135429364d0498e1213a7adc2be8"}, - {file = "pandas-2.1.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8ea107e0be2aba1da619cc6ba3f999b2bfc9669a83554b1904ce3dd9507f0860"}, - {file = "pandas-2.1.4-cp39-cp39-win_amd64.whl", hash = "sha256:d65148b14788b3758daf57bf42725caa536575da2b64df9964c563b015230984"}, - {file = "pandas-2.1.4.tar.gz", hash = "sha256:fcb68203c833cc735321512e13861358079a96c174a61f5116a1de89c58c0ef7"}, -] - -[package.dependencies] -numpy = [ - {version = ">=1.23.2,<2", markers = "python_version == \"3.11\""}, - {version = ">=1.26.0,<2", markers = "python_version >= \"3.12\""}, -] -python-dateutil = ">=2.8.2" -pytz = ">=2020.1" -tzdata = ">=2022.1" - -[package.extras] -all = ["PyQt5 (>=5.15.6)", "SQLAlchemy (>=1.4.36)", "beautifulsoup4 (>=4.11.1)", "bottleneck (>=1.3.4)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=0.8.1)", "fsspec (>=2022.05.0)", "gcsfs (>=2022.05.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.8.0)", "matplotlib (>=3.6.1)", "numba (>=0.55.2)", "numexpr (>=2.8.0)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pandas-gbq (>=0.17.5)", "psycopg2 (>=2.9.3)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.5)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "pyxlsb (>=1.0.9)", "qtpy (>=2.2.0)", "s3fs (>=2022.05.0)", "scipy (>=1.8.1)", "tables (>=3.7.0)", "tabulate (>=0.8.10)", "xarray (>=2022.03.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)", "zstandard (>=0.17.0)"] -aws = ["s3fs (>=2022.05.0)"] -clipboard = ["PyQt5 (>=5.15.6)", "qtpy (>=2.2.0)"] -compression = ["zstandard (>=0.17.0)"] -computation = ["scipy (>=1.8.1)", "xarray (>=2022.03.0)"] -consortium-standard = ["dataframe-api-compat (>=0.1.7)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pyxlsb (>=1.0.9)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)"] -feather = ["pyarrow (>=7.0.0)"] -fss = ["fsspec (>=2022.05.0)"] -gcp = ["gcsfs (>=2022.05.0)", "pandas-gbq (>=0.17.5)"] -hdf5 = ["tables (>=3.7.0)"] -html = ["beautifulsoup4 (>=4.11.1)", "html5lib (>=1.1)", "lxml (>=4.8.0)"] -mysql = ["SQLAlchemy (>=1.4.36)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.8.10)"] -parquet = ["pyarrow (>=7.0.0)"] -performance = ["bottleneck (>=1.3.4)", "numba (>=0.55.2)", "numexpr (>=2.8.0)"] -plot = ["matplotlib (>=3.6.1)"] -postgresql = ["SQLAlchemy (>=1.4.36)", "psycopg2 (>=2.9.3)"] -spss = ["pyreadstat (>=1.1.5)"] -sql-other = ["SQLAlchemy (>=1.4.36)"] -test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.8.0)"] - -[[package]] -name = "pluggy" -version = "1.5.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] - -[[package]] -name = "pyaml" -version = "24.9.0" -description = "PyYAML-based module to produce a bit more pretty and readable YAML-serialized data" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyaml-24.9.0-py3-none-any.whl", hash = "sha256:31080551502f1014852b3c966a96c796adc79b4cf86e165f28ed83455bf19c62"}, - {file = "pyaml-24.9.0.tar.gz", hash = "sha256:e78dee8b0d4fed56bb9fa11a8a7858e6fade1ec70a9a122cee6736efac3e69b5"}, -] - -[package.dependencies] -PyYAML = "*" - -[package.extras] -anchors = ["unidecode"] - -[[package]] -name = "pycodestyle" -version = "2.12.1" -description = "Python style guide checker" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, - {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, -] - -[[package]] -name = "pyflakes" -version = "3.2.0" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, - {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, -] - -[[package]] -name = "pyjson" -version = "1.4.1" -description = "Compare the similarities between two JSONs." -optional = false -python-versions = "*" -files = [ - {file = "pyjson-1.4.1-py3-none-any.whl", hash = "sha256:625ee332ca09056216595e232b562a8d42e1cba8b6695fc169b0a0c61587d56d"}, - {file = "pyjson-1.4.1.tar.gz", hash = "sha256:79ebe55cccb6224302baca9f7119927c73dcb1c18c3fed193db80cfb320d0ca6"}, -] - -[[package]] -name = "pytest" -version = "8.3.3" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, - {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2" - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-cov" -version = "3.0.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.6" -files = [ - {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, - {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, -] - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] - -[[package]] -name = "pytest-datadir" -version = "1.5.0" -description = "pytest plugin for test data directories and files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-datadir-1.5.0.tar.gz", hash = "sha256:1617ed92f9afda0c877e4eac91904b5f779d24ba8f5e438752e3ae39d8d2ee3f"}, - {file = "pytest_datadir-1.5.0-py3-none-any.whl", hash = "sha256:34adf361bcc7b37961bbc1dfa8d25a4829e778bab461703c38a5c50ca9c36dc8"}, -] - -[package.dependencies] -pytest = ">=5.0" - -[[package]] -name = "pytest-datafiles" -version = "2.0" -description = "py.test plugin to create a 'tmpdir' containing predefined files/directories." -optional = false -python-versions = "*" -files = [ - {file = "pytest-datafiles-2.0.tar.gz", hash = "sha256:143329cbb1dbbb07af24f88fa4668e2f59ce233696cf12c49fd1c98d1756dbf9"}, - {file = "pytest_datafiles-2.0-py2.py3-none-any.whl", hash = "sha256:e349b6ad7bcca111f3677b7201d3ca81f93b5e09dcfae8ee2be2c3cae9f55bc7"}, -] - -[package.dependencies] -py = "*" -pytest = ">=3.6" - -[[package]] -name = "pytest-dataset" -version = "0.3.2" -description = "Plugin for loading different datasets for pytest by prefix from json or yaml files" -optional = false -python-versions = "*" -files = [ - {file = "pytest-dataset-0.3.2.tar.gz", hash = "sha256:15d018f589b38f690408936fa29bce84a9a16bd9f4cea39e384470ff70920cb6"}, -] - -[package.dependencies] -pyaml = "*" -pyjson = "*" -pytest = "*" - -[[package]] -name = "pytest-flake8" -version = "1.0.7" -description = "pytest plugin to check FLAKE8 requirements" -optional = false -python-versions = "*" -files = [ - {file = "pytest-flake8-1.0.7.tar.gz", hash = "sha256:f0259761a903563f33d6f099914afef339c085085e643bee8343eb323b32dd6b"}, - {file = "pytest_flake8-1.0.7-py2.py3-none-any.whl", hash = "sha256:c28cf23e7d359753c896745fd4ba859495d02e16c84bac36caa8b1eec58f5bc1"}, -] - -[package.dependencies] -flake8 = ">=3.5" -pytest = ">=3.5" - -[[package]] -name = "pytest-mock" -version = "3.8.2" -description = "Thin-wrapper around the mock package for easier use with pytest" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-mock-3.8.2.tar.gz", hash = "sha256:77f03f4554392558700295e05aed0b1096a20d4a60a4f3ddcde58b0c31c8fca2"}, - {file = "pytest_mock-3.8.2-py3-none-any.whl", hash = "sha256:8a9e226d6c0ef09fcf20c94eb3405c388af438a90f3e39687f84166da82d5948"}, -] - -[package.dependencies] -pytest = ">=5.0" - -[package.extras] -dev = ["pre-commit", "pytest-asyncio", "tox"] - -[[package]] -name = "pytest-raises" -version = "0.11" -description = "An implementation of pytest.raises as a pytest.mark fixture" -optional = false -python-versions = "*" -files = [ - {file = "pytest-raises-0.11.tar.gz", hash = "sha256:f64a4dbcb5f89c100670fe83d87a5cd9d956586db461c5c628f7eb94b749c90b"}, - {file = "pytest_raises-0.11-py2.py3-none-any.whl", hash = "sha256:33a1351f2debb9f74ca6ef70e374899f608a1217bf13ca4a0767f37b49e9cdda"}, -] - -[package.dependencies] -pytest = ">=3.2.2" - -[package.extras] -develop = ["pylint", "pytest-cov"] - -[[package]] -name = "pytest-unordered" -version = "0.5.2" -description = "Test equality of unordered collections in pytest" -optional = false -python-versions = "*" -files = [ - {file = "pytest-unordered-0.5.2.tar.gz", hash = "sha256:8187e6d68a7d54e5447e88c229cbeafa38205e55baf7da7ae57cc965c1ecdbb3"}, - {file = "pytest_unordered-0.5.2-py3-none-any.whl", hash = "sha256:b01bb0e8ba80db6dd8c840fe24ad1804c8672919303dc9302688221390a7dc29"}, -] - -[package.dependencies] -pytest = ">=6.0.0" - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "pytz" -version = "2024.2" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, - {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.1" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.6" -files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, -] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "tzdata" -version = "2024.2" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -files = [ - {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, - {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, -] - -[metadata] -lock-version = "2.0" -python-versions = "^3.11.4" -content-hash = "8f17402f1b27d1ca0810b83fbb3b1dc42b5e5a1d98607f32c3d38aa20e989b51" diff --git a/CheckQC/pyproject.toml b/CheckQC/pyproject.toml deleted file mode 100644 index cfb9f284..00000000 --- a/CheckQC/pyproject.toml +++ /dev/null @@ -1,31 +0,0 @@ -[tool.poetry] -name = "checkqc" -version = "0.1.0" -description = "Check and judge qc metrics (files)" -authors = ["Bioinformatica Genetica "] -license = 'MIT' -package-mode = false - -[tool.poetry.dependencies] -python = "^3.11.4" -pandas = "2.1.4" -pyyaml = "6.0.1" - -[tool.poetry.group.dev.dependencies] -pytest = "^8.3.3" -pytest-cov = "3.0.0" -pytest-datadir = "1.5.0" -pytest-datafiles = "2.0" -pytest-dataset = "0.3.2" -pytest-flake8 = "1.0.7" -pytest-mock = "3.8.2" -pytest-raises = "0.11" -pytest-unordered = "0.5.2" - -[tool.ruff] -line-length = 127 -indent-width = 4 - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" diff --git a/CheckQC/requirements.txt b/CheckQC/requirements.txt new file mode 100644 index 00000000..66b6acda --- /dev/null +++ b/CheckQC/requirements.txt @@ -0,0 +1,12 @@ +pandas==2.1.4 +pytest==6.2.5 +pytest-cov==3.0.0 +pytest-datadir==1.5.0 +pytest-datafiles==2.0 +pytest-dataset==0.3.2 +pytest-flake8==1.0.7 +pytest-mock==3.8.2 +pytest-raises==0.11 +pytest-reqs==0.2.1 +pytest-unordered==0.5.2 +PyYAML==6.0.1 \ No newline at end of file diff --git a/GenderCheck/CompareGender.nf b/GenderCheck/CompareGender.nf index 7c87d4b4..31d9a182 100644 --- a/GenderCheck/CompareGender.nf +++ b/GenderCheck/CompareGender.nf @@ -3,7 +3,7 @@ process CompareGender { tag {"CompareGender ${sample_id}"} label 'CompareGender' label 'CompareGender_Pysam' - container = 'ghcr.io/umcugenetics/custommodules_gendercheck:1.0.1' + container = 'ghcr.io/umcugenetics/custommodules_gendercheck:1.0.0' shell = ['/bin/bash', '-eo', 'pipefail'] input: diff --git a/GenderCheck/Dockerfile b/GenderCheck/Dockerfile index 69bdb4db..6d4895c8 100644 --- a/GenderCheck/Dockerfile +++ b/GenderCheck/Dockerfile @@ -3,37 +3,9 @@ FROM --platform=linux/amd64 python:3.11 ################## METADATA ###################### LABEL base_image="python:3.11" -LABEL version="1.0.1" +LABEL version="1.0.0" LABEL extra.binaries="pysam,pytest" ################## INSTALLATIONS ###################### -# Use poetry to install virtualenv. -ENV POETRY_VERSION=1.8.3 -ENV POETRY_HOME=/opt/poetry -ENV POETRY_VENV=/opt/poetry-venv - -# Tell Poetry where to place its cache and virtual environment -ENV POETRY_CACHE_DIR=/tmp/poetry_cache - -# Do not ask any interactive question -ENV POETRY_NO_INTERACTION=1 - -# Make poetry create the virtual environment in the project's root -# it gets named `.venv` -ENV POETRY_VIRTUALENVS_IN_PROJECT=1 -ENV POETRY_VIRTUALENVS_CREATE=1 - -# Set virtual_env variable -ENV VIRTUAL_ENV=/.venv -# Prepend virtual environments path -ENV PATH="${VIRTUAL_ENV}/bin:${POETRY_VENV}/bin:${PATH}" - -# Creating a virtual environment just for poetry and install it with pip -RUN python3 -m venv $POETRY_VENV \ - && $POETRY_VENV/bin/pip3 install poetry==${POETRY_VERSION} - -# Copy project requirement files here to ensure they will be cached. -COPY pyproject.toml poetry.lock ./ - -# Install dependencies. -RUN --mount=type=cache,target=$POETRY_CACHE_DIR poetry install --without dev --no-interaction --no-cache +COPY requirements.txt requirements.txt +RUN pip install -r requirements.txt \ No newline at end of file diff --git a/GenderCheck/poetry.lock b/GenderCheck/poetry.lock deleted file mode 100644 index 7220270b..00000000 --- a/GenderCheck/poetry.lock +++ /dev/null @@ -1,124 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "packaging" -version = "23.2" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.7" -files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, -] - -[[package]] -name = "pluggy" -version = "1.5.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pysam" -version = "0.22.0" -description = "Package for reading, manipulating, and writing genomic data" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pysam-0.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:116278a7caa122b2b8acc56d13b3599be9b1236f27a12488bffc306858ff0d57"}, - {file = "pysam-0.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da2f1af461e44d5c2c7210d458ee216f8ab98486adf1eea6c88eea5c1058a62f"}, - {file = "pysam-0.22.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:021fbf6874ad998aba19be33828ad9d23d52273643793488ac4b12917d714c68"}, - {file = "pysam-0.22.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:26199e403855b9da45341d25682e0df27013687d9cb1b4fd328136fbd506292b"}, - {file = "pysam-0.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9bfebf89b1dc2ff6f88d64b5f05d8630deb89562b22764f8ee7f6fa9e677bb91"}, - {file = "pysam-0.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:942dd4a2263996bc2daa21200886e9fde027f32ce8820e7832b20bbdb97eb393"}, - {file = "pysam-0.22.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:83776ba587eb9575a209efed1cedb49d69c5fa6cc520dd722a0a09d0bb4e9b87"}, - {file = "pysam-0.22.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:4779a99d1ece17a98724d87a5c10c455cf212b3baa3a8399d3d072e4d0ae5ba0"}, - {file = "pysam-0.22.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bb61bf30c15f6767403b423b04c293e96fd7635457b506c849aafcf48fc13242"}, - {file = "pysam-0.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:32042e0bf3c5dd8554769442c2e1f7b6ada902c33ee44c616d0403e7acd12ee3"}, - {file = "pysam-0.22.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f23b2f47528b94e8abe3b700103fb1214c623ae1c1b8125ecf22d4d33d76720f"}, - {file = "pysam-0.22.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cfd2b858c7405cf38c730cba779ddf9f8cff28b4842c6440e64781650dcb9a52"}, - {file = "pysam-0.22.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:87dbf72f3e61fd6d3f92b1b683d9a9e797b6cc213ffcd971899f24a16f9f6e8f"}, - {file = "pysam-0.22.0-cp36-cp36m-manylinux_2_28_aarch64.whl", hash = "sha256:9af1cd3d07fd4c84e9b3d8a46c65b25f95278185bc6d44c4a48951679d5189ac"}, - {file = "pysam-0.22.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:f73d7923c89618fb7024875ed8eddc5fb0c911f430e3495de482fcee48143e45"}, - {file = "pysam-0.22.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6ffe5c98725fea54b1b2aa8f14a60ee9ceaed32c04460d1b861a62603dcd7153"}, - {file = "pysam-0.22.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:34f5653a82138d28a8e86205785a0398eb6c89f776b4145ff42783168757323c"}, - {file = "pysam-0.22.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:9d3ebb1515c2fd9b11823469e5b211ca3cc89e976c00c284a2190804c9f11726"}, - {file = "pysam-0.22.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b8e18520e7a79bad91b44cf9199c7fa42cec5c3020024d7ef9a7161d0099bf8"}, - {file = "pysam-0.22.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a98d1ddca64943f3ead507721e52466aea2f7303e549d4960a2eb1d9fff8e3d7"}, - {file = "pysam-0.22.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:6d6aa2346b11ad35e88c65eb0067321318c25c7f35f75c98061173eabefcf8b0"}, - {file = "pysam-0.22.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:4f6657a09c81333adb5545cf9a20d4c2ca1686acf8609ad58f13b3ec1b52a9cf"}, - {file = "pysam-0.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:93eb12be3822fb387e5438811f62a0f5e56c1edd5c830aaa316fb50d3d0bc181"}, - {file = "pysam-0.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9ba53f9b0b2c5cb57908855cdb35a31b34c5211d215aa01bdb3e9b3d05c659cc"}, - {file = "pysam-0.22.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:1b84f99aa04e30bd1cc35c01bd41c2b7680131f56c71a740805aff8086f24b56"}, - {file = "pysam-0.22.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:481e4efbfbc07b6b92194a005cb9a98006c8378024f41c7b66c58b14f6e77f9c"}, - {file = "pysam-0.22.0.tar.gz", hash = "sha256:ab7a46973cf0ab8c6ac327f4c3fb67698d7ccbeef8631a716898c6ba01ef3e45"}, -] - -[[package]] -name = "pytest" -version = "8.3.3" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, - {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2" - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-datadir" -version = "1.5.0" -description = "pytest plugin for test data directories and files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-datadir-1.5.0.tar.gz", hash = "sha256:1617ed92f9afda0c877e4eac91904b5f779d24ba8f5e438752e3ae39d8d2ee3f"}, - {file = "pytest_datadir-1.5.0-py3-none-any.whl", hash = "sha256:34adf361bcc7b37961bbc1dfa8d25a4829e778bab461703c38a5c50ca9c36dc8"}, -] - -[package.dependencies] -pytest = ">=5.0" - -[metadata] -lock-version = "2.0" -python-versions = "^3.11" -content-hash = "9d44fccb4bb8753f5ac3dff1ee21c05e4475867600ce65d0c6809bc7d3628302" diff --git a/GenderCheck/pyproject.toml b/GenderCheck/pyproject.toml deleted file mode 100644 index 9a4d458b..00000000 --- a/GenderCheck/pyproject.toml +++ /dev/null @@ -1,26 +0,0 @@ -[tool.poetry] -name = "gendercheck" -version = "0.1.0" -description = "" -authors = ["Bioinformatica Genetica "] -license = "MIT" -package-mode = false - -[tool.poetry.dependencies] -python = "^3.11" -iniconfig = "2.0.0" -packaging = "23.2" -pluggy = "^1.5.0" -pysam = "0.22.0" - -[tool.poetry.group.dev.dependencies] -pytest = "^8.3.3" -pytest-datadir = "^1.5.0" - -[tool.ruff] -line-length = 127 -indent-width = 4 - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" diff --git a/GenderCheck/requirements.txt b/GenderCheck/requirements.txt new file mode 100644 index 00000000..7664a681 --- /dev/null +++ b/GenderCheck/requirements.txt @@ -0,0 +1,5 @@ +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +pysam==0.22.0 +pytest==8.0.2 \ No newline at end of file diff --git a/GenderCheck/test_calculate_gender/test_bam.bam b/GenderCheck/test_bam.bam similarity index 100% rename from GenderCheck/test_calculate_gender/test_bam.bam rename to GenderCheck/test_bam.bam diff --git a/GenderCheck/test_calculate_gender/test_bam.bam.bai b/GenderCheck/test_bam.bam.bai similarity index 100% rename from GenderCheck/test_calculate_gender/test_bam.bam.bai rename to GenderCheck/test_bam.bam.bai diff --git a/GenderCheck/test_calculate_gender.py b/GenderCheck/test_calculate_gender.py index 5dd232fd..85083198 100644 --- a/GenderCheck/test_calculate_gender.py +++ b/GenderCheck/test_calculate_gender.py @@ -1,11 +1,7 @@ -#!/usr/bin/env python -# Import statements, alphabetic order of main package. -# Third party libraries alphabetic order of main package. -import pytest - -# Custom libraries alphabetic order of main package. import calculate_gender +import pytest + class TestIsValidRead(): @@ -16,11 +12,11 @@ def __init__(self, qual, start, end): self.reference_end = end @pytest.mark.parametrize("read,min_mapping_qual,expected", [ - (MyObject(19, True, True), 20, False), # Mapping quality is below the threshold - (MyObject(20, True, True), 20, True), # Mapping quality is equal to the threshold - (MyObject(20, True, True), 19, True), # Mapping quality is higher than the threshold - (MyObject(20, False, True), 20, False), # Reference_end is false - (MyObject(20, True, False), 20, False), # Reference_start is false + (MyObject(19, True, True), 20, False), # mapping quality is below the threshold + (MyObject(20, True, True), 20, True), # mapping quality is equal to the threshold + (MyObject(20, True, True), 19, True), # mapping quality is higher than the threshold + (MyObject(20, False, True), 20, False), # reference_end is false + (MyObject(20, True, False), 20, False), # reference_start is false ]) def test_is_valid_read(self, read, min_mapping_qual, expected): assert expected == calculate_gender.is_valid_read(read, min_mapping_qual) @@ -67,11 +63,11 @@ def test_not_allowed_genders(self, input_gender): class TestGetGenderFromBam(): @pytest.mark.parametrize("bam,min_mapping_qual,locus_y,ratio_y,expected", [ - ("test_bam.bam", 20, "Y:2649520-59034050", 0.02, "male"), # Output male below - ("test_bam.bam", 20, "Y:2649520-59034050", 0.22, "female"), # Output female + ("./test_bam.bam", 20, "Y:2649520-59034050", 0.02, "male"), # output male below + ("./test_bam.bam", 20, "Y:2649520-59034050", 0.22, "female"), # output female ]) - def test_get_gender_from_bam(self, bam, min_mapping_qual, locus_y, ratio_y, expected, datadir): - assert expected == calculate_gender.get_gender_from_bam(f"{datadir}/{bam}", min_mapping_qual, locus_y, ratio_y) + def test_get_gender_from_bam(self, bam, min_mapping_qual, locus_y, ratio_y, expected): + assert expected == calculate_gender.get_gender_from_bam(bam, min_mapping_qual, locus_y, ratio_y) class TestCompareGender(): diff --git a/MosaicHunter/1.0.0/MosaicHunter.nf b/MosaicHunter/1.0.0/MosaicHunter.nf index e60c9ff1..3f04dae9 100644 --- a/MosaicHunter/1.0.0/MosaicHunter.nf +++ b/MosaicHunter/1.0.0/MosaicHunter.nf @@ -3,7 +3,7 @@ process MosaicHunterGetGender { tag {"MosaicHunterGetGender ${sample_id}"} label 'MosaicHunterGetGender' - container = 'ghcr.io/umcugenetics/custommodules_gendercheck:1.0.1' + container = 'ghcr.io/umcugenetics/custommodules_gendercheck:1.0.0' shell = ['/bin/bash', '-eo', 'pipefail'] /* diff --git a/MosaicHunter/1.0.0/poetry.lock b/MosaicHunter/1.0.0/poetry.lock deleted file mode 100644 index cfb23f6a..00000000 --- a/MosaicHunter/1.0.0/poetry.lock +++ /dev/null @@ -1,124 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "packaging" -version = "23.2" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.7" -files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, -] - -[[package]] -name = "pluggy" -version = "1.5.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pysam" -version = "0.22.0" -description = "Package for reading, manipulating, and writing genomic data" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pysam-0.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:116278a7caa122b2b8acc56d13b3599be9b1236f27a12488bffc306858ff0d57"}, - {file = "pysam-0.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da2f1af461e44d5c2c7210d458ee216f8ab98486adf1eea6c88eea5c1058a62f"}, - {file = "pysam-0.22.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:021fbf6874ad998aba19be33828ad9d23d52273643793488ac4b12917d714c68"}, - {file = "pysam-0.22.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:26199e403855b9da45341d25682e0df27013687d9cb1b4fd328136fbd506292b"}, - {file = "pysam-0.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9bfebf89b1dc2ff6f88d64b5f05d8630deb89562b22764f8ee7f6fa9e677bb91"}, - {file = "pysam-0.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:942dd4a2263996bc2daa21200886e9fde027f32ce8820e7832b20bbdb97eb393"}, - {file = "pysam-0.22.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:83776ba587eb9575a209efed1cedb49d69c5fa6cc520dd722a0a09d0bb4e9b87"}, - {file = "pysam-0.22.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:4779a99d1ece17a98724d87a5c10c455cf212b3baa3a8399d3d072e4d0ae5ba0"}, - {file = "pysam-0.22.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bb61bf30c15f6767403b423b04c293e96fd7635457b506c849aafcf48fc13242"}, - {file = "pysam-0.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:32042e0bf3c5dd8554769442c2e1f7b6ada902c33ee44c616d0403e7acd12ee3"}, - {file = "pysam-0.22.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f23b2f47528b94e8abe3b700103fb1214c623ae1c1b8125ecf22d4d33d76720f"}, - {file = "pysam-0.22.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cfd2b858c7405cf38c730cba779ddf9f8cff28b4842c6440e64781650dcb9a52"}, - {file = "pysam-0.22.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:87dbf72f3e61fd6d3f92b1b683d9a9e797b6cc213ffcd971899f24a16f9f6e8f"}, - {file = "pysam-0.22.0-cp36-cp36m-manylinux_2_28_aarch64.whl", hash = "sha256:9af1cd3d07fd4c84e9b3d8a46c65b25f95278185bc6d44c4a48951679d5189ac"}, - {file = "pysam-0.22.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:f73d7923c89618fb7024875ed8eddc5fb0c911f430e3495de482fcee48143e45"}, - {file = "pysam-0.22.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6ffe5c98725fea54b1b2aa8f14a60ee9ceaed32c04460d1b861a62603dcd7153"}, - {file = "pysam-0.22.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:34f5653a82138d28a8e86205785a0398eb6c89f776b4145ff42783168757323c"}, - {file = "pysam-0.22.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:9d3ebb1515c2fd9b11823469e5b211ca3cc89e976c00c284a2190804c9f11726"}, - {file = "pysam-0.22.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b8e18520e7a79bad91b44cf9199c7fa42cec5c3020024d7ef9a7161d0099bf8"}, - {file = "pysam-0.22.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a98d1ddca64943f3ead507721e52466aea2f7303e549d4960a2eb1d9fff8e3d7"}, - {file = "pysam-0.22.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:6d6aa2346b11ad35e88c65eb0067321318c25c7f35f75c98061173eabefcf8b0"}, - {file = "pysam-0.22.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:4f6657a09c81333adb5545cf9a20d4c2ca1686acf8609ad58f13b3ec1b52a9cf"}, - {file = "pysam-0.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:93eb12be3822fb387e5438811f62a0f5e56c1edd5c830aaa316fb50d3d0bc181"}, - {file = "pysam-0.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9ba53f9b0b2c5cb57908855cdb35a31b34c5211d215aa01bdb3e9b3d05c659cc"}, - {file = "pysam-0.22.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:1b84f99aa04e30bd1cc35c01bd41c2b7680131f56c71a740805aff8086f24b56"}, - {file = "pysam-0.22.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:481e4efbfbc07b6b92194a005cb9a98006c8378024f41c7b66c58b14f6e77f9c"}, - {file = "pysam-0.22.0.tar.gz", hash = "sha256:ab7a46973cf0ab8c6ac327f4c3fb67698d7ccbeef8631a716898c6ba01ef3e45"}, -] - -[[package]] -name = "pytest" -version = "8.0.2" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, - {file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.3.0,<2.0" - -[package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-datadir" -version = "1.5.0" -description = "pytest plugin for test data directories and files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-datadir-1.5.0.tar.gz", hash = "sha256:1617ed92f9afda0c877e4eac91904b5f779d24ba8f5e438752e3ae39d8d2ee3f"}, - {file = "pytest_datadir-1.5.0-py3-none-any.whl", hash = "sha256:34adf361bcc7b37961bbc1dfa8d25a4829e778bab461703c38a5c50ca9c36dc8"}, -] - -[package.dependencies] -pytest = ">=5.0" - -[metadata] -lock-version = "2.0" -python-versions = "^3.11" -content-hash = "d2dc5a03871e9ede54e5fc47a1cb8cc3f1adefd93e3f4b354b4871cbdc60e08d" diff --git a/MosaicHunter/1.0.0/pyproject.toml b/MosaicHunter/1.0.0/pyproject.toml deleted file mode 100644 index 7b74836a..00000000 --- a/MosaicHunter/1.0.0/pyproject.toml +++ /dev/null @@ -1,26 +0,0 @@ -[tool.poetry] -name = "mosaichunter" -version = "1.0.0" -description = "" -authors = ["Bioinformatica Genetica "] -license = "MIT" -package-mode = false - -[tool.poetry.dependencies] -python = "^3.11" -iniconfig = "2.0.0" -packaging = "23.2" -pluggy = "1.5.0" -pysam = "0.22.0" - -[tool.poetry.group.dev.dependencies] -pytest = "8.0.2" -pytest-datadir = "^1.5.0" - -[tool.ruff] -line-length = 127 -indent-width = 4 - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" diff --git a/MosaicHunter/1.0.0/requirements.txt b/MosaicHunter/1.0.0/requirements.txt new file mode 100644 index 00000000..7664a681 --- /dev/null +++ b/MosaicHunter/1.0.0/requirements.txt @@ -0,0 +1,5 @@ +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +pysam==0.22.0 +pytest==8.0.2 \ No newline at end of file diff --git a/MosaicHunter/1.0.0/test_get_gender_from_bam_chrx/test_bam.bam b/MosaicHunter/1.0.0/test_bam.bam similarity index 100% rename from MosaicHunter/1.0.0/test_get_gender_from_bam_chrx/test_bam.bam rename to MosaicHunter/1.0.0/test_bam.bam diff --git a/MosaicHunter/1.0.0/test_get_gender_from_bam_chrx/test_bam.bam.bai b/MosaicHunter/1.0.0/test_bam.bam.bai similarity index 100% rename from MosaicHunter/1.0.0/test_get_gender_from_bam_chrx/test_bam.bam.bai rename to MosaicHunter/1.0.0/test_bam.bam.bai diff --git a/MosaicHunter/1.0.0/test_get_gender_from_bam_chrx.py b/MosaicHunter/1.0.0/test_get_gender_from_bam_chrx.py index a3fa47a4..0a494ea9 100644 --- a/MosaicHunter/1.0.0/test_get_gender_from_bam_chrx.py +++ b/MosaicHunter/1.0.0/test_get_gender_from_bam_chrx.py @@ -10,11 +10,11 @@ def __init__(self, qual, start, end): self.reference_end = end @pytest.mark.parametrize("read,mapping_qual,expected", [ - (ValidReadObject(19, True, True), 20, False), # Mapping quality is below the threshold - (ValidReadObject(20, True, True), 20, True), # Mapping quality is equal to the threshold - (ValidReadObject(20, True, True), 19, True), # Mapping quality is higher than the threshold - (ValidReadObject(20, False, True), 20, False), # Reference_end is false - (ValidReadObject(20, True, False), 20, False), # Reference_start is false + (ValidReadObject(19, True, True), 20, False), # mapping quality is below the threshold + (ValidReadObject(20, True, True), 20, True), # mapping quality is equal to the threshold + (ValidReadObject(20, True, True), 19, True), # mapping quality is higher than the threshold + (ValidReadObject(20, False, True), 20, False), # reference_end is false + (ValidReadObject(20, True, False), 20, False), # reference_start is false ]) def test_is_valid_read(self, read, mapping_qual, expected): assert expected == get_gender_from_bam_chrx.is_valid_read(read, mapping_qual) @@ -22,15 +22,13 @@ def test_is_valid_read(self, read, mapping_qual, expected): class TestGetGenderFromBam: @pytest.mark.parametrize("bam,mapping_qual,locus_x,ratio_x_threshold_male,ratio_x_threshold_female,expected_outcome", [ - ("test_bam.bam", 20, "X:2699520-154931044", 3.5, 4.5, ("F", False)), - ("test_bam.bam", 20, "X:2699520-154931044", 5.5, 7.5, ("M", False)), - ("test_bam.bam", 20, "X:2699520-154931044", 4.5, 6.5, ("F", True)), + ("./test_bam.bam", 20, "X:2699520-154931044", 3.5, 4.5, ("F", False)), + ("./test_bam.bam", 20, "X:2699520-154931044", 5.5, 7.5, ("M", False)), + ("./test_bam.bam", 20, "X:2699520-154931044", 4.5, 6.5, ("F", True)), ]) - def test_get_gender_from_bam( - self, bam, mapping_qual, locus_x, ratio_x_threshold_male, ratio_x_threshold_female, expected_outcome, datadir - ): + def test_get_gender_from_bam(self, bam, mapping_qual, locus_x, ratio_x_threshold_male, ratio_x_threshold_female, expected_outcome): assert expected_outcome == get_gender_from_bam_chrx.get_gender_from_bam_chrx( - f"{datadir}/{bam}", mapping_qual, locus_x, ratio_x_threshold_male, ratio_x_threshold_female) + bam, mapping_qual, locus_x, ratio_x_threshold_male, ratio_x_threshold_female) class TestWriteGenderDataToFile: diff --git a/Utils/create_hsmetrics_summary.py b/Utils/create_hsmetrics_summary.py index 582b61e4..405e1ce1 100644 --- a/Utils/create_hsmetrics_summary.py +++ b/Utils/create_hsmetrics_summary.py @@ -8,7 +8,7 @@ parser.add_argument('hsmetrics_files', type=argparse.FileType('r'), nargs='*', help='HSMetric file') arguments = parser.parse_args() - interval_files_pattern = re.compile(r"BAIT_INTERVALS=\[(\S*)\].TARGET_INTERVALS=\[(\S*)\]") + interval_files_pattern = re.compile("BAIT_INTERVALS=\[(\S*)\].TARGET_INTERVALS=\[(\S*)\]") summary_header = [] summary_data = {} for hsmetrics_file in arguments.hsmetrics_files: diff --git a/Utils/get_stats_from_flagstat.py b/Utils/get_stats_from_flagstat.py index 75a145a7..2f46abbe 100644 --- a/Utils/get_stats_from_flagstat.py +++ b/Utils/get_stats_from_flagstat.py @@ -43,26 +43,12 @@ print("\n\t{0} %duplication\n".format(100*sample_dups/sample_mapped)) - print( - "Total raw reads: {total:,} reads " - "(Total throughput, 75bp={total_75bp:,} bp, 100bp={total_100bp:,} bp, 150bp={total_150bp:,} bp)".format( - total=counts['total'], - total_75bp=counts['total']*75, - total_100bp=counts['total']*100, - total_150bp=counts['total']*150 - ) - ) - - print( - "Total mapped reads: {total:,} reads " - "(Total throughput, 75bp={total_75bp:,} bp, 100bp={total_100bp:,} bp, 150bp={total_150bp:,} bp)" - .format( - total=counts['mapped'], - total_75bp=counts['mapped']*75, - total_100bp=counts['mapped']*100, - total_150bp=counts['mapped']*150 - ) - ) + print("Total raw reads: {total:,} reads (Total throughput, 75bp={total_75bp:,} bp, 100bp={total_100bp:,} bp, 150bp={total_150bp:,} bp)".format( + total=counts['total'], total_75bp=counts['total']*75, total_100bp=counts['total']*100, total_150bp=counts['total']*150 + )) + print("Total mapped reads: {total:,} reads (Total throughput, 75bp={total_75bp:,} bp, 100bp={total_100bp:,} bp, 150bp={total_150bp:,} bp)".format( + total=counts['mapped'], total_75bp=counts['mapped']*75, total_100bp=counts['mapped']*100, total_150bp=counts['mapped']*150 + )) print("Average mapped per lib: {:,} reads".format(int(round(float(counts['mapped'])/float(counts['files']))))) print("Average dups per lib: {:,} reads".format(int(round(float(counts['dups'])/float(counts['files']))))) print("Average dups % per lib: {:.2f} %".format(100*float(counts['dups'])/float(counts['mapped']))) diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 0e005192..00000000 --- a/poetry.lock +++ /dev/null @@ -1,253 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. - -[[package]] -name = "cfgv" -version = "3.4.0" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "distlib" -version = "0.3.8" -description = "Distribution utilities" -optional = false -python-versions = "*" -files = [ - {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, - {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, -] - -[[package]] -name = "filelock" -version = "3.16.1" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.8" -files = [ - {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, - {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] -typing = ["typing-extensions (>=4.12.2)"] - -[[package]] -name = "identify" -version = "2.6.1" -description = "File identification library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, - {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "nodeenv" -version = "1.9.1" -description = "Node.js virtual environment builder" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, - {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, -] - -[[package]] -name = "packaging" -version = "24.1" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, -] - -[[package]] -name = "platformdirs" -version = "4.3.6" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, - {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.11.2)"] - -[[package]] -name = "pluggy" -version = "1.5.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pre-commit" -version = "3.8.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -optional = false -python-versions = ">=3.9" -files = [ - {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, - {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "pytest" -version = "8.3.3" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, - {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2" - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pyyaml" -version = "6.0.2" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "virtualenv" -version = "20.26.5" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.7" -files = [ - {file = "virtualenv-20.26.5-py3-none-any.whl", hash = "sha256:4f3ac17b81fba3ce3bd6f4ead2749a72da5929c01774948e243db9ba41df4ff6"}, - {file = "virtualenv-20.26.5.tar.gz", hash = "sha256:ce489cac131aa58f4b25e321d6d186171f78e6cb13fafbf32a840cee67733ff4"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<5" - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] - -[metadata] -lock-version = "2.0" -python-versions = "^3.11.4" -content-hash = "aa9362db1f4605a8d183a481eed9b3763b2a3382b38f9ed4a3989ee82a70c1c6" diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 67a3fbde..00000000 --- a/pyproject.toml +++ /dev/null @@ -1,22 +0,0 @@ -[tool.poetry] -name = "custommodules" -version = "0.1.0" -description = "custom nextflow processes and their linked files" -authors = ["Bioinformatica Genetica "] -license = "MIT" -readme = "README.md" - -[tool.poetry.dependencies] -python = "^3.11.4" - -[tool.poetry.group.dev.dependencies] -pre-commit = "^3.8.0" -pytest = "^8.3.3" - -[tool.ruff] -line-length = 127 -indent-width = 4 - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" From 6db86f5223d0553c21a0bc40ed370a388b1bc138 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 24 Mar 2026 10:18:40 +0100 Subject: [PATCH 158/161] fixed subset for internal standards in DIMS/GenerateQCOutput.R --- DIMS/GenerateQCOutput.R | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/DIMS/GenerateQCOutput.R b/DIMS/GenerateQCOutput.R index f83bb3fc..b618d7e8 100644 --- a/DIMS/GenerateQCOutput.R +++ b/DIMS/GenerateQCOutput.R @@ -208,7 +208,7 @@ if (dims_matrix == "Plasma") { } if (nrow(is_below_threshold) > 0) { - write.table(is_below_threshold, + write.table(cbind(is_below_threshold, scanmode = scanmode_is), file = "internal_standards_below_threshold.txt", row.names = FALSE, sep = "\t") } else { @@ -273,39 +273,38 @@ patterns <- c("^(P1002\\.)[[:digit:]]+_", "^(P1003\\.)[[:digit:]]+_", "^(P1005\\ positive_controls_index <- grepl(pattern = paste(patterns, collapse = "|"), column_list) positive_control_list <- column_list[positive_controls_index] -if (sum(positive_controls_index) > 0) { +if (positive_controls_index > 0) { # find if one or more positive control samples are missing pos_contr_warning <- c() if (all(sapply(c("^P1002", "^P1003", "^P1005"), function(x) any(grepl(x, positive_control_list))))) { - pos_contr_warning <- "All three positive controls are present" + cat("All three positive controls are present") } else { pos_contr_warning <- paste( "positive controls list is not complete. Only", - paste(positive_control_list, collapse = ", "), " present" + paste(positive_control_list, collapse = ", "), "is/are present" ) } if (length(positive_control_list) > 0) { # make positive control excel with specific HMDB_codes in combination with specific control samples positive_control <- NULL for (pos_ctrl in positive_control_list) { - pos_ctrl_samplename <- gsub("_Zscore", "", pos_ctrl) if (any(grepl("^P1002", pos_ctrl))) { - pa_sample_name <- positive_control_list[grepl(pos_ctrl_samplename, positive_control_list)] + pa_sample_name <- positive_control_list[grepl("P1002", positive_control_list)] pa_codes <- c("HMDB0000824", "HMDB0000725", "HMDB0000123") pa_names <- c("Propionylcarnitine", "Propionylglycine", "Glycine") pa_data <- get_pos_ctrl_data(outlist, pa_sample_name, pa_codes, pa_names) positive_control <- rbind(positive_control, pa_data) } if (any(grepl("^P1003", pos_ctrl))) { - pku_sample_name <- positive_control_list[grepl(pos_ctrl_samplename, positive_control_list)] + pku_sample_name <- positive_control_list[grepl("P1003", positive_control_list)] pku_codes <- c("HMDB0000159") pku_names <- c("L-Phenylalanine") pku_data <- get_pos_ctrl_data(outlist, pku_sample_name, pku_codes, pku_names) positive_control <- rbind(positive_control, pku_data) } if (any(grepl("^P1005", pos_ctrl))) { - lpi_sample_name <- positive_control_list[grepl(pos_ctrl_samplename, positive_control_list)] + lpi_sample_name <- positive_control_list[grepl("P1005", positive_control_list)] lpi_codes <- c("HMDB0000904", "HMDB0000641", "HMDB0000182") lpi_names <- c("Citrulline", "L-Glutamine", "L-Lysine") lpi_data <- get_pos_ctrl_data(outlist, lpi_sample_name, lpi_codes, lpi_names) @@ -327,14 +326,13 @@ if (sum(positive_controls_index) > 0) { file = paste0(outdir, "/", project, "_positive_control.xlsx"), sheetName = "Sheet1", col.names = TRUE, row.names = TRUE, append = FALSE ) - } - if (length(pos_contr_warning) == 0) { - pos_contr_warning <- "No positive controls found" } - write.table(pos_contr_warning, - file = paste(outdir, "positive_controls_warning.txt", sep = "/"), - row.names = FALSE, col.names = FALSE, quote = FALSE - ) + if (length(pos_contr_warning) != 0) { + write.table(pos_contr_warning, + file = paste(outdir, "positive_controls_warning.txt", sep = "/"), + row.names = FALSE, col.names = FALSE, quote = FALSE + ) + } } ### SST components output #### @@ -377,7 +375,7 @@ for (col_nr in seq_len(ncol(sst_intensities_df))) { # Round numeric value of Z-score columns to 2 decimal places sst_intensities_df[, col_nr] <- round(sst_intensities_df[, col_nr], 2) } else { - # Round numeric value of intensity columns to an intiger + # Round numeric value of intensity columns to an integer sst_intensities_df[, col_nr] <- round(sst_intensities_df[, col_nr]) } } @@ -407,10 +405,6 @@ if (sum(grepl("P1001", colnames(sst_intensities_df))) > 0) { sst_intensities_df_qc <- sst_intensities_df[sst_intensities_df[, zscore_column] < 2, ] sst_intensities_df_qc <- select(sst_intensities_df_qc, -c("CV_controls")) write.table(sst_intensities_df_qc, file = paste(outdir, "sst_qc.txt", sep = "/"), row.names = FALSE, sep = "\t") - # in case of an empty table, the column header doesn't need to appear in the mail - if (nrow(sst_intensities_df_qc) == 0) { - write.table("none", file = paste(outdir, "sst_qc.txt", sep = "/"), row.names = FALSE, col.names = FALSE) - } } else { write.table("no SST sample present", file = paste(outdir, "sst_qc.txt", sep = "/"), row.names = FALSE, col.names = FALSE) } From 3d527afb255045e5d4a4f93dadaacff8c4bb1d72 Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 24 Mar 2026 10:49:06 +0100 Subject: [PATCH 159/161] removed print statement in DIMS/export/generate_violin_plots_functions.R --- DIMS/export/generate_violin_plots_functions.R | 1 - 1 file changed, 1 deletion(-) diff --git a/DIMS/export/generate_violin_plots_functions.R b/DIMS/export/generate_violin_plots_functions.R index 499ad73c..468bbde2 100644 --- a/DIMS/export/generate_violin_plots_functions.R +++ b/DIMS/export/generate_violin_plots_functions.R @@ -782,7 +782,6 @@ run_diem_algorithm <- function(expected_biomarkers_df, zscore_patients_df, sampl if (sum(grepl("Zscore", sample_cols)) > 0) { sample_cols <- sample_cols[-grep("Zscore", sample_cols)] } - print(sample_cols) # Change Z-score to zero for specific cases zscore_expected_df <- zscore_expected_df %>% mutate(across( From 599dc86bb705c80c2e1a16c8c3efe4808267b7ef Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Tue, 24 Mar 2026 16:53:50 +0100 Subject: [PATCH 160/161] refactored positive control section in DIMS/GenerateQCOutput.R --- DIMS/GenerateQCOutput.R | 104 +++++++++++++++++++++------------------- 1 file changed, 54 insertions(+), 50 deletions(-) diff --git a/DIMS/GenerateQCOutput.R b/DIMS/GenerateQCOutput.R index b618d7e8..2efb4d55 100644 --- a/DIMS/GenerateQCOutput.R +++ b/DIMS/GenerateQCOutput.R @@ -272,67 +272,71 @@ column_list <- colnames(outlist) patterns <- c("^(P1002\\.)[[:digit:]]+_", "^(P1003\\.)[[:digit:]]+_", "^(P1005\\.)[[:digit:]]+_") positive_controls_index <- grepl(pattern = paste(patterns, collapse = "|"), column_list) positive_control_list <- column_list[positive_controls_index] +pos_contr_warning <- c() -if (positive_controls_index > 0) { - # find if one or more positive control samples are missing - pos_contr_warning <- c() +# find if one or more positive control samples are missing +if (sum(positive_controls_index) > 0) { if (all(sapply(c("^P1002", "^P1003", "^P1005"), function(x) any(grepl(x, positive_control_list))))) { - cat("All three positive controls are present") + pos_contr_warning <- "All three positive controls are present" } else { pos_contr_warning <- paste( "positive controls list is not complete. Only", paste(positive_control_list, collapse = ", "), "is/are present" ) } - if (length(positive_control_list) > 0) { - # make positive control excel with specific HMDB_codes in combination with specific control samples - positive_control <- NULL - for (pos_ctrl in positive_control_list) { - if (any(grepl("^P1002", pos_ctrl))) { - pa_sample_name <- positive_control_list[grepl("P1002", positive_control_list)] - pa_codes <- c("HMDB0000824", "HMDB0000725", "HMDB0000123") - pa_names <- c("Propionylcarnitine", "Propionylglycine", "Glycine") - pa_data <- get_pos_ctrl_data(outlist, pa_sample_name, pa_codes, pa_names) - positive_control <- rbind(positive_control, pa_data) - } - if (any(grepl("^P1003", pos_ctrl))) { - pku_sample_name <- positive_control_list[grepl("P1003", positive_control_list)] - pku_codes <- c("HMDB0000159") - pku_names <- c("L-Phenylalanine") - pku_data <- get_pos_ctrl_data(outlist, pku_sample_name, pku_codes, pku_names) - positive_control <- rbind(positive_control, pku_data) - } - if (any(grepl("^P1005", pos_ctrl))) { - lpi_sample_name <- positive_control_list[grepl("P1005", positive_control_list)] - lpi_codes <- c("HMDB0000904", "HMDB0000641", "HMDB0000182") - lpi_names <- c("Citrulline", "L-Glutamine", "L-Lysine") - lpi_data <- get_pos_ctrl_data(outlist, lpi_sample_name, lpi_codes, lpi_names) - positive_control <- rbind(positive_control, lpi_data) - } - } +} +# if there are no positive controls, generate a warning +if (length(pos_contr_warning) == 0) { + pos_contr_warning <- "No positive controls found" +} +write.table(pos_contr_warning, + file = paste(outdir, "positive_controls_warning.txt", sep = "/"), + row.names = FALSE, col.names = FALSE, quote = FALSE +) - positive_control$Zscore <- as.numeric(positive_control$Zscore) - # extra information added to excel for future reference. made in beginning of this script - positive_control$Matrix <- dims_matrix - positive_control$Rundate <- rundate - positive_control$Project <- project - - # Save results - save(positive_control, file = paste0(outdir, "/", project, "_positive_control.RData")) - # round the Z-scores to 2 digits - positive_control$Zscore <- round_df(positive_control$Zscore, 2) - write.xlsx(positive_control, - file = paste0(outdir, "/", project, "_positive_control.xlsx"), - sheetName = "Sheet1", col.names = TRUE, row.names = TRUE, append = FALSE - ) - } - if (length(pos_contr_warning) != 0) { - write.table(pos_contr_warning, - file = paste(outdir, "positive_controls_warning.txt", sep = "/"), - row.names = FALSE, col.names = FALSE, quote = FALSE - ) +# make positive control excel with specific HMDB_codes in combination with specific control samples +if (length(positive_control_list) > 0) { + positive_control <- NULL + for (pos_ctrl in positive_control_list) { + pos_ctrl_samplename <- gsub("_Zscore", "", pos_ctrl) + if (any(grepl("^P1002", pos_ctrl))) { + pa_sample_name <- positive_control_list[grepl(pos_ctrl_samplename, positive_control_list)] + pa_codes <- c("HMDB0000824", "HMDB0000725", "HMDB0000123") + pa_names <- c("Propionylcarnitine", "Propionylglycine", "Glycine") + pa_data <- get_pos_ctrl_data(outlist, pa_sample_name, pa_codes, pa_names) + positive_control <- rbind(positive_control, pa_data) + } + if (any(grepl("^P1003", pos_ctrl))) { + pku_sample_name <- positive_control_list[grepl(pos_ctrl_samplename, positive_control_list)] + pku_codes <- c("HMDB0000159") + pku_names <- c("L-Phenylalanine") + pku_data <- get_pos_ctrl_data(outlist, pku_sample_name, pku_codes, pku_names) + positive_control <- rbind(positive_control, pku_data) + } + if (any(grepl("^P1005", pos_ctrl))) { + lpi_sample_name <- positive_control_list[grepl(pos_ctrl_samplename, positive_control_list)] + lpi_codes <- c("HMDB0000904", "HMDB0000641", "HMDB0000182") + lpi_names <- c("Citrulline", "L-Glutamine", "L-Lysine") + lpi_data <- get_pos_ctrl_data(outlist, lpi_sample_name, lpi_codes, lpi_names) + positive_control <- rbind(positive_control, lpi_data) + } } + + positive_control$Zscore <- as.numeric(positive_control$Zscore) + # extra information added to excel for future reference. made in beginning of this script + positive_control$Matrix <- dims_matrix + positive_control$Rundate <- rundate + positive_control$Project <- project + + # Save results + save(positive_control, file = paste0(outdir, "/", project, "_positive_control.RData")) + # round the Z-scores to 2 digits + positive_control$Zscore <- round_df(positive_control$Zscore, 2) + write.xlsx(positive_control, + file = paste0(outdir, "/", project, "_positive_control.xlsx"), + sheetName = "Sheet1", col.names = TRUE, row.names = TRUE, append = FALSE + ) } ### SST components output #### From 94fc3feddc1d7c296137f19170c8f5e13ec80f9f Mon Sep 17 00:00:00 2001 From: Mia Pras-Raves Date: Fri, 27 Mar 2026 09:07:51 +0100 Subject: [PATCH 161/161] added fix for empty output in DIMS/preprocessing/evaluate_tics_functions.R --- DIMS/preprocessing/evaluate_tics_functions.R | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/DIMS/preprocessing/evaluate_tics_functions.R b/DIMS/preprocessing/evaluate_tics_functions.R index 318ce2b6..8cb782e2 100644 --- a/DIMS/preprocessing/evaluate_tics_functions.R +++ b/DIMS/preprocessing/evaluate_tics_functions.R @@ -30,6 +30,12 @@ find_bad_replicates <- function(repl_pattern, thresh2remove) { } } cat("\n") + if (length(remove_pos) == 0) { + remove_pos <- "none" + } + if (length(remove_neg) == 0) { + remove_neg <- "none" + } # write information on miss_infusions for both scan modes write.table(remove_pos, file = paste0("miss_infusions_positive.txt"),