RefgenDetector 3.0.5__tar.gz → 3.0.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {refgendetector-3.0.5/src/RefgenDetector.egg-info → refgendetector-3.0.7}/PKG-INFO +92 -7
  2. {refgendetector-3.0.5 → refgendetector-3.0.7}/README.md +91 -6
  3. {refgendetector-3.0.5 → refgendetector-3.0.7}/setup.py +1 -1
  4. {refgendetector-3.0.5 → refgendetector-3.0.7/src/RefgenDetector.egg-info}/PKG-INFO +92 -7
  5. {refgendetector-3.0.5 → refgendetector-3.0.7}/src/RefgenDetector.egg-info/SOURCES.txt +5 -1
  6. {refgendetector-3.0.5 → refgendetector-3.0.7}/src/refgenDetector/aligment_files.py +24 -21
  7. {refgendetector-3.0.5 → refgendetector-3.0.7}/src/refgenDetector/download_reference.py +1 -0
  8. refgendetector-3.0.7/src/refgenDetector/msgpacks/__init__.py +0 -0
  9. refgendetector-3.0.7/src/refgenDetector/msgpacks/create-plots.py +243 -0
  10. refgendetector-3.0.7/src/refgenDetector/msgpacks/create-plots2.py +220 -0
  11. refgendetector-3.0.7/src/refgenDetector/msgpacks/open_file.py +15 -0
  12. {refgendetector-3.0.5 → refgendetector-3.0.7}/src/refgenDetector/refgenDetector_main.py +1 -1
  13. {refgendetector-3.0.5 → refgendetector-3.0.7}/src/refgenDetector/variant_files.py +9 -4
  14. {refgendetector-3.0.5 → refgendetector-3.0.7}/LICENSE +0 -0
  15. {refgendetector-3.0.5 → refgendetector-3.0.7}/setup.cfg +0 -0
  16. {refgendetector-3.0.5 → refgendetector-3.0.7}/src/RefgenDetector.egg-info/dependency_links.txt +0 -0
  17. {refgendetector-3.0.5 → refgendetector-3.0.7}/src/RefgenDetector.egg-info/entry_points.txt +0 -0
  18. {refgendetector-3.0.5 → refgendetector-3.0.7}/src/RefgenDetector.egg-info/requires.txt +0 -0
  19. {refgendetector-3.0.5 → refgendetector-3.0.7}/src/RefgenDetector.egg-info/top_level.txt +0 -0
  20. {refgendetector-3.0.5 → refgendetector-3.0.7}/src/refgenDetector/__init__.py +0 -0
  21. {refgendetector-3.0.5 → refgendetector-3.0.7}/src/refgenDetector/chromosomes_dict.py +0 -0
  22. {refgendetector-3.0.5 → refgendetector-3.0.7}/src/refgenDetector/exceptions/NoFileException.py +0 -0
  23. {refgendetector-3.0.5 → refgendetector-3.0.7}/src/refgenDetector/exceptions/__init__.py +0 -0
  24. {refgendetector-3.0.5/src/refgenDetector/msgpacks → refgendetector-3.0.7/src/refgenDetector/github_msgpacks}/__init__.py +0 -0
  25. {refgendetector-3.0.5 → refgendetector-3.0.7}/src/refgenDetector/ref_manager.py +0 -0
  26. {refgendetector-3.0.5 → refgendetector-3.0.7}/src/refgenDetector/reference_genome_dictionaries.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: RefgenDetector
3
- Version: 3.0.5
3
+ Version: 3.0.7
4
4
  Author: Mireia Marin i Ginestar
5
5
  Author-email: <mireia.marin@crg.eu>
6
6
  Keywords: python
@@ -180,7 +180,9 @@ To infer the reference genome from a VCF the tool will read the VCF file in chun
180
180
 
181
181
  The msgpack files were created comparing the nucleotides in each position for hg18, GRCh37, GRCh38 and T2T. Each file contains a list of the positions where each reference had a different nucleotide (distinguishing positions).
182
182
 
183
- By getting the number of matches between these distinguishing positions and the `REF` present in the VCF we infer the reference genome version used to call the variants.
183
+ By getting the number of matches between these distinguishing positions and the `REF` present in the VCF we infer the reference genome version used to call the variants.
184
+
185
+ **Bear in mind** that the input VCF must be sorted to improve runtime performance.
184
186
 
185
187
  ## Requirements
186
188
 
@@ -197,15 +199,16 @@ Depending on how you want to install the package:
197
199
 
198
200
  1. Clone this repository
199
201
 
200
- 2.```git clone https://github.com/EGA-archive/refgenDetector.git
202
+ ```git clone https://github.com/EGA-archive/refgenDetector.git
201
203
  cd refgenDetector
202
- pip install -e . ```
204
+ pip install -e .
205
+ ```
203
206
 
204
- 3. ``$ python3 refgenDetector_main.py -h ``
207
+ 2. ``$ python3 refgenDetector_main.py -h ``
205
208
 
206
- 4. Download the `msgpack` files for the inference with VCFs: [Download the msgpack reference](https://crgcnag-my.sharepoint.com/:u:/g/personal/mimarin_crg_es/IQDa5CICZDAoRZmbfhBG3ZPEAWdVnNqvefFJB_r5Hc8aM70?e=kID7zn)
209
+ 3. Download the `msgpack` files for the inference with VCFs: [Download the msgpack reference](https://crgcnag-my.sharepoint.com/:u:/g/personal/mimarin_crg_es/IQDa5CICZDAoRZmbfhBG3ZPEAWdVnNqvefFJB_r5Hc8aM70?e=kID7zn)
207
210
 
208
- 5. Move the `msgpack` to the correct path:
211
+ 4. Move the `msgpack` to the correct path:
209
212
 
210
213
  ```
211
214
  mv msgpacks.zip /refgenDetector/src/refgenDetector/
@@ -216,9 +219,91 @@ unzip /refgenDetector/src/refgenDetector/msgpacks.zip
216
219
 
217
220
  ``$ pip install refgenDetector``
218
221
 
222
+ The reference files for the VCF inference will be automatically downloaded after the first execution of refgenDetector
223
+
224
+ ### With Docker
225
+
226
+ refgenDetector can be run using Docker, allowing it to work on Linux, Windows, and macOS without installing Python or other dependencies locally.
227
+
228
+ #### 1. Requirements
229
+
230
+ - [Docker](https://www.docker.com/) with Docker Compose installed
231
+ - The refgenDetector repository cloned locally
232
+
233
+ #### 2. Build and start the container
234
+
235
+ From the root directory of the repository, run:
236
+
237
+ ```bash
238
+ docker compose up -d --build
239
+ ```
240
+
241
+ This builds the Docker image and starts the container in the background.
242
+
243
+ For subsequent starts, use:
244
+
245
+ ```bash
246
+ docker compose up -d
247
+ ```
248
+
249
+ #### 3. Add input files
250
+
251
+ Place the files you want to analyse in the `data/` directory of the repository.
252
+
253
+ This folder is automatically mounted inside the container, so files can be added or removed without rebuilding it.
254
+
255
+ #### 4. Run refgenDetector
256
+
257
+ Execute refgenDetector using:
258
+
259
+ ```bash
260
+ docker compose exec refgendetector refgenDetector -t VCF -f sample.vcf.gz
261
+ ```
262
+
263
+ Replace `sample.vcf.gz` with your input filename.
264
+
265
+ Supported input types include:
266
+
267
+ - `VCF` — VCF files
268
+ - `BAM/CRAM` — BAM or CRAM files
269
+ - `Header` — extracted BAM/CRAM headers
270
+
271
+ For example, to analyse a BAM file:
272
+
273
+ ```bash
274
+ docker compose exec refgendetector refgenDetector -t BAM/CRAM -f sample.bam
275
+ ```
276
+
277
+ To display the available options:
278
+
279
+ ```bash
280
+ docker compose exec refgendetector refgenDetector -h
281
+ ```
282
+
283
+ **Note:** On the first execution requiring reference databases, refgenDetector automatically downloads them. These files are stored in a persistent Docker volume and reused in subsequent executions, including after rebuilding the container.
284
+
285
+ #### 5. Stop the container
286
+
287
+ ```bash
288
+ docker compose down
289
+ ```
290
+
291
+ This stops and removes the container while preserving the downloaded reference databases.
292
+
293
+ To start it again:
294
+
295
+ ```bash
296
+ docker compose up -d
297
+ ```
298
+
299
+ **Important:** Avoid using `docker compose down -v`, as this will also remove the persistent volume containing the reference databases.
300
+
219
301
 
220
302
  ## Usage
221
303
 
304
+
305
+ **Bear in mind that the VCF must be sorted for optimal execution.**
306
+
222
307
  You can get the help menu by running:
223
308
 
224
309
  ```
@@ -154,7 +154,9 @@ To infer the reference genome from a VCF the tool will read the VCF file in chun
154
154
 
155
155
  The msgpack files were created comparing the nucleotides in each position for hg18, GRCh37, GRCh38 and T2T. Each file contains a list of the positions where each reference had a different nucleotide (distinguishing positions).
156
156
 
157
- By getting the number of matches between these distinguishing positions and the `REF` present in the VCF we infer the reference genome version used to call the variants.
157
+ By getting the number of matches between these distinguishing positions and the `REF` present in the VCF we infer the reference genome version used to call the variants.
158
+
159
+ **Bear in mind** that the input VCF must be sorted to improve runtime performance.
158
160
 
159
161
  ## Requirements
160
162
 
@@ -171,15 +173,16 @@ Depending on how you want to install the package:
171
173
 
172
174
  1. Clone this repository
173
175
 
174
- 2.```git clone https://github.com/EGA-archive/refgenDetector.git
176
+ ```git clone https://github.com/EGA-archive/refgenDetector.git
175
177
  cd refgenDetector
176
- pip install -e . ```
178
+ pip install -e .
179
+ ```
177
180
 
178
- 3. ``$ python3 refgenDetector_main.py -h ``
181
+ 2. ``$ python3 refgenDetector_main.py -h ``
179
182
 
180
- 4. Download the `msgpack` files for the inference with VCFs: [Download the msgpack reference](https://crgcnag-my.sharepoint.com/:u:/g/personal/mimarin_crg_es/IQDa5CICZDAoRZmbfhBG3ZPEAWdVnNqvefFJB_r5Hc8aM70?e=kID7zn)
183
+ 3. Download the `msgpack` files for the inference with VCFs: [Download the msgpack reference](https://crgcnag-my.sharepoint.com/:u:/g/personal/mimarin_crg_es/IQDa5CICZDAoRZmbfhBG3ZPEAWdVnNqvefFJB_r5Hc8aM70?e=kID7zn)
181
184
 
182
- 5. Move the `msgpack` to the correct path:
185
+ 4. Move the `msgpack` to the correct path:
183
186
 
184
187
  ```
185
188
  mv msgpacks.zip /refgenDetector/src/refgenDetector/
@@ -190,9 +193,91 @@ unzip /refgenDetector/src/refgenDetector/msgpacks.zip
190
193
 
191
194
  ``$ pip install refgenDetector``
192
195
 
196
+ The reference files for the VCF inference will be automatically downloaded after the first execution of refgenDetector
197
+
198
+ ### With Docker
199
+
200
+ refgenDetector can be run using Docker, allowing it to work on Linux, Windows, and macOS without installing Python or other dependencies locally.
201
+
202
+ #### 1. Requirements
203
+
204
+ - [Docker](https://www.docker.com/) with Docker Compose installed
205
+ - The refgenDetector repository cloned locally
206
+
207
+ #### 2. Build and start the container
208
+
209
+ From the root directory of the repository, run:
210
+
211
+ ```bash
212
+ docker compose up -d --build
213
+ ```
214
+
215
+ This builds the Docker image and starts the container in the background.
216
+
217
+ For subsequent starts, use:
218
+
219
+ ```bash
220
+ docker compose up -d
221
+ ```
222
+
223
+ #### 3. Add input files
224
+
225
+ Place the files you want to analyse in the `data/` directory of the repository.
226
+
227
+ This folder is automatically mounted inside the container, so files can be added or removed without rebuilding it.
228
+
229
+ #### 4. Run refgenDetector
230
+
231
+ Execute refgenDetector using:
232
+
233
+ ```bash
234
+ docker compose exec refgendetector refgenDetector -t VCF -f sample.vcf.gz
235
+ ```
236
+
237
+ Replace `sample.vcf.gz` with your input filename.
238
+
239
+ Supported input types include:
240
+
241
+ - `VCF` — VCF files
242
+ - `BAM/CRAM` — BAM or CRAM files
243
+ - `Header` — extracted BAM/CRAM headers
244
+
245
+ For example, to analyse a BAM file:
246
+
247
+ ```bash
248
+ docker compose exec refgendetector refgenDetector -t BAM/CRAM -f sample.bam
249
+ ```
250
+
251
+ To display the available options:
252
+
253
+ ```bash
254
+ docker compose exec refgendetector refgenDetector -h
255
+ ```
256
+
257
+ **Note:** On the first execution requiring reference databases, refgenDetector automatically downloads them. These files are stored in a persistent Docker volume and reused in subsequent executions, including after rebuilding the container.
258
+
259
+ #### 5. Stop the container
260
+
261
+ ```bash
262
+ docker compose down
263
+ ```
264
+
265
+ This stops and removes the container while preserving the downloaded reference databases.
266
+
267
+ To start it again:
268
+
269
+ ```bash
270
+ docker compose up -d
271
+ ```
272
+
273
+ **Important:** Avoid using `docker compose down -v`, as this will also remove the persistent volume containing the reference databases.
274
+
193
275
 
194
276
  ## Usage
195
277
 
278
+
279
+ **Bear in mind that the VCF must be sorted for optimal execution.**
280
+
196
281
  You can get the help menu by running:
197
282
 
198
283
  ```
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
 
6
6
  this_directory = Path(__file__).parent
7
7
  long_description = (this_directory / "README.md").read_text()
8
- VERSION = '3.0.5'
8
+ VERSION = '3.0.7'
9
9
  DESCRIPTION = 'RefgenDetector'
10
10
 
11
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: RefgenDetector
3
- Version: 3.0.5
3
+ Version: 3.0.7
4
4
  Author: Mireia Marin i Ginestar
5
5
  Author-email: <mireia.marin@crg.eu>
6
6
  Keywords: python
@@ -180,7 +180,9 @@ To infer the reference genome from a VCF the tool will read the VCF file in chun
180
180
 
181
181
  The msgpack files were created comparing the nucleotides in each position for hg18, GRCh37, GRCh38 and T2T. Each file contains a list of the positions where each reference had a different nucleotide (distinguishing positions).
182
182
 
183
- By getting the number of matches between these distinguishing positions and the `REF` present in the VCF we infer the reference genome version used to call the variants.
183
+ By getting the number of matches between these distinguishing positions and the `REF` present in the VCF we infer the reference genome version used to call the variants.
184
+
185
+ **Bear in mind** that the input VCF must be sorted to improve runtime performance.
184
186
 
185
187
  ## Requirements
186
188
 
@@ -197,15 +199,16 @@ Depending on how you want to install the package:
197
199
 
198
200
  1. Clone this repository
199
201
 
200
- 2.```git clone https://github.com/EGA-archive/refgenDetector.git
202
+ ```git clone https://github.com/EGA-archive/refgenDetector.git
201
203
  cd refgenDetector
202
- pip install -e . ```
204
+ pip install -e .
205
+ ```
203
206
 
204
- 3. ``$ python3 refgenDetector_main.py -h ``
207
+ 2. ``$ python3 refgenDetector_main.py -h ``
205
208
 
206
- 4. Download the `msgpack` files for the inference with VCFs: [Download the msgpack reference](https://crgcnag-my.sharepoint.com/:u:/g/personal/mimarin_crg_es/IQDa5CICZDAoRZmbfhBG3ZPEAWdVnNqvefFJB_r5Hc8aM70?e=kID7zn)
209
+ 3. Download the `msgpack` files for the inference with VCFs: [Download the msgpack reference](https://crgcnag-my.sharepoint.com/:u:/g/personal/mimarin_crg_es/IQDa5CICZDAoRZmbfhBG3ZPEAWdVnNqvefFJB_r5Hc8aM70?e=kID7zn)
207
210
 
208
- 5. Move the `msgpack` to the correct path:
211
+ 4. Move the `msgpack` to the correct path:
209
212
 
210
213
  ```
211
214
  mv msgpacks.zip /refgenDetector/src/refgenDetector/
@@ -216,9 +219,91 @@ unzip /refgenDetector/src/refgenDetector/msgpacks.zip
216
219
 
217
220
  ``$ pip install refgenDetector``
218
221
 
222
+ The reference files for the VCF inference will be automatically downloaded after the first execution of refgenDetector
223
+
224
+ ### With Docker
225
+
226
+ refgenDetector can be run using Docker, allowing it to work on Linux, Windows, and macOS without installing Python or other dependencies locally.
227
+
228
+ #### 1. Requirements
229
+
230
+ - [Docker](https://www.docker.com/) with Docker Compose installed
231
+ - The refgenDetector repository cloned locally
232
+
233
+ #### 2. Build and start the container
234
+
235
+ From the root directory of the repository, run:
236
+
237
+ ```bash
238
+ docker compose up -d --build
239
+ ```
240
+
241
+ This builds the Docker image and starts the container in the background.
242
+
243
+ For subsequent starts, use:
244
+
245
+ ```bash
246
+ docker compose up -d
247
+ ```
248
+
249
+ #### 3. Add input files
250
+
251
+ Place the files you want to analyse in the `data/` directory of the repository.
252
+
253
+ This folder is automatically mounted inside the container, so files can be added or removed without rebuilding it.
254
+
255
+ #### 4. Run refgenDetector
256
+
257
+ Execute refgenDetector using:
258
+
259
+ ```bash
260
+ docker compose exec refgendetector refgenDetector -t VCF -f sample.vcf.gz
261
+ ```
262
+
263
+ Replace `sample.vcf.gz` with your input filename.
264
+
265
+ Supported input types include:
266
+
267
+ - `VCF` — VCF files
268
+ - `BAM/CRAM` — BAM or CRAM files
269
+ - `Header` — extracted BAM/CRAM headers
270
+
271
+ For example, to analyse a BAM file:
272
+
273
+ ```bash
274
+ docker compose exec refgendetector refgenDetector -t BAM/CRAM -f sample.bam
275
+ ```
276
+
277
+ To display the available options:
278
+
279
+ ```bash
280
+ docker compose exec refgendetector refgenDetector -h
281
+ ```
282
+
283
+ **Note:** On the first execution requiring reference databases, refgenDetector automatically downloads them. These files are stored in a persistent Docker volume and reused in subsequent executions, including after rebuilding the container.
284
+
285
+ #### 5. Stop the container
286
+
287
+ ```bash
288
+ docker compose down
289
+ ```
290
+
291
+ This stops and removes the container while preserving the downloaded reference databases.
292
+
293
+ To start it again:
294
+
295
+ ```bash
296
+ docker compose up -d
297
+ ```
298
+
299
+ **Important:** Avoid using `docker compose down -v`, as this will also remove the persistent volume containing the reference databases.
300
+
219
301
 
220
302
  ## Usage
221
303
 
304
+
305
+ **Bear in mind that the VCF must be sorted for optimal execution.**
306
+
222
307
  You can get the help menu by running:
223
308
 
224
309
  ```
@@ -17,4 +17,8 @@ src/refgenDetector/refgenDetector_main.py
17
17
  src/refgenDetector/variant_files.py
18
18
  src/refgenDetector/exceptions/NoFileException.py
19
19
  src/refgenDetector/exceptions/__init__.py
20
- src/refgenDetector/msgpacks/__init__.py
20
+ src/refgenDetector/github_msgpacks/__init__.py
21
+ src/refgenDetector/msgpacks/__init__.py
22
+ src/refgenDetector/msgpacks/create-plots.py
23
+ src/refgenDetector/msgpacks/create-plots2.py
24
+ src/refgenDetector/msgpacks/open_file.py
@@ -250,33 +250,36 @@ def get_info_txt(header_txt, md5, assembly):
250
250
 
251
251
  def process_data_txt(target_file, md5, assembly):
252
252
  """
253
- First function of the txt module. It opens each header in --path. gzip or uncompressed and encoded in utf-8 or
254
- iso-8859-1.
255
-
256
- Args:
257
- target_file (str): path to the file
258
-
259
- Returns:
260
- header_txt (io.TextIOWrapper): text object
253
+ Read a BAM/CRAM header from a plain-text or gzip-compressed file.
254
+ Supports UTF-8 and ISO-8859-1 encodings.
261
255
  """
262
256
  try:
263
- if os.path.isfile(target_file):
264
- with open(target_file, "r") as header_txt:
265
- get_info_txt(header_txt, md5, assembly)
266
- else:
257
+ if not os.path.isfile(target_file):
267
258
  raise NoFileException()
268
- except UnicodeError:
269
- with open(target_file, "r", encoding="iso-8859-1") as header_txt:
270
- get_info_txt(header_txt, md5, assembly)
271
- except OSError:
259
+
260
+ # Detect gzip from its magic bytes
261
+ with open(target_file, "rb") as f:
262
+ is_gzip = f.read(2) == b"\x1f\x8b"
263
+
264
+ opener = gzip.open if is_gzip else open
265
+
272
266
  try:
273
- with gzip.open(target_file, "rt") as header_txt:
267
+ with opener(target_file, "rt", encoding="utf-8") as header_txt:
274
268
  get_info_txt(header_txt, md5, assembly)
275
- except UnicodeError:
276
- with gzip.open(target_file, "rt", encoding="iso-8859-1") as header_txt:
269
+ except UnicodeDecodeError:
270
+ with opener(target_file, "rt", encoding="iso-8859-1") as header_txt:
277
271
  get_info_txt(header_txt, md5, assembly)
272
+
278
273
  except NoFileException:
279
- console.print(f"[bold]File:[/bold] {target_file} \n[bold][red]Error:[/bold][red] The path provided is not "
280
- f"found or you are using the incorrect --type option.")
274
+ console.print(
275
+ f"[bold]File:[/bold] {target_file}\n"
276
+ "[bold red]Error:[/bold red] File not found."
277
+ )
278
+
279
+ except (OSError, EOFError) as e:
280
+ console.print(
281
+ f"[bold red]Error reading file:[/bold red] {e}"
282
+ )
283
+
281
284
  except Exception as e:
282
285
  print("Unexpected error:\n", e)
@@ -57,6 +57,7 @@ def get_paths():
57
57
 
58
58
  def is_already_setup(dst: Path) -> bool:
59
59
  """Return True if at least one .msgpack file exists in dst."""
60
+ print(dst)
60
61
  if not dst.exists():
61
62
  return False
62
63
  return any(dst.glob("*.msgpack"))
@@ -0,0 +1,243 @@
1
+ """
2
+ count_and_plot.py
3
+ -----------------
4
+ Reads all msgpack files (one per assembly × chromosome) and produces:
5
+ 1. A stacked bar chart – positions per chromosome, coloured by assembly
6
+ 2. A donut / bar summary – total positions per assembly
7
+ 3. A heatmap – positions (chromosome × assembly)
8
+
9
+ Usage:
10
+ python count_and_plot.py --dir /path/to/msgpack/folder --out /path/to/output/dir
11
+
12
+ Defaults to the current directory for both --dir and --out.
13
+ """
14
+
15
+ import argparse
16
+ import os
17
+ import re
18
+ import collections
19
+ import msgpack
20
+ import matplotlib
21
+ matplotlib.use("Agg")
22
+ import matplotlib.pyplot as plt
23
+ import matplotlib.ticker as mticker
24
+ import numpy as np
25
+
26
+ # ── helpers ──────────────────────────────────────────────────────────────────
27
+
28
+ ASSEMBLIES = ["hg18", "GRCh37", "GRCh38", "T2T"]
29
+
30
+ # Natural sort order for chromosomes: 1-22, X, Y
31
+ def _chr_sort_key(c):
32
+ c = c.replace("chr", "")
33
+ if c == "X": return (23, 0)
34
+ if c == "Y": return (24, 0)
35
+ try: return (int(c), 0)
36
+ except: return (99, 0)
37
+
38
+ ASSEMBLY_COLORS = {
39
+ "hg18": "#4C72B0",
40
+ "GRCh37": "#DD8452",
41
+ "GRCh38": "#55A868",
42
+ "T2T": "#C44E52",
43
+ }
44
+
45
+ def load_counts(msgpack_dir: str) -> dict:
46
+ """
47
+ Returns counts[assembly][chrom] = int (number of distinguishing positions).
48
+ """
49
+ counts = {asm: {} for asm in ASSEMBLIES}
50
+ pattern = re.compile(r"^(hg18|GRCh37|GRCh38|T2T)-(chr[0-9XY]+)\.msgpack$")
51
+
52
+ for fname in sorted(os.listdir(msgpack_dir)):
53
+ m = pattern.match(fname)
54
+ if not m:
55
+ continue
56
+ assembly, chrom = m.group(1), m.group(2)
57
+ fpath = os.path.join(msgpack_dir, fname)
58
+ with open(fpath, "rb") as fh:
59
+ data = msgpack.unpack(fh, raw=False, strict_map_key=False)
60
+ counts[assembly][chrom] = len(data)
61
+
62
+ return counts
63
+
64
+
65
+ # ── plot 1 : stacked bar per chromosome ──────────────────────────────────────
66
+
67
+ def plot_stacked_bar(counts, out_path):
68
+ # collect all chromosomes present
69
+ all_chroms = sorted(
70
+ {c for asm in counts.values() for c in asm.keys()},
71
+ key=_chr_sort_key
72
+ )
73
+
74
+ x = np.arange(len(all_chroms))
75
+ width = 0.55
76
+
77
+ fig, ax = plt.subplots(figsize=(14, 5))
78
+ bottoms = np.zeros(len(all_chroms))
79
+
80
+ for asm in ASSEMBLIES:
81
+ vals = np.array([counts[asm].get(c, 0) for c in all_chroms])
82
+ ax.bar(x, vals, width, bottom=bottoms,
83
+ label=asm, color=ASSEMBLY_COLORS[asm], alpha=0.88)
84
+ bottoms += vals
85
+
86
+ ax.set_xticks(x)
87
+ ax.set_xticklabels(all_chroms, rotation=45, ha="right", fontsize=9)
88
+ ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{int(v):,}"))
89
+ ax.set_ylabel("Distinguishing positions", fontsize=11)
90
+ ax.set_title("Distinguishing positions per chromosome\n(stacked by assembly)", fontsize=13)
91
+ ax.legend(title="Assembly", framealpha=0.9)
92
+ ax.spines[["top", "right"]].set_visible(False)
93
+ fig.tight_layout()
94
+ fig.savefig(out_path, dpi=150)
95
+ plt.close(fig)
96
+ print(f" Saved: {out_path}")
97
+
98
+
99
+ # ── plot 2 : total per assembly (horizontal bar) ──────────────────────────────
100
+
101
+ def plot_totals(counts, out_path):
102
+ totals = {asm: sum(counts[asm].values()) for asm in ASSEMBLIES}
103
+ asms = list(totals.keys())
104
+ vals = [totals[a] for a in asms]
105
+ colors = [ASSEMBLY_COLORS[a] for a in asms]
106
+
107
+ fig, ax = plt.subplots(figsize=(7, 3.5))
108
+ bars = ax.barh(asms, vals, color=colors, height=0.5, alpha=0.9)
109
+ ax.bar_label(bars, labels=[f"{v:,}" for v in vals],
110
+ padding=6, fontsize=10)
111
+ ax.xaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{int(v):,}"))
112
+ ax.set_xlabel("Total distinguishing positions", fontsize=11)
113
+ ax.set_title("Total distinguishing positions per assembly", fontsize=13)
114
+ ax.spines[["top", "right"]].set_visible(False)
115
+ max_val = max(vals) if vals else 1
116
+ ax.set_xlim(0, max_val * 1.15)
117
+ fig.tight_layout()
118
+ fig.savefig(out_path, dpi=150)
119
+ plt.close(fig)
120
+ print(f" Saved: {out_path}")
121
+
122
+
123
+ # ── plot 3 : heatmap (chromosome × assembly) ─────────────────────────────────
124
+
125
+ def plot_heatmap(counts, out_path):
126
+ all_chroms = sorted(
127
+ {c for asm in counts.values() for c in asm.keys()},
128
+ key=_chr_sort_key
129
+ )
130
+
131
+ matrix = np.array(
132
+ [[counts[asm].get(c, 0) for asm in ASSEMBLIES] for c in all_chroms],
133
+ dtype=float
134
+ )
135
+
136
+ fig, ax = plt.subplots(figsize=(6, 9))
137
+ im = ax.imshow(matrix, aspect="auto", cmap="YlOrRd")
138
+
139
+ ax.set_xticks(range(len(ASSEMBLIES)))
140
+ ax.set_xticklabels(ASSEMBLIES, fontsize=10)
141
+ ax.set_yticks(range(len(all_chroms)))
142
+ ax.set_yticklabels(all_chroms, fontsize=8)
143
+ ax.set_title("Distinguishing positions\n(chromosome × assembly)", fontsize=12)
144
+
145
+ cb = fig.colorbar(im, ax=ax, shrink=0.6, pad=0.02)
146
+ cb.set_label("# positions", fontsize=9)
147
+ cb.formatter = mticker.FuncFormatter(lambda v, _: f"{int(v):,}")
148
+ cb.update_ticks()
149
+
150
+ # annotate cells if the matrix is not too large
151
+ if matrix.size <= 200:
152
+ for i in range(len(all_chroms)):
153
+ for j in range(len(ASSEMBLIES)):
154
+ v = int(matrix[i, j])
155
+ ax.text(j, i, f"{v:,}", ha="center", va="center",
156
+ fontsize=6.5, color="black" if v < matrix.max() * 0.7 else "white")
157
+
158
+ fig.tight_layout()
159
+ fig.savefig(out_path, dpi=150)
160
+ plt.close(fig)
161
+ print(f" Saved: {out_path}")
162
+
163
+
164
+ # ── plot 4 : grouped bar (per-chrom, side-by-side assemblies) ─────────────────
165
+
166
+ def plot_grouped_bar(counts, out_path):
167
+ all_chroms = sorted(
168
+ {c for asm in counts.values() for c in asm.keys()},
169
+ key=_chr_sort_key
170
+ )
171
+
172
+ n_chr = len(all_chroms)
173
+ n_asm = len(ASSEMBLIES)
174
+ group_w = 0.8
175
+ bar_w = group_w / n_asm
176
+ x = np.arange(n_chr)
177
+
178
+ fig, ax = plt.subplots(figsize=(16, 5))
179
+ for i, asm in enumerate(ASSEMBLIES):
180
+ offsets = x - group_w / 2 + bar_w * (i + 0.5)
181
+ vals = [counts[asm].get(c, 0) for c in all_chroms]
182
+ ax.bar(offsets, vals, width=bar_w * 0.92,
183
+ label=asm, color=ASSEMBLY_COLORS[asm], alpha=0.88)
184
+
185
+ ax.set_xticks(x)
186
+ ax.set_xticklabels(all_chroms, rotation=45, ha="right", fontsize=9)
187
+ ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{int(v):,}"))
188
+ ax.set_ylabel("Distinguishing positions", fontsize=11)
189
+ ax.set_title("Distinguishing positions per chromosome per assembly", fontsize=13)
190
+ ax.legend(title="Assembly", framealpha=0.9)
191
+ ax.spines[["top", "right"]].set_visible(False)
192
+ fig.tight_layout()
193
+ fig.savefig(out_path, dpi=150)
194
+ plt.close(fig)
195
+ print(f" Saved: {out_path}")
196
+
197
+
198
+ # ── main ──────────────────────────────────────────────────────────────────────
199
+
200
+ def main():
201
+ parser = argparse.ArgumentParser(description=__doc__,
202
+ formatter_class=argparse.RawDescriptionHelpFormatter)
203
+ parser.add_argument("--dir", default=".", help="Folder containing the msgpack files")
204
+ parser.add_argument("--out", default=".", help="Output folder for PNG plots")
205
+ args = parser.parse_args()
206
+
207
+ os.makedirs(args.out, exist_ok=True)
208
+
209
+ print(f"Loading msgpack files from: {os.path.abspath(args.dir)}")
210
+ counts = load_counts(args.dir)
211
+
212
+ # Print a summary table
213
+ all_chroms = sorted(
214
+ {c for asm in counts.values() for c in asm.keys()},
215
+ key=_chr_sort_key
216
+ )
217
+ print(f"\n{'Chrom':<10}", end="")
218
+ for asm in ASSEMBLIES:
219
+ print(f"{asm:>12}", end="")
220
+ print()
221
+ print("-" * (10 + 12 * len(ASSEMBLIES)))
222
+ for chrom in all_chroms:
223
+ print(f"{chrom:<10}", end="")
224
+ for asm in ASSEMBLIES:
225
+ print(f"{counts[asm].get(chrom, 0):>12,}", end="")
226
+ print()
227
+ print("-" * (10 + 12 * len(ASSEMBLIES)))
228
+ print(f"{'TOTAL':<10}", end="")
229
+ for asm in ASSEMBLIES:
230
+ print(f"{sum(counts[asm].values()):>12,}", end="")
231
+ print("\n")
232
+
233
+ print("Generating plots...")
234
+ plot_stacked_bar(counts, os.path.join(args.out, "01_stacked_bar_per_chrom.png"))
235
+ plot_totals (counts, os.path.join(args.out, "02_total_per_assembly.png"))
236
+ plot_heatmap (counts, os.path.join(args.out, "03_heatmap_chrom_x_assembly.png"))
237
+ plot_grouped_bar(counts, os.path.join(args.out, "04_grouped_bar_per_chrom.png"))
238
+
239
+ print("\nDone.")
240
+
241
+
242
+ if __name__ == "__main__":
243
+ main()
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ count_and_plot.py
5
+ -----------------
6
+ Reads msgpack files named as:
7
+
8
+ hg18-chr1.msgpack
9
+ GRCh37-chr1.msgpack
10
+ GRCh38-chr1.msgpack
11
+ T2T-chr1.msgpack
12
+
13
+ and creates one plot showing the number of distinguishing variants per
14
+ chromosome. Because all assemblies are expected to contain the same number
15
+ of variants, only one bar is shown per chromosome.
16
+
17
+ Usage:
18
+ python count_and_plot.py \
19
+ --dir /path/to/msgpack/folder \
20
+ --out /path/to/output/dir
21
+
22
+ Defaults to the current directory for both --dir and --out.
23
+ """
24
+
25
+ import argparse
26
+ import os
27
+ import re
28
+
29
+ import msgpack
30
+ import matplotlib
31
+
32
+ matplotlib.use("Agg")
33
+
34
+ import matplotlib.pyplot as plt
35
+ import matplotlib.ticker as mticker
36
+ import numpy as np
37
+
38
+
39
+ ASSEMBLIES = ["hg18", "GRCh37", "GRCh38", "T2T"]
40
+
41
+
42
+ def chromosome_sort_key(chromosome):
43
+ """Natural chromosome order: chr1-chr22, chrX, chrY."""
44
+ chromosome = chromosome.removeprefix("chr")
45
+
46
+ if chromosome == "X":
47
+ return 23
48
+ if chromosome == "Y":
49
+ return 24
50
+
51
+ try:
52
+ return int(chromosome)
53
+ except ValueError:
54
+ return 99
55
+
56
+
57
+ def load_counts(msgpack_dir):
58
+ """
59
+ Return:
60
+
61
+ counts[chromosome][assembly] = number of variants
62
+ """
63
+ counts = {}
64
+
65
+ pattern = re.compile(
66
+ r"^(hg18|GRCh37|GRCh38|T2T)-(chr[0-9XY]+)\.msgpack$"
67
+ )
68
+
69
+ for filename in sorted(os.listdir(msgpack_dir)):
70
+ match = pattern.match(filename)
71
+
72
+ if not match:
73
+ continue
74
+
75
+ assembly, chromosome = match.groups()
76
+ file_path = os.path.join(msgpack_dir, filename)
77
+
78
+ with open(file_path, "rb") as file_handle:
79
+ data = msgpack.unpack(
80
+ file_handle,
81
+ raw=False,
82
+ strict_map_key=False,
83
+ )
84
+
85
+ counts.setdefault(chromosome, {})
86
+ counts[chromosome][assembly] = len(data)
87
+
88
+ if not counts:
89
+ raise RuntimeError(
90
+ f"No matching msgpack files found in: {msgpack_dir}"
91
+ )
92
+
93
+ return counts
94
+
95
+
96
+ def get_chromosome_counts(counts):
97
+ """
98
+ Return one count per chromosome.
99
+
100
+ All available assemblies must have the same count for a chromosome.
101
+ """
102
+ chromosome_counts = {}
103
+
104
+ for chromosome, assembly_counts in counts.items():
105
+ unique_counts = set(assembly_counts.values())
106
+
107
+ if len(unique_counts) != 1:
108
+ details = ", ".join(
109
+ f"{assembly}={count:,}"
110
+ for assembly, count in sorted(assembly_counts.items())
111
+ )
112
+
113
+ raise ValueError(
114
+ f"Assemblies have different counts for {chromosome}: "
115
+ f"{details}"
116
+ )
117
+
118
+ chromosome_counts[chromosome] = next(iter(unique_counts))
119
+
120
+ return chromosome_counts
121
+
122
+
123
+ def plot_variants_per_chromosome(chromosome_counts, output_path):
124
+ chromosomes = sorted(
125
+ chromosome_counts,
126
+ key=chromosome_sort_key,
127
+ )
128
+
129
+ values = [chromosome_counts[chromosome] for chromosome in chromosomes]
130
+ x_positions = np.arange(len(chromosomes))
131
+
132
+ figure, axis = plt.subplots(figsize=(14, 5))
133
+
134
+ axis.bar(
135
+ x_positions,
136
+ values,
137
+ width=0.7,
138
+ alpha=0.9,
139
+ )
140
+
141
+ axis.set_xticks(x_positions)
142
+ axis.set_xticklabels(
143
+ chromosomes,
144
+ rotation=45,
145
+ ha="right",
146
+ fontsize=9,
147
+ )
148
+
149
+ axis.yaxis.set_major_formatter(
150
+ mticker.FuncFormatter(lambda value, _: f"{int(value):,}")
151
+ )
152
+
153
+ axis.set_xlabel("Chromosome", fontsize=11)
154
+ axis.set_ylabel("Number of distinguishing variants", fontsize=11)
155
+ axis.set_title(
156
+ "Number of distinguishing variants per chromosome",
157
+ fontsize=13,
158
+ )
159
+
160
+ axis.spines[["top", "right"]].set_visible(False)
161
+
162
+ figure.tight_layout()
163
+ figure.savefig(output_path, dpi=150)
164
+ plt.close(figure)
165
+
166
+ print(f"Saved: {output_path}")
167
+
168
+
169
+ def main():
170
+ parser = argparse.ArgumentParser(
171
+ description=__doc__,
172
+ formatter_class=argparse.RawDescriptionHelpFormatter,
173
+ )
174
+
175
+ parser.add_argument(
176
+ "--dir",
177
+ default=".",
178
+ help="Folder containing the msgpack files",
179
+ )
180
+
181
+ parser.add_argument(
182
+ "--out",
183
+ default=".",
184
+ help="Output folder for the PNG plot",
185
+ )
186
+
187
+ args = parser.parse_args()
188
+
189
+ os.makedirs(args.out, exist_ok=True)
190
+
191
+ print(f"Loading msgpack files from: {os.path.abspath(args.dir)}")
192
+
193
+ counts = load_counts(args.dir)
194
+ chromosome_counts = get_chromosome_counts(counts)
195
+
196
+ print(f"\n{'Chromosome':<12}{'Variants':>15}")
197
+ print("-" * 27)
198
+
199
+ for chromosome in sorted(
200
+ chromosome_counts,
201
+ key=chromosome_sort_key,
202
+ ):
203
+ print(
204
+ f"{chromosome:<12}"
205
+ f"{chromosome_counts[chromosome]:>15,}"
206
+ )
207
+
208
+ output_path = os.path.join(
209
+ args.out,
210
+ "variants_per_chromosome.png",
211
+ )
212
+
213
+ plot_variants_per_chromosome(
214
+ chromosome_counts,
215
+ output_path,
216
+ )
217
+
218
+
219
+ if __name__ == "__main__":
220
+ main()
@@ -0,0 +1,15 @@
1
+ import msgpack
2
+
3
+ path = "/home/mireia/GitHub/refgenDetector/src/refgenDetector/msgpacks/GRCh37-chr1.msgpack"
4
+
5
+ with open(path, "rb") as f:
6
+ unpacker = msgpack.Unpacker(
7
+ f,
8
+ raw=False,
9
+ strict_map_key=False # <-- THIS fixes your error
10
+ )
11
+
12
+ for i, obj in enumerate(unpacker):
13
+ print(obj)
14
+ if i == 10:
15
+ break
@@ -8,7 +8,7 @@ __maintainer__ = "Mireia Marin Ginestar"
8
8
  __email__ = "mireia.marin@crg.eu"
9
9
  __status__ = "Developement"
10
10
 
11
- version = "3.0.5"
11
+ version = "3.0.6"
12
12
 
13
13
  import os
14
14
  import sys
@@ -24,6 +24,7 @@ MSGPACK_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "msgpacks
24
24
  final_results = []
25
25
  console = Console(highlight=False)
26
26
  _msgpack_cache = {}
27
+ _current_chr = None
27
28
 
28
29
  def gather_and_sum(lists):
29
30
  """
@@ -54,13 +55,18 @@ def get_matches(snps_dict, chr_):
54
55
  A list with the number of matches for each version, which is used to infer the reference genome version.
55
56
  """
56
57
 
57
- global _msgpack_cache
58
+ global _msgpack_cache, _current_chr
58
59
  start = time.time()
59
60
 
60
61
  genome_versions = ["hg18", "GRCh37", "GRCh38", "T2T"]
61
62
  matches = []
62
63
 
63
- # Extract arrays once
64
+ # If we've moved on to a different chromosome, drop the previous
65
+ # chromosome's msgpacks before loading the new ones.
66
+ if chr_ != _current_chr:
67
+ _msgpack_cache.clear()
68
+ _current_chr = chr_
69
+
64
70
  positions = np.array(list(snps_dict.keys()), dtype=np.int64)
65
71
  nucleotides = np.array(list(snps_dict.values()))
66
72
 
@@ -76,16 +82,15 @@ def get_matches(snps_dict, chr_):
76
82
 
77
83
  ref_dict = _msgpack_cache[cache_key]
78
84
 
79
- # Vectorized: look up all positions at once, compare arrays
80
85
  ref_nucs = np.array([ref_dict.get(p, None) for p in positions])
81
86
  match_count = int(np.sum(ref_nucs == nucleotides))
82
87
 
83
88
  matches.append([version_name, match_count])
84
89
 
85
- #console.print("Getting matches. Took:", time.time() - start, "s")
86
90
  return matches
87
91
 
88
92
 
93
+
89
94
  def trimming_indels(content, ref):
90
95
  """
91
96
  If a row is longer than one position it is deleted, deleting this way any indels
File without changes
File without changes